diff --git a/.prettierrc b/.prettierrc index 387ff33b..56531352 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { "printWidth": 120, - "trailingComma": "none" + "trailingComma": "none", + "semi": false } diff --git a/examples/node/collections.js b/examples/node/collections.js index f3f85756..6ae8cf0d 100644 --- a/examples/node/collections.js +++ b/examples/node/collections.js @@ -1,8 +1,6 @@ import { Ordit } from "@sadoprotocol/ordit-sdk"; -import { base64_encode } from "./utils.js"; const WORDS = ""; -const IMG_BASE64 = base64_encode("./assets/collection/cover.png"); // relative path to image async function publish() { // Load wallet @@ -16,15 +14,15 @@ async function publish() { //publish const transaction = await Ordit.collection.publish({ - title: "Elemental", - description: "Azuki Elementals are a collection of 20,000 characters within the four domains of the Garden.", - slug: "elemental", + title: "Collection Name", + description: "Lorem ipsum something else", + slug: "collection-name", creator: { address: wallet.selectedAddress, - email: "iamsaikranthi@gmail.com", - name: "Sai Kranthi" + email: "your-email@example.com", + name: "Your Name" }, - publishers: ["n4PnWbQRkn4XxcYjsSao97D5Xx96SYAvLw"], + publishers: [""], inscriptions: [ { iid: "el-01", @@ -37,13 +35,13 @@ async function publish() { sri: "sha256-zjQXDuk++5sICrObmfWqAM5EibidXd2emZoUcU2l5Pg=" } ], - url: "https://google.com", + url: "https://example.com", publicKey: wallet.publicKey, destination: wallet.selectedAddress, changeAddress: wallet.selectedAddress, postage: 1000, - mediaContent: IMG_BASE64, - mediaType: "image/png" + mediaContent: 'Collection Name', // this will be inscribed on-chain as primary content + mediaType: "text/plain" }); const depositDetails = transaction.generateCommit(); @@ -58,7 +56,7 @@ async function publish() { // sign transaction const psbtHex = transaction.toHex(); - const sig = wallet.signPsbt(psbtHex, { finalized: true }); + const sig = wallet.signPsbt(psbtHex, { isRevealTx: true }); // console.log(JSON.stringify(sig, null, 2)) // Broadcast transaction const submittedTx = await wallet.relayTx(sig, "testnet"); @@ -117,7 +115,7 @@ async function mint() { // sign transaction const psbtHex = transaction.toHex(); - const sig = userWallet.signPsbt(psbtHex, { finalized: true }); + const sig = userWallet.signPsbt(psbtHex, { isRevealTx: true }); // console.log(JSON.stringify(sig, null, 2)) // Broadcast transaction const submittedTx = await userWallet.relayTx(sig, "testnet"); diff --git a/examples/node/create-psbt.js b/examples/node/create-psbt.js index 75bce347..514d7e3a 100644 --- a/examples/node/create-psbt.js +++ b/examples/node/create-psbt.js @@ -1,6 +1,14 @@ import { Ordit, ordit } from '@sadoprotocol/ordit-sdk' async function main() { + const MNEMONIC = ""; // Generated HD wallet seed phrase + const wallet = new Ordit({ + bip39: MNEMONIC, + network: "testnet" + }); + + wallet.setDefaultAddress('taproot') + const psbt = await ordit.transactions.createPsbt({ pubKey: '039ce27aa7666731648421004ba943b90b8273e23a175d9c58e3ec2e643a9b01d1', ins: [{ @@ -10,18 +18,12 @@ async function main() { address: 'tb1qatkgzm0hsk83ysqja5nq8ecdmtwl73zwurawww', cardinals: 1200 }], - network: 'testnet' + network: 'testnet', + satsPerByte: 9, + format: 'p2tr' }) - const WORDS = "caution curtain there off know kit market gather slim april dutch sister"; // Generated HD wallet seed phrase - const wallet = new Ordit({ - bip39: WORDS, - network: "testnet" - }); - - wallet.setDefaultAddress('taproot') - - const signature = await wallet.signPsbt(psbt.hex, { finalized: true, tweak: true }) + const signature = await wallet.signPsbt(psbt.hex) const txResponse = await wallet.relayTx(signature, 'testnet') console.log("tx >>", txResponse) diff --git a/examples/node/inscribe.js b/examples/node/inscribe.js index d0a70625..d3106988 100644 --- a/examples/node/inscribe.js +++ b/examples/node/inscribe.js @@ -1,79 +1,55 @@ -import { Ordit } from "@sadoprotocol/ordit-sdk"; //import Ordit -import { base64_encode } from "./utils"; +import { Ordit } from "@sadoprotocol/ordit-sdk" -const WORDS = ""; // Generated HD wallet seed phrase -const IMG_BASE64 = base64_encode("./azuki-small-compressed.png"); // relative path to image +const MNEMONIC = "" async function main() { - // Load wallet + // init wallet const wallet = new Ordit({ - bip39: WORDS, - network: "mainnet" + bip39: MNEMONIC, + network: "testnet" }); - // new ordinal transaction + wallet.setDefaultAddress('taproot') + + // new inscription tx const transaction = Ordit.inscription.new({ - publicKey: wallet.taprootPublicKey, + network: "testnet", + publicKey: wallet.publicKey, changeAddress: wallet.selectedAddress, destination: wallet.selectedAddress, - mediaContent: IMG_BASE64, + mediaContent: 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAADGElEQVR4nO2by04UQRSGvw34CjoGDAQ3XhDXRnTBAiG4ExMn8QlMmHh5ghE2OOMTsOPmgkSJuhiIuNCVvgFGRia6YFxoNPEGjClyOjlpZyKMU101dn1Jp5Pups7pw+mqv07VQCAQCARaTg9wDcgDc8AjYEXOc3I9C/TyH3EamAbKQO0Ah3n+HtBPmzIIlBq83DawAbwGXsp5Q67Xe74k7bUFR4CF2AvsAGtADjgLdDb42065n5Pnd2LtLAIZPOYSsKUc/grcB4412V43UAS+qDarwAgechPYVY4utPC/ZdqZV20bO7fwiLxy7jNw1ZKdcWk/sjWFB9xQDn0Azli2dwLYVDbv4Pib3xVH3ss4nwSmT6moz2EUR739lkr7UwnbP6k+h6qL0WFRpaFRby64EhsiE2Mw1tu7RI8OF5MyWlLjvGthclT8MP6sJqXta3IYkeIDReXTgG1j00reNqvwWk23ks0F28bKYshodZ94Jn69s2mkR6WambD4RE751mfLSFYZMbM2nxhQvl23rfm3gUP4RQfwU/y7a8vIrBh4i5+8Ef+MNrDCshgwFRwfeSX+PbYtgF7gJ8/FPzMiWGE57Rkwm/Y+IK9GgUaFTR9GgUlbRrJp1wG9baIEj9s0VE7zXCA+GzSzMB/oUrNBsw5hlX6VatannvukkGQ9wLeKUEZVhBL7LC+oiFsbc5uoCZ53VRUex31VeMlF6lXVuoCp0yeJWSH6JPY/AodxwIhaGaokWCPsUstjpvcfwyG3VRpWEsiE+NrgDB4wFVsdHrf4zUdpHx3fXa0N1ssEvT9gvsX7A+ZiO05m5OW9CsKo6hgjnVD8B8XYJSInGuejDm9M2fMuCBngQYM9QhOyf8BMXevRIfcnRNvH9wgt1entvQxCJJZWG+z6+gWsS2VpRc7rcr3e82t/ETneBgHR5gWZpdUOcGzKxGa/2t7rIET0SbFiUjrJJ5IBT6WTm5T7zc7n2yIIthkGvkkQfgCXSSHDIQiEIBhCJhCCsEfIBEIQ9giZwJ9iaYiUB+EhKWVIfqx1zrUjgUAggG/8BsfNc0SX+zvYAAAAAElFTkSuQmCC', mediaType: "image/png", feeRate: 15, - meta: { - title: "Elemental", - desc: "Azuki Elementals are a collection of 20,000 characters within the four domains of the Garden.", - slug: "elemental", - traits: [ - { - traitType: "Hair", - value: "Electrified Long - Black" - }, - { - traitType: "Offhand", - value: "Elemental Blade - Lightning" - }, - { - traitType: "Eyes", - value: "Enticing" - }, - { - traitType: "Type", - value: "Blue" - } - ], + meta: { // Flexible object: Record + title: "Example title", + desc: "Lorem ipsum", + slug: "cool-digital-artifact", creator: { - name: "TheArtist", + name: "Your Name", email: "artist@example.org", address: wallet.selectedAddress } }, - network: "mainnet", - postage: 1500 - }); + postage: 1500 // base value of the inscription in sats + }) - // // Get deposit address and fee for inscription - const depositDetails = transaction.generateCommit(); - console.log(depositDetails); - // // { - // // address: "", - // // revealFee: 23456, - // // } + // generate deposit address and fee for inscription + const revealed = transaction.generateCommit(); + console.log(revealed) // deposit revealFee to address - // // confirm if deposit address has been funded - const ready = await transaction.isReady(); //- true/false + // confirm if deposit address has been funded + const ready = await transaction.isReady(); if (ready || transaction.ready) { // build transaction transaction.build(); // sign transaction - const psbtHex = transaction.toHex(); - const sig = wallet.signPsbt(psbtHex); - // console.log(JSON.stringify(sig, null, 2)) + const signature = wallet.signPsbt(transaction.toHex()); + // Broadcast transaction - const submittedTx = await wallet.relayTx(sig.hex, "mainnet"); - console.log(submittedTx); - //{"txid": ""} + const tx = await wallet.relayTx(signature, "testnet"); + console.log(tx); } } diff --git a/examples/node/instant-buy.js b/examples/node/instant-buy.js new file mode 100644 index 00000000..64e644a8 --- /dev/null +++ b/examples/node/instant-buy.js @@ -0,0 +1,88 @@ +import { OrditApi, Ordit } from '@sadoprotocol/ordit-sdk' + +const BUYER_MNEMONIC = `<12-WORDS-PHRASE>` +const SELLER_MNEMONIC = `<12-WORDS-PHRASE>` + +// Initialise seller wallet +const sellerWallet = new Ordit({ + bip39: SELLER_MNEMONIC, + network: 'testnet' +}) +sellerWallet.setDefaultAddress('taproot') // Switch to address that owns inscription + +// Initialise buyer wallet +const buyerWallet = new Ordit({ + bip39: BUYER_MNEMONIC, + network: 'testnet' +}) + +// Switch to address that has enough BTC to cover the sell price + network fees +buyerWallet.setDefaultAddress('taproot') + +async function createSellOrder() { + // replace w/ inscription outputpoint you'd like to sell, price, and address to receive sell proceeds + const sellerPSBT = await Ordit.instantBuy.generateSellerPsbt({ + inscriptionOutPoint: '8d4a576aecb33b809c208d672a43fd6b175478d9454df4455ed0a2dc7eb7cbf6:0', + price: 4000, // Total sale proceeds will be price + inscription output value (4000 + 2000 = 6000 sats) + receiveAddress: sellerWallet.selectedAddress, + pubKeyType: sellerWallet.selectedAddressType, + publicKey: sellerWallet.publicKey, + network: 'testnet' + }) + + const signedSellerPSBT = sellerWallet.signPsbt(sellerPSBT.toHex(), { finalize: false, extractTx: false }) + + return signedSellerPSBT // hex +} + +async function createBuyOrder({ sellerPSBT }) { + await checkForExistingRefundableUTXOs(buyerWallet.selectedAddress) + + const buyerPSBT = await Ordit.instantBuy.generateBuyerPsbt({ + sellerPsbt: sellerPSBT, + publicKey: buyerWallet.publicKey, + pubKeyType: buyerWallet.selectedAddressType, + feeRate: 10, // set correct rate to prevent tx from getting stuck in mempool + network: 'testnet', + inscriptionOutPoint: '0f3891f61b944c31fb48b0d9e770dc9e66a4b49097027be53b078be67aca72d4:0' + }) + + const signature = buyerWallet.signPsbt(buyerPSBT.toHex()) + const tx = await buyerWallet.relayTx(signature, 'testnet') + + return tx +} + +async function checkForExistingRefundableUTXOs(address) { + const response = await OrditApi.fetch('/utxo/unspents', { + data: { + address: address, + options: { + txhex: true, + notsafetospend: false, + allowedrarity: ["common"] + } + }, + network: 'testnet' + }) + + const utxos = response.rdata + const filteredUTXOs = utxos + .filter(utxo => utxo.safeToSpend && !utxo.inscriptions.length && utxo.sats > 600 && utxo.sats <= 1000) + .sort((a, b) => a.sats - b.sats) // Sort by lowest value utxo to highest such that we spend only the ones that are lowest + + if(filteredUTXOs.length < 2) { + throw new Error("Not enough UTXOs in 600-1000 sats range. Use Ordit.instantBuy.generateDummyUtxos() to generate dummy utxos.") + } +} + +async function main() { + const signedSellerPSBT = await createSellOrder() + const tx = await createBuyOrder({ sellerPSBT: signedSellerPSBT }) + + console.log(tx) // 6dc768015dda40c3752bfc011077ae9b1445d0c9cb5b385fda6ee26dab6cb267 +} + +;(async() => { + await main() +})() \ No newline at end of file diff --git a/examples/node/package.json b/examples/node/package.json index f1e54135..b2e0e736 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -8,7 +8,8 @@ "inscribe": "node inscribe", "read": "node read", "send": "node send", - "create-psbt": "node create-psbt" + "create-psbt": "node create-psbt", + "instant-buy": "node instant-buy" }, "author": "", "license": "ISC", diff --git a/examples/node/utils.js b/examples/node/utils.js deleted file mode 100644 index 77ea1bce..00000000 --- a/examples/node/utils.js +++ /dev/null @@ -1,9 +0,0 @@ -import fs from "fs"; - -//Utility methods -export function base64_encode(file) { - // read binary data - var bitmap = fs.readFileSync(file); - // convert binary data to base64 encoded string - return Buffer.from(bitmap).toString("base64"); -} diff --git a/packages/sdk/src/api/index.ts b/packages/sdk/src/api/index.ts index 5e0dafd5..aede2609 100644 --- a/packages/sdk/src/api/index.ts +++ b/packages/sdk/src/api/index.ts @@ -20,13 +20,21 @@ export class OrditApi { throw new Error('Invalid address') } - const utxos = await rpc[network].call('GetUnspents', { - address, - options: { - allowedrarity: rarity, - safetospend: type === "spendable", - } - }, rpc.id) + const utxos = await rpc[network].call( + "GetUnspents", + { + address, + options: { + allowedrarity: rarity, + safetospend: type === "spendable" + }, + pagination: { + page: 1, + limit: 25 + } + }, + rpc.id + ) const { spendableUTXOs, unspendableUTXOs } = utxos.reduce((acc, utxo) => { if(utxo.inscriptions?.length && !utxo.safeToSpend) { diff --git a/packages/sdk/src/browser-wallets/unisat/index.ts b/packages/sdk/src/browser-wallets/unisat/index.ts index 7044e662..ebbce1e6 100644 --- a/packages/sdk/src/browser-wallets/unisat/index.ts +++ b/packages/sdk/src/browser-wallets/unisat/index.ts @@ -1,3 +1,3 @@ -export * from "./addresses"; -export * from "./signatures"; -export * from "./utils"; +export * from "./addresses" +export * from "./signatures" +export * from "./utils" diff --git a/packages/sdk/src/browser-wallets/unisat/signatures.ts b/packages/sdk/src/browser-wallets/unisat/signatures.ts index 16d86d11..13a42f21 100644 --- a/packages/sdk/src/browser-wallets/unisat/signatures.ts +++ b/packages/sdk/src/browser-wallets/unisat/signatures.ts @@ -1,15 +1,16 @@ import { Psbt } from "bitcoinjs-lib"; -import { isUnisatInstalled } from "./utils"; +import { UnisatSignPSBTOptions } from "./types" +import { isUnisatInstalled } from "./utils" -export async function signPsbt(psbt: Psbt) { +export async function signPsbt(psbt: Psbt, { finalize = true }: UnisatSignPSBTOptions = {}) { if (!isUnisatInstalled()) { throw new Error("Unisat not installed."); } const psbtHex = psbt.toHex(); - const signedPsbtHex = await window.unisat.signPsbt(psbtHex); + const signedPsbtHex = await window.unisat.signPsbt(psbtHex, { autoFinalized: finalize }) if (!signedPsbtHex) { throw new Error("Failed to sign psbt hex using Unisat."); diff --git a/packages/sdk/src/browser-wallets/unisat/types.ts b/packages/sdk/src/browser-wallets/unisat/types.ts new file mode 100644 index 00000000..75e82759 --- /dev/null +++ b/packages/sdk/src/browser-wallets/unisat/types.ts @@ -0,0 +1,3 @@ +export interface UnisatSignPSBTOptions { + finalize?: boolean +} diff --git a/packages/sdk/src/constants/index.ts b/packages/sdk/src/constants/index.ts new file mode 100644 index 00000000..51d4a71d --- /dev/null +++ b/packages/sdk/src/constants/index.ts @@ -0,0 +1,3 @@ +// amount lower than this is considered as dust value +// and majority of the miners don't pick txs w/ the following output value or lower +export const MINIMUM_AMOUNT_IN_SATS = 600 diff --git a/packages/sdk/src/inscription/commit.ts b/packages/sdk/src/inscription/commit.ts index ec5ec158..223f8e92 100644 --- a/packages/sdk/src/inscription/commit.ts +++ b/packages/sdk/src/inscription/commit.ts @@ -1,44 +1,44 @@ -import { getAddresses } from "../addresses"; -import { createTransaction } from "../utils"; -import { GetWalletOptions } from "../wallet"; -import { buildWitnessScript } from "./witness"; +import { getAddresses } from "../addresses" +import { createTransaction } from "../utils" +import { GetWalletOptions } from "../wallet" +import { buildWitnessScript } from "./witness" export async function generateCommitAddress(options: GenerateCommitAddressOptions) { - const { satsPerByte = 10, network, pubKey } = options; - const key = (await getAddresses({ pubKey, network, format: "p2tr" }))[0]; - const xkey = key.xkey; + const { satsPerByte = 10, network, pubKey } = options + const key = (await getAddresses({ pubKey, network, format: "p2tr" }))[0] + const xkey = key.xkey if (xkey) { - const witnessScript = buildWitnessScript({ ...options, xkey }); + const witnessScript = buildWitnessScript({ ...options, xkey }) if (!witnessScript) { - throw new Error("Failed to build witness script."); + throw new Error("Failed to build witness script.") } const scriptTree = { output: witnessScript - }; + } const p2tr = createTransaction(Buffer.from(xkey, "hex"), "p2tr", options.network, { scriptTree - }); + }) - const fees = JSON.parse(JSON.stringify((80 + 1 * 180) * satsPerByte)); - const scriptLength = witnessScript.toString("hex").length; - const scriptFees = (scriptLength / 10) * satsPerByte + fees; + const fees = (80 + 1 * 180) * satsPerByte + const scriptLength = witnessScript.toString("hex").length + const scriptFees = (scriptLength / 10) * satsPerByte + fees return { address: p2tr.address, xkey, format: "inscribe", fees: scriptFees - }; + } } } export type GenerateCommitAddressOptions = Omit & { - satsPerByte: number; - mediaType: string; - mediaContent: string; - meta: any; -}; + satsPerByte: number + mediaType: string + mediaContent: string + meta: any +} diff --git a/packages/sdk/src/inscription/instant-buy.ts b/packages/sdk/src/inscription/instant-buy.ts index 3cac86aa..efeceaf7 100644 --- a/packages/sdk/src/inscription/instant-buy.ts +++ b/packages/sdk/src/inscription/instant-buy.ts @@ -1,24 +1,25 @@ -import * as bitcoin from "bitcoinjs-lib"; +import * as bitcoin from "bitcoinjs-lib" import { AddressFormats, addressNameToType, AddressTypes, - calculateTxFeeWithRate, - createTransaction, + calculateTxFee, getAddressesFromPublicKey, getNetwork, + InputType, OrditApi, - toXOnly -} from ".."; -import { Network } from "../config/types"; + processInput +} from ".." +import { Network } from "../config/types" +import { MINIMUM_AMOUNT_IN_SATS } from "../constants" export async function generateSellerPsbt({ inscriptionOutPoint, price, receiveAddress, publicKey, - pubKeyType = "taproot", + pubKeyType, network = "testnet" }: GenerateSellerInstantBuyPsbtOptions) { const { inputs, outputs } = await getSellerInputsOutputs({ @@ -28,279 +29,208 @@ export async function generateSellerPsbt({ publicKey, pubKeyType, network - }); + }) - const networkObj = getNetwork("testnet"); - const psbt = new bitcoin.Psbt({ network: networkObj }); + const networkObj = getNetwork(network) + const psbt = new bitcoin.Psbt({ network: networkObj }) - psbt.addInput(inputs[0]); - psbt.addOutput(outputs[0]); + psbt.addInput(inputs[0]) + psbt.addOutput(outputs[0]) - return psbt; + return psbt } export async function generateBuyerPsbt({ publicKey, - pubKeyType = "legacy", + pubKeyType, feeRate = 10, network = "testnet", sellerPsbt, inscriptionOutPoint }: GenerateBuyerInstantBuyPsbtOptions) { - const networkObj = getNetwork(network); - const format = addressNameToType[pubKeyType]; - const address = getAddressesFromPublicKey(publicKey, network, format)[0]; - let postage = 10000; // default postage - let ordOutNumber = 0; + const networkObj = getNetwork(network) + const format = addressNameToType[pubKeyType] + const address = getAddressesFromPublicKey(publicKey, network, format)[0] + let postage = 10000 // default postage + let ordOutNumber = 0 // get postage from outpoint try { - const [ordTxId, ordOut] = inscriptionOutPoint.split(":"); + const [ordTxId, ordOut] = inscriptionOutPoint.split(":") if (!ordTxId || !ordOut) { - throw new Error("Invalid outpoint."); + throw new Error("Invalid outpoint.") } - ordOutNumber = parseInt(ordOut); + ordOutNumber = parseInt(ordOut) const { tx } = await OrditApi.fetchTx({ txId: ordTxId, network }) if (!tx) { - throw new Error("Failed to get raw transaction for id: " + ordTxId); + throw new Error("Failed to get raw transaction for id: " + ordTxId) } - const output = tx && tx.vout[ordOutNumber]; + const output = tx && tx.vout[ordOutNumber] if (!output) { - throw new Error("Outpoint not found."); + throw new Error("Outpoint not found.") } - postage = output.value * 1e8; + postage = parseInt((output.value * 1e8).toString()) } catch (error) { - throw new Error(error.message); + throw new Error(error.message) } const { totalUTXOs, spendableUTXOs } = await OrditApi.fetchUnspentUTXOs({ address: address.address!, network }) if (!totalUTXOs) { - throw new Error("No UTXOs found."); + throw new Error("No UTXOs found.") } - const psbt = new bitcoin.Psbt({ network: networkObj }); - const dummyUtxos = []; + const psbt = new bitcoin.Psbt({ network: networkObj }) + const refundableUTXOs = [] - //find dummy utxos + // find refundableUTXOs utxos for (let i = 0; i < spendableUTXOs.length; i++) { - const utxo = spendableUTXOs[i]; + const utxo = spendableUTXOs[i] - if (utxo.sats >= 580 && utxo.sats <= 1000) { - dummyUtxos.push(utxo); + if (utxo.sats >= MINIMUM_AMOUNT_IN_SATS) { + refundableUTXOs.push(utxo) } } - if (dummyUtxos.length < 2 || !spendableUTXOs.length) { - throw new Error("No suitable UTXOs found."); + if (refundableUTXOs.length < 2 || !spendableUTXOs.length) { + throw new Error("No suitable UTXOs found.") } - let totalInput = 0; + let totalInput = 0 + const witnessScripts: Buffer[] = [] + const usedUTXOTxIds: string[] = [] for (let i = 0; i < 2; i++) { - const dummyUtxo = dummyUtxos[i]; - const { rawTx } = await OrditApi.fetchTx({ txId: dummyUtxo.txid, network, hex: true }) - if (!rawTx) { - throw new Error("Failed to get raw transaction for id: " + dummyUtxo.txid); - } - - if (format !== "p2tr") { - for (const output in rawTx.outs) { - try { - rawTx.setWitness(parseInt(output), []); - } catch {} - } - } - const input: any = { - hash: dummyUtxo.txid, - index: dummyUtxo.n, - nonWitnessUtxo: rawTx.toBuffer(), - sequence: 0xfffffffd // Needs to be at least 2 below max int value to be RBF - }; - - const p2shInputRedeemScript: any = {}; - const p2shInputWitnessUTXO: any = {}; - - if (format === "p2sh") { - const p2sh = createTransaction(Buffer.from(publicKey, "hex"), format, network); - p2shInputWitnessUTXO.witnessUtxo = { - script: p2sh.output, - value: dummyUtxo.sats - }; - p2shInputRedeemScript.redeemScript = p2sh.redeem?.output; - } - - if (format === "p2tr") { - const xKey = toXOnly(Buffer.from(publicKey, "hex")); - const p2tr = createTransaction(xKey, "p2tr", network); + const refundableUTXO = refundableUTXOs[i] + if (usedUTXOTxIds.includes(refundableUTXO.txid)) continue - input.tapInternalKey = toXOnly(Buffer.from(publicKey, "hex")); - input.witnessUtxo = { - script: p2tr.output!, - value: dummyUtxo.sats - }; - } + const input = await processInput({ utxo: refundableUTXO, pubKey: publicKey, network }) - psbt.addInput({ - ...input, - ...p2shInputWitnessUTXO, - ...p2shInputRedeemScript - }); - totalInput += dummyUtxo.sats; + usedUTXOTxIds.push(input.hash) + psbt.addInput(input) + totalInput += refundableUTXO.sats } - // Add dummy output + // Add refundable output psbt.addOutput({ address: address.address!, - value: dummyUtxos[0].sats + dummyUtxos[1].sats + ordOutNumber - }); + value: refundableUTXOs[0].sats + refundableUTXOs[1].sats + }) // Add ordinal output psbt.addOutput({ address: address.address!, value: postage - }); + }) // seller psbt merge - const decodedSellerPsbt = bitcoin.Psbt.fromHex(sellerPsbt, { network: networkObj }); + const decodedSellerPsbt = bitcoin.Psbt.fromHex(sellerPsbt, { network: networkObj }) // inputs - (psbt.data.globalMap.unsignedTx as any).tx.ins[2] = (decodedSellerPsbt.data.globalMap.unsignedTx as any).tx.ins[0]; - psbt.data.inputs[2] = decodedSellerPsbt.data.inputs[0]; + ;(psbt.data.globalMap.unsignedTx as any).tx.ins[2] = (decodedSellerPsbt.data.globalMap.unsignedTx as any).tx.ins[0] + psbt.data.inputs[2] = decodedSellerPsbt.data.inputs[0] // outputs - (psbt.data.globalMap.unsignedTx as any).tx.outs[2] = (decodedSellerPsbt.data.globalMap.unsignedTx as any).tx.outs[0]; - psbt.data.outputs[2] = decodedSellerPsbt.data.outputs[0]; + ;(psbt.data.globalMap.unsignedTx as any).tx.outs[2] = (decodedSellerPsbt.data.globalMap.unsignedTx as any).tx.outs[0] + psbt.data.outputs[2] = decodedSellerPsbt.data.outputs[0] for (let i = 0; i < spendableUTXOs.length; i++) { - const utxo = spendableUTXOs[i]; - - const { rawTx } = await OrditApi.fetchTx({ txId: utxo.txid, network, hex: true }) - - if (format !== "p2tr") { - for (const output in rawTx?.outs) { - try { - rawTx.setWitness(parseInt(output), []); - } catch {} - } - } + const utxo = spendableUTXOs[i] + if (usedUTXOTxIds.includes(utxo.txid)) continue - const input: any = { - hash: utxo.txid, - index: utxo.n, - nonWitnessUtxo: rawTx?.toBuffer(), - sequence: 0xfffffffd // Needs to be at least 2 below max int value to be RBF - }; - - if (pubKeyType === "taproot") { - const xKey = toXOnly(Buffer.from(publicKey, "hex")); - const p2tr = createTransaction(xKey, "p2tr", network); - - input.tapInternalKey = toXOnly(Buffer.from(publicKey, "hex")); - input.witnessUtxo = { - script: p2tr.output!, - value: utxo.sats - }; - } + const input = await processInput({ utxo, pubKey: publicKey, network }) + input.witnessUtxo?.script && witnessScripts.push(input.witnessUtxo?.script) - psbt.addInput({ - ...input - }); + usedUTXOTxIds.push(input.hash) - totalInput += utxo.sats; + psbt.addInput(input) + totalInput += utxo.sats } - const fee = calculateTxFeeWithRate(psbt.txInputs.length, psbt.txOutputs.length, feeRate); - const totalOutput = psbt.txOutputs.reduce((partialSum, a) => partialSum + a.value, 0); + const fee = calculateTxFee({ + totalInputs: psbt.txInputs.length, + totalOutputs: psbt.txOutputs.length, + satsPerByte: feeRate, + type: pubKeyType, + additional: { witnessScripts } + }) + + const totalOutput = psbt.txOutputs.reduce((partialSum, a) => partialSum + a.value, 0) - const changeValue = totalInput - totalOutput - fee; + const changeValue = totalInput - totalOutput - fee if (changeValue < 0) { - throw new Error("Insufficient funds to buy this inscription"); + throw new Error("Insufficient funds to buy this inscription") } if (changeValue > 580) { psbt.addOutput({ address: address.address!, value: changeValue - }); + }) } - return psbt; + return psbt } -export async function generateDummyUtxos({ - value = 600, - count = 2, +export async function generateRefundableUTXOs({ + value = MINIMUM_AMOUNT_IN_SATS, publicKey, + pubKeyType, feeRate = 10, - pubKeyType = "taproot", + count = 2, network = "testnet" -}: GenerateDummyUtxos) { - const networkObj = getNetwork(network); - const format = addressNameToType[pubKeyType]; - const address = getAddressesFromPublicKey(publicKey, network, format)[0]; +}: GenerateRefundableUTXOsOptions) { + const networkObj = getNetwork(network) + const format = addressNameToType[pubKeyType] + const address = getAddressesFromPublicKey(publicKey, network, format)[0] const { totalUTXOs, spendableUTXOs } = await OrditApi.fetchUnspentUTXOs({ address: address.address!, network }) if (!totalUTXOs) { - throw new Error("No UTXOs found."); + throw new Error("No UTXOs found.") } - const psbt = new bitcoin.Psbt({ network: networkObj }); - let totalValue = 0; - let paymentUtxoCount = 0; + const psbt = new bitcoin.Psbt({ network: networkObj }) + let totalValue = 0 + let paymentUtxoCount = 0 + const witnessScripts: Buffer[] = [] for (let i = 0; i < spendableUTXOs.length; i++) { - const utxo = spendableUTXOs[i]; - const { rawTx } = await OrditApi.fetchTx({ txId: utxo.txid, network, hex: true }) - if (!rawTx) { - throw new Error("Failed to get raw transaction for id: " + utxo.txid); - } + const utxo = spendableUTXOs[i] + const input = await processInput({ utxo, pubKey: publicKey, network }) - const input: any = { - hash: utxo.txid, - index: utxo.n, - nonWitnessUtxo: rawTx.toBuffer(), - sequence: 0xfffffffd, // Needs to be at least 2 below max int value to be RBF - }; - - if (pubKeyType === "taproot") { - const xKey = toXOnly(Buffer.from(publicKey, "hex")); - const p2tr = createTransaction(xKey, "p2tr", network); - - input.tapInternalKey = toXOnly(Buffer.from(publicKey, "hex")); - input.witnessUtxo = { - script: p2tr.output!, - value: utxo.sats - }; - } + input.witnessUtxo?.script && witnessScripts.push(input.witnessUtxo?.script) + psbt.addInput(input) - psbt.addInput(input); + totalValue += utxo.sats + paymentUtxoCount += 1 - totalValue += utxo.sats; - paymentUtxoCount += 1; + const fees = calculateTxFee({ + totalInputs: paymentUtxoCount, + totalOutputs: count, + satsPerByte: feeRate, + type: pubKeyType, + additional: { witnessScripts } + }) - const fees = calculateTxFeeWithRate( - paymentUtxoCount, - count, // 2-dummy outputs - feeRate - ); if (totalValue >= value * count + fees) { - break; + break } } - const finalFees = calculateTxFeeWithRate( - paymentUtxoCount, - count, // 2-dummy outputs - feeRate - ); + const finalFees = calculateTxFee({ + totalInputs: paymentUtxoCount, + totalOutputs: count, + satsPerByte: feeRate, + type: pubKeyType, + additional: { witnessScripts } + }) - const changeValue = totalValue - value * count - finalFees; - // We must have enough value to create a dummy utxo and pay for tx fees + const changeValue = totalValue - value * count - finalFees + // We must have enough value to create a refundable utxo and pay for tx fees if (changeValue < 0) { - throw new Error(`You might have pending transactions or not enough fund`); + throw new Error(`You might have pending transactions or not enough fund`) } Array(count) @@ -309,17 +239,17 @@ export async function generateDummyUtxos({ psbt.addOutput({ address: address.address!, value: val - }); - }); + }) + }) if (changeValue > 580) { psbt.addOutput({ address: address.address!, value: changeValue - }); + }) } - return psbt; + return psbt } export async function getSellerInputsOutputs({ @@ -328,121 +258,76 @@ export async function getSellerInputsOutputs({ receiveAddress, publicKey, pubKeyType = "taproot", - network = "testnet", - side = "seller" + network = "testnet" }: GenerateSellerInstantBuyPsbtOptions) { - const format = addressNameToType[pubKeyType]; - const address = getAddressesFromPublicKey(publicKey, network, format)[0]; - - const inputs = []; - const outputs = []; + const format = addressNameToType[pubKeyType] + const address = getAddressesFromPublicKey(publicKey, network, format)[0] + const inputs: InputType[] = [] + const outputs = [] - const { totalUTXOs, unspendableUTXOs } = await OrditApi.fetchUnspentUTXOs({ address: address.address!, network, type: "all" }) + const { totalUTXOs, unspendableUTXOs } = await OrditApi.fetchUnspentUTXOs({ + address: address.address!, + network, + type: "all" + }) if (!totalUTXOs) { - throw new Error("No UTXOs found."); + throw new Error("No UTXOs found") } - let found = false; - - for (let i = 0; i < unspendableUTXOs.length; i++) { - const unspendableUTXO = unspendableUTXOs[i]; - if (unspendableUTXO.inscriptions!.find((v: any) => v.outpoint == inscriptionOutPoint)) { - if (unspendableUTXO.inscriptions!.length > 1) { - throw new Error("Multiple inscriptions! Please split them first."); - } - const { rawTx } = await OrditApi.fetchTx({ txId: unspendableUTXO.txid, network, hex: true }) - if (!rawTx) { - throw new Error("Failed to get raw transaction for id: " + unspendableUTXO.txid); - } - - if (format !== "p2tr") { - for (const output in rawTx.outs) { - try { - rawTx.setWitness(parseInt(output), []); - } catch {} - } - } - - const options: any = {}; - - const data: any = { - hash: unspendableUTXO.txid, - index: unspendableUTXO.n, - nonWitnessUtxo: rawTx.toBuffer(), - sequence: 0xfffffffd // Needs to be at least 2 below max int value to be RBF - }; - const postage = unspendableUTXO.sats; - - if (side === "seller") { - options.sighashType = bitcoin.Transaction.SIGHASH_SINGLE | bitcoin.Transaction.SIGHASH_ANYONECANPAY; - } - - if (format === "p2tr") { - const xKey = toXOnly(Buffer.from(publicKey, "hex")); - const p2tr = createTransaction(xKey, "p2tr", network); - - data.tapInternalKey = toXOnly(Buffer.from(publicKey, "hex")); - data.witnessUtxo = { - script: p2tr.output!, - value: postage - }; - } - - inputs.push({ - ...data, - ...options - }); - outputs.push({ address: receiveAddress, value: price + postage }); - - found = true; - break; - } + const utxo = unspendableUTXOs.find((utxo) => utxo.inscriptions?.find((i) => i.outpoint === inscriptionOutPoint)) + if (!utxo) { + throw new Error("Inscription not found") } - if (!found) { - throw new Error("inscription not found."); - } + const input = await processInput({ + utxo, + pubKey: publicKey, + network, + sighashType: bitcoin.Transaction.SIGHASH_SINGLE | bitcoin.Transaction.SIGHASH_ANYONECANPAY + }) + + inputs.push(input) + outputs.push({ address: receiveAddress, value: price + utxo.sats }) - return { inputs, outputs }; + return { inputs, outputs } } export interface UnspentOutput { - txId: string; - outputIndex: number; - satoshis: number; - scriptPk: string; - addressType: AddressTypes; - address: string; + txId: string + outputIndex: number + satoshis: number + scriptPk: string + addressType: AddressTypes + address: string ords: { - id: string; - offset: number; - }[]; + id: string + offset: number + }[] } export interface GenerateSellerInstantBuyPsbtOptions { - inscriptionOutPoint: string; - price: number; - receiveAddress: string; - publicKey: string; - pubKeyType?: AddressFormats; - network?: Network; - side?: "seller" | "buyer"; + inscriptionOutPoint: string + price: number + receiveAddress: string + publicKey: string + pubKeyType?: AddressFormats + network?: Network } export interface GenerateBuyerInstantBuyPsbtOptions { - publicKey: string; - pubKeyType?: AddressFormats; - network?: Network; - feeRate?: number; - inscriptionOutPoint: string; - sellerPsbt: string; + publicKey: string + pubKeyType: AddressFormats + network?: Network + feeRate?: number + inscriptionOutPoint: string + sellerPsbt: string } -export interface GenerateDummyUtxos { - value: number; - count: number; - publicKey: string; - pubKeyType?: AddressFormats; - network?: Network; - feeRate?: number; +export interface GenerateRefundableUTXOsOptions { + value: number + count: number + publicKey: string + pubKeyType: AddressFormats + network?: Network + feeRate?: number } diff --git a/packages/sdk/src/inscription/psbt.ts b/packages/sdk/src/inscription/psbt.ts index 3a2fd42d..9a5521cb 100644 --- a/packages/sdk/src/inscription/psbt.ts +++ b/packages/sdk/src/inscription/psbt.ts @@ -1,10 +1,11 @@ -import { Psbt } from "bitcoinjs-lib"; +import { Psbt } from "bitcoinjs-lib" -import { getAddresses } from "../addresses"; -import { OrditApi } from "../api"; -import { createTransaction, getNetwork } from "../utils"; -import { GetWalletOptions } from "../wallet"; -import { buildWitnessScript } from "./witness"; +import { getAddresses } from "../addresses" +import { OrditApi } from "../api" +import { MINIMUM_AMOUNT_IN_SATS } from "../constants" +import { createTransaction, getNetwork } from "../utils" +import { GetWalletOptions } from "../wallet" +import { buildWitnessScript } from "./witness" export async function createRevealPsbt(options: CreateRevealPsbtOptions) { const networkObj = getNetwork(options.network); @@ -75,7 +76,7 @@ export async function createRevealPsbt(options: CreateRevealPsbtOptions) { value: options.postage }); - if (change > 600) { + if (change > MINIMUM_AMOUNT_IN_SATS) { let changeAddress = inscribePayTx.address; if (options.changeAddress) { changeAddress = options.changeAddress; diff --git a/packages/sdk/src/transactions/OrdTransaction.ts b/packages/sdk/src/transactions/OrdTransaction.ts index d1c2e346..bf44fb64 100644 --- a/packages/sdk/src/transactions/OrdTransaction.ts +++ b/packages/sdk/src/transactions/OrdTransaction.ts @@ -1,40 +1,41 @@ -import * as ecc from "@bitcoinerlab/secp256k1"; -import * as bitcoin from "bitcoinjs-lib"; -import { Tapleaf } from "bitcoinjs-lib/src/types"; +import * as ecc from "@bitcoinerlab/secp256k1" +import * as bitcoin from "bitcoinjs-lib" +import { Tapleaf } from "bitcoinjs-lib/src/types" import { buildWitnessScript, - calculateTxFeeWithRate, + calculateTxFee, createTransaction, getAddressesFromPublicKey, getNetwork, GetWalletOptions, OnOffUnion, OrditApi -} from ".."; -import { Network } from "../config/types"; +} from ".." +import { Network } from "../config/types" +import { MINIMUM_AMOUNT_IN_SATS } from "../constants" -bitcoin.initEccLib(ecc); +bitcoin.initEccLib(ecc) export class OrdTransaction { - publicKey: string; - feeRate: number; - postage: number; - mediaType: string; - mediaContent: string; - destinationAddress: string; - changeAddress: string; - meta: object | unknown; - network: Network; - psbt: bitcoin.Psbt | null = null; - ready = false; - #xKey: string; - #feeForWitnessData: number | null = null; - #commitAddress: string | null = null; - #inscribePayTx: ReturnType | null = null; - #suitableUnspent: any = null; - #recovery = false; - #outs: Outputs = []; + publicKey: string + feeRate: number + postage: number + mediaType: string + mediaContent: string + destinationAddress: string + changeAddress: string + meta: object | unknown + network: Network + psbt: bitcoin.Psbt | null = null + ready = false + #xKey: string + #feeForWitnessData: number | null = null + #commitAddress: string | null = null + #inscribePayTx: ReturnType | null = null + #suitableUnspent: any = null + #recovery = false + #outs: Outputs = [] #safeMode: OnOffUnion constructor({ @@ -47,124 +48,125 @@ export class OrdTransaction { ...otherOptions }: OrdTransactionOptions) { if (!publicKey || !otherOptions.changeAddress || !otherOptions.destination || !otherOptions.mediaContent) { - throw new Error("Invalid options provided."); + throw new Error("Invalid options provided.") } - this.publicKey = publicKey; - this.feeRate = feeRate; - this.mediaType = mediaType; - this.network = network; - this.changeAddress = otherOptions.changeAddress; - this.destinationAddress = otherOptions.destination; - this.mediaContent = otherOptions.mediaContent; - this.meta = otherOptions.meta; - this.postage = postage; - this.#outs = outs; - this.#safeMode = !otherOptions.safeMode ? "on": otherOptions.safeMode + this.publicKey = publicKey + this.feeRate = feeRate + this.mediaType = mediaType + this.network = network + this.changeAddress = otherOptions.changeAddress + this.destinationAddress = otherOptions.destination + this.mediaContent = otherOptions.mediaContent + this.meta = otherOptions.meta + this.postage = postage + this.#outs = outs + this.#safeMode = !otherOptions.safeMode ? "on" : otherOptions.safeMode - const xKey = getAddressesFromPublicKey(publicKey, network, "p2tr")[0].xkey; + const xKey = getAddressesFromPublicKey(publicKey, network, "p2tr")[0].xkey if (!xKey) { - throw new Error("Failed to derive xKey from the provided public key."); + throw new Error("Failed to derive xKey from the provided public key.") } - this.#xKey = xKey; + this.#xKey = xKey } get outs() { - return this.#outs; + return this.#outs } build() { if (!this.#suitableUnspent || !this.#inscribePayTx) { - throw new Error("Failed to build PSBT. Transaction not ready."); + throw new Error("Failed to build PSBT. Transaction not ready.") } - let fees = this.#feeForWitnessData!; + let fees = this.#feeForWitnessData! if (this.#recovery) { - fees = calculateTxFeeWithRate(1, 0, this.feeRate, 1); + fees = calculateTxFee({ + totalInputs: 1, + totalOutputs: 1, // change output + satsPerByte: this.feeRate, + type: "taproot" // hardcoding because recovery is only supported by Taproot txs + }) } const customOutsAmount = this.#outs.reduce((acc, cur) => { - return acc + cur.value; - }, 0); - const change = this.#suitableUnspent.sats - fees - customOutsAmount - this.postage; - - const networkObj = getNetwork(this.network); - - const psbt = new bitcoin.Psbt({ network: networkObj }); - - try { - psbt.addInput({ - sequence: 0xfffffffd, // Needs to be at least 2 below max int value to be RBF - hash: this.#suitableUnspent.txid, - index: parseInt(this.#suitableUnspent.n), - tapInternalKey: Buffer.from(this.#xKey, "hex"), - witnessUtxo: { - script: this.#inscribePayTx.output!, - value: parseInt(this.#suitableUnspent.sats) - }, - tapLeafScript: [ - { - leafVersion: this.#inscribePayTx.redeemVersion!, - script: this.#inscribePayTx.redeem!.output!, - controlBlock: this.#inscribePayTx.witness![this.#inscribePayTx.witness!.length - 1] - } - ] - }); - - if (!this.#recovery) { - psbt.addOutput({ - address: this.destinationAddress, - value: this.postage - }); - - this.#outs.forEach((out) => { - psbt.addOutput(out); - }); - } - - if (change > 600) { - let changeAddress = this.#inscribePayTx.address; - if (this.changeAddress) { - changeAddress = this.changeAddress; + return acc + cur.value + }, 0) + const change = this.#suitableUnspent.sats - fees - customOutsAmount - this.postage + + const networkObj = getNetwork(this.network) + + const psbt = new bitcoin.Psbt({ network: networkObj }) + + psbt.addInput({ + sequence: 0xfffffffd, // Needs to be at least 2 below max int value to be RBF + hash: this.#suitableUnspent.txid, + index: parseInt(this.#suitableUnspent.n), + tapInternalKey: Buffer.from(this.#xKey, "hex"), + witnessUtxo: { + script: this.#inscribePayTx.output!, + value: parseInt(this.#suitableUnspent.sats) + }, + tapLeafScript: [ + { + leafVersion: this.#inscribePayTx.redeemVersion!, + script: this.#inscribePayTx.redeem!.output!, + controlBlock: this.#inscribePayTx.witness![this.#inscribePayTx.witness!.length - 1] } + ] + }) + + if (!this.#recovery) { + psbt.addOutput({ + address: this.destinationAddress, + value: this.postage + }) - psbt.addOutput({ - address: changeAddress!, - value: change - }); + this.#outs.forEach((out) => { + psbt.addOutput(out) + }) + } + + if (change > MINIMUM_AMOUNT_IN_SATS) { + let changeAddress = this.#inscribePayTx.address + if (this.changeAddress) { + changeAddress = this.changeAddress } - this.psbt = psbt; - } catch (error) { - throw new Error(error.message); + psbt.addOutput({ + address: changeAddress!, + value: change + }) } + + this.psbt = psbt } toPsbt() { if (!this.psbt) { - throw new Error("No PSBT found. Please build first."); + throw new Error("No PSBT found. Please build first.") } - return this.psbt; + return this.psbt } toHex() { if (!this.psbt) { - throw new Error("No PSBT found. Please build first."); + throw new Error("No PSBT found. Please build first.") } - return this.psbt.toHex(); + return this.psbt.toHex() } toBase64() { if (!this.psbt) { - throw new Error("No PSBT found. Please build first."); + throw new Error("No PSBT found. Please build first.") } - return this.psbt.toBase64(); + return this.psbt.toBase64() } generateCommit() { @@ -173,17 +175,17 @@ export class OrdTransaction { mediaType: this.mediaType, meta: this.meta, xkey: this.#xKey - }); + }) const recoverScript = buildWitnessScript({ mediaContent: this.mediaContent, mediaType: this.mediaType, meta: this.meta, xkey: this.#xKey, recover: true - }); + }) if (!witnessScript || !recoverScript) { - throw new Error("Failed to build createRevealPsbt"); + throw new Error("Failed to build createRevealPsbt") } const scriptTree: [Tapleaf, Tapleaf] = [ @@ -193,38 +195,44 @@ export class OrdTransaction { { output: recoverScript } - ]; + ] const redeemScript = { output: witnessScript, redeemVersion: 192 - }; + } const inscribePayTx = createTransaction(Buffer.from(this.#xKey, "hex"), "p2tr", this.network, { scriptTree: scriptTree, redeem: redeemScript - }); + }) + + // inscription tx always have 1 input and 1 output + const fees = calculateTxFee({ + totalInputs: 1, + totalOutputs: 1, + satsPerByte: this.feeRate, + type: "taproot", // hardcoding because this process is only supported by Taproot txs + additional: { witnessScripts: [witnessScript] } + }) - const fees = JSON.parse(JSON.stringify((80 + 1 * 180) * this.feeRate)); - const scriptLength = witnessScript.toString("hex").length; - const scriptFees = Math.ceil((scriptLength / 10) * this.feeRate + fees); const customOutsAmount = this.#outs.reduce((acc, cur) => { - return acc + cur.value; - }, 0); + return acc + cur.value + }, 0) - this.#feeForWitnessData = scriptFees; - this.#commitAddress = inscribePayTx.address!; - this.#inscribePayTx = inscribePayTx; + this.#feeForWitnessData = fees + this.#commitAddress = inscribePayTx.address! + this.#inscribePayTx = inscribePayTx return { address: inscribePayTx.address!, - revealFee: this.postage + scriptFees + customOutsAmount - }; + revealFee: this.postage + fees + customOutsAmount + } } recover() { if (!this.#inscribePayTx || !this.ready) { - throw new Error("Transaction not ready."); + throw new Error("Transaction not ready.") } const witnessScript = buildWitnessScript({ @@ -232,17 +240,17 @@ export class OrdTransaction { mediaType: this.mediaType, meta: this.meta, xkey: this.#xKey - }); + }) const recoverScript = buildWitnessScript({ mediaContent: this.mediaContent, mediaType: this.mediaType, meta: this.meta, xkey: this.#xKey, recover: true - }); + }) if (!witnessScript || !recoverScript) { - throw new Error("Failed to build createRevealPsbt"); + throw new Error("Failed to build createRevealPsbt") } const scriptTree: [Tapleaf, Tapleaf] = [ @@ -252,79 +260,83 @@ export class OrdTransaction { { output: recoverScript } - ]; + ] const redeemScript = { output: recoverScript, redeemVersion: 192 - }; + } const inscribePayTx = createTransaction(Buffer.from(this.#xKey, "hex"), "p2tr", this.network, { scriptTree: scriptTree, redeem: redeemScript - }); + }) - this.#inscribePayTx = inscribePayTx; - this.#recovery = true; + this.#inscribePayTx = inscribePayTx + this.#recovery = true } async isReady() { if (!this.#commitAddress || !this.#feeForWitnessData) { - throw new Error("No commit address found. Please generate a commit address."); + throw new Error("No commit address found. Please generate a commit address.") } if (!this.ready) { try { - await this.fetchAndSelectSuitableUnspent(); + await this.fetchAndSelectSuitableUnspent() } catch (error) { - return false; + return false } } - return this.ready; + return this.ready } async fetchAndSelectSuitableUnspent() { if (!this.#commitAddress || !this.#feeForWitnessData) { - throw new Error("No commit address found. Please generate a commit address."); + throw new Error("No commit address found. Please generate a commit address.") } - const { spendableUTXOs } = await OrditApi.fetchUnspentUTXOs({ - address: this.#commitAddress, network: this.network, + const { spendableUTXOs } = await OrditApi.fetchUnspentUTXOs({ + address: this.#commitAddress, + network: this.network }) const customOutsAmount = this.#outs.reduce((acc, cur) => { - return acc + cur.value; - }, 0); + return acc + cur.value + }, 0) const suitableUTXO = spendableUTXOs.find((utxo) => { - if (utxo.sats >= this.postage + this.#feeForWitnessData! + customOutsAmount && (this.#safeMode === 'off' || (this.#safeMode === 'on' && utxo.safeToSpend === true))) { - return true; + if ( + utxo.sats >= this.postage + this.#feeForWitnessData! + customOutsAmount && + (this.#safeMode === "off" || (this.#safeMode === "on" && utxo.safeToSpend === true)) + ) { + return true } - }, this); + }, this) if (!suitableUTXO) { - throw new Error("No suitable unspent found for reveal."); + throw new Error("No suitable unspent found for reveal.") } - this.#suitableUnspent = suitableUTXO; - this.ready = true; + this.#suitableUnspent = suitableUTXO + this.ready = true - return suitableUTXO; + return suitableUTXO } } -export type OrdTransactionOptions = Pick & { - feeRate?: number; - postage?: number; - mediaType?: string; - mediaContent: string; - destination: string; - changeAddress: string; - meta?: object | unknown; - network?: Network; - publicKey: string; - outs?: Outputs; -}; - -type Outputs = Array<{ address: string; value: number }>; +export type OrdTransactionOptions = Pick & { + feeRate?: number + postage?: number + mediaType?: string + mediaContent: string + destination: string + changeAddress: string + meta?: object | unknown + network?: Network + publicKey: string + outs?: Outputs +} + +type Outputs = Array<{ address: string; value: number }> diff --git a/packages/sdk/src/transactions/psbt.ts b/packages/sdk/src/transactions/psbt.ts index fcc73208..84ba48b7 100644 --- a/packages/sdk/src/transactions/psbt.ts +++ b/packages/sdk/src/transactions/psbt.ts @@ -1,216 +1,277 @@ -import * as ecc from "@bitcoinerlab/secp256k1"; -import BIP32Factory, { BIP32API } from "bip32"; -import { Network, Psbt } from "bitcoinjs-lib"; +import * as ecc from "@bitcoinerlab/secp256k1" +import { BIP32Factory } from "bip32" +import { Psbt, Transaction } from "bitcoinjs-lib" -import { OrditApi } from "../api"; -import { createTransaction, getNetwork } from "../utils"; -import { GetWalletOptions, getWalletWithBalances } from "../wallet"; +import { getAddressType } from "../addresses" +import { addressTypeToName } from "../addresses/formats" +import { OrditApi } from "../api" +import { Network } from "../config/types" +import { MINIMUM_AMOUNT_IN_SATS } from "../constants" +import { calculateTxFee, createTransaction, getNetwork, toXOnly } from "../utils" +import { GetWalletOptions } from "../wallet" +import { UTXO } from "./types" + +const bip32 = BIP32Factory(ecc) export async function createPsbt({ network, - format, pubKey, ins, outs, + satsPerByte = 10, safeMode = "on", - satsPerByte + enableRBF = true }: CreatePsbtOptions) { - const netWorkObj = getNetwork(network); - const bip32 = BIP32Factory(ecc); - - const walletWithBalances = await getWalletWithBalances({ - pubKey, - format, + if (!ins.length || !outs.length) { + throw new Error("Invalid request") + } + const { address } = ins[0] + const { spendableUTXOs, unspendableUTXOs, totalUTXOs } = await OrditApi.fetchUnspentUTXOs({ + address, network, - safeMode - }); - - if (walletWithBalances?.spendables === undefined) { - throw new Error( - "The derived wallet doesn't contain any spendable sats. It may be empty or contain sats that are either linked to inscriptions or tied to ordinals." - ); - } - - let fees = 0; - let change = 0; - const dust = 600; - let inputs_used = 0; - const sats_per_byte = satsPerByte ?? 10; - let total_cardinals_to_send = 0; - let total_cardinals_available = 0; - const unsupported_inputs = []; - const unspents_to_use = []; - const xverse_inputs = []; - - const psbt = new Psbt({ network: netWorkObj }); - - outs.forEach((output) => { - try { - if (!output.cardinals) throw new Error("No cardinals in output."); - - total_cardinals_to_send = +parseInt(output.cardinals); - psbt.addOutput({ - address: output.address, - value: parseInt(output.cardinals) - }); - } catch (error) { - //handle error - } - }); - - for (const [idx, input] of ins.entries()) { - if (input.address) { - for (const spendable of walletWithBalances.spendables) { - const sats = spendable.sats; - const scriptPubKeyAddress = spendable.scriptPubKey.address; - const scriptPubKeyType = spendable.scriptPubKey.type as string; - - fees = JSON.parse(JSON.stringify((80 + (inputs_used + 1) * 180) * sats_per_byte)); - - if (input.address === "any") { - ins[idx].address = scriptPubKeyAddress; - } - - if (input.address === scriptPubKeyAddress) { - const addedInputSuccessfully = await addInputToPsbtByType( - spendable, - scriptPubKeyType, - psbt, - bip32, - netWorkObj - ); - - if (addedInputSuccessfully) { - unspents_to_use.push(spendable); - total_cardinals_available += sats; - xverse_inputs.push({ - address: scriptPubKeyAddress, - signingIndexes: [inputs_used] - }); - inputs_used++; - } else { - unsupported_inputs.push(spendable); - } - } - } - } - } - - if (!unspents_to_use.length) { - throw new Error( - `Not enough input value to cover outputs and fees. Total cardinals available: '${total_cardinals_available}'. Cardinals to send: '${total_cardinals_to_send}'. Estimated fees: '${fees}'.` - ); - } - - change = total_cardinals_available - (total_cardinals_to_send + fees); - - if (change < 0) { - throw new Error(`Insufficient balance for tx. Deposit ${change * -1} sats or adjust transfer amount to proceed`); - } - - if (change >= dust) { + type: safeMode === "off" ? "all" : "spendable" + }) + + if (!totalUTXOs) { + throw new Error("No spendable UTXOs") + } + + const nativeNetwork = getNetwork(network) + const psbt = new Psbt({ network: nativeNetwork }) + const inputSats = spendableUTXOs + .concat(safeMode === "off" ? unspendableUTXOs : []) + .reduce((acc, utxo) => (acc += utxo.sats), 0) + const outputSats = outs.reduce((acc, utxo) => (acc += utxo.cardinals), 0) + + // add inputs + const witnessScripts: Buffer[] = [] + for (const utxo of spendableUTXOs) { + if (utxo.scriptPubKey.address !== address) continue + + const payload = await processInput({ utxo, pubKey, network, enableRBF }) + payload.witnessUtxo?.script && witnessScripts.push(payload.witnessUtxo?.script) + psbt.addInput(payload) + } + + const fees = calculateTxFee({ + totalInputs: totalUTXOs, // select only relevant utxos to spend. NOT ALL! + totalOutputs: outs.length, + satsPerByte, + type: addressTypeToName[getAddressType(address, network)], + additional: { witnessScripts } + }) + + const remainingBalance = inputSats - outputSats - fees + if (remainingBalance < 0) { + throw new Error(`Insufficient balance. Available: ${inputSats}. Attemping to spend: ${outputSats}. Fees: ${fees}`) + } + + const isChangeOwed = remainingBalance > MINIMUM_AMOUNT_IN_SATS + if (isChangeOwed) { + outs.push({ + address, + cardinals: remainingBalance + }) + } + + // add outputs + outs.forEach((out) => { psbt.addOutput({ - address: ins[0].address, - value: change - }); + address: out.address, + value: out.cardinals + }) + }) + + return { + hex: psbt.toHex(), + base64: psbt.toBase64() } +} + +export async function processInput({ + utxo, + pubKey, + network, + enableRBF = true, + ...options +}: Omit): Promise { + const { rawTx } = await OrditApi.fetchTx({ txId: utxo.txid, network, hex: true }) + + switch (utxo.scriptPubKey.type) { + case "witness_v1_taproot": + return generateTaprootInput({ utxo, pubKey, network, rawTx, enableRBF, ...options }) - const psbtHex = psbt.toHex(); - const psbtBase64 = psbt.toBase64(); + case "witness_v0_scripthash": + return generateSegwitInput({ utxo, pubKey, network, rawTx, enableRBF, ...options }) + + case "scripthash": + return generateNestedSegwitInput({ utxo, pubKey, network, rawTx, enableRBF, ...options }) + + case "pubkeyhash": + return generateLegacyInput({ utxo, rawTx, enableRBF, ...options }) + + default: + throw new Error("invalid script pub type") + } +} + +function generateTaprootInput({ + utxo, + pubKey, + network, + enableRBF, + sighashType, + rawTx +}: ProcessInputOptions): TaprootInputType { + const chainCode = Buffer.alloc(32) + chainCode.fill(1) + + const key = bip32.fromPublicKey(Buffer.from(pubKey, "hex"), chainCode, getNetwork(network)) + const childNodeXOnlyPubkey = toXOnly(key.publicKey) + + const p2tr = createTransaction(childNodeXOnlyPubkey, "p2tr", network) + if (!p2tr || !p2tr.output) { + throw new Error("Unable to process p2tr input") + } return { - hex: psbtHex, - base64: psbtBase64 - }; + hash: utxo.txid, + index: utxo.n, + sequence: enableRBF ? 0xfffffffd : undefined, + tapInternalKey: childNodeXOnlyPubkey, + nonWitnessUtxo: rawTx?.toBuffer() ?? undefined, + witnessUtxo: { + script: p2tr.output, + value: utxo.sats + }, + ...(sighashType ? { sighashType } : undefined) + } } -async function addInputToPsbtByType(spendable: any, type: string, psbt: Psbt, bip32: BIP32API, network: Network) { - if (type === "witness_v1_taproot") { - const chainCode = Buffer.alloc(32); - chainCode.fill(1); - - let childNodeXOnlyPubkey = Buffer.from(spendable.pub, "hex"); - try { - const key = bip32.fromPublicKey(Buffer.from(spendable.pub, "hex"), chainCode, network); - childNodeXOnlyPubkey = key.publicKey.subarray(1, 33); - } catch (error) { - // fail silently - } - - const p2tr = createTransaction(childNodeXOnlyPubkey, "p2tr", network); - - if (p2tr && p2tr.output) { - psbt.addInput({ - hash: spendable.txid, - index: parseInt(spendable.n), - tapInternalKey: childNodeXOnlyPubkey, - witnessUtxo: { - script: p2tr.output, - value: parseInt(spendable.sats) - } - }); - - return true; - } - } else if (type === "witness_v0_keyhash") { - try { - const p2wpkh = createTransaction(Buffer.from(spendable.pub, "hex"), "p2wpkh", network); - - if (p2wpkh && p2wpkh.output) { - psbt.addInput({ - hash: spendable.txid, - index: parseInt(spendable.n), - witnessUtxo: { - script: p2wpkh.output, - value: parseInt(spendable.sats) - } - }); - } - - return true; - } catch (e) { - //fail silently - } - } else if (type === "scripthash") { - try { - const p2sh = createTransaction(Buffer.from(spendable.pub, "hex"), "p2sh", network); - - if (p2sh && p2sh.output && p2sh.redeem) { - psbt.addInput({ - hash: spendable.txid, - index: parseInt(spendable.n), - redeemScript: p2sh.redeem.output, - witnessUtxo: { - script: p2sh.output, - value: parseInt(spendable.sats) - } - }); - - return true; - } - } catch (error) { - //fail silently - } - } else if (type === "pubkeyhash") { - const { tx } = await OrditApi.fetchTx({ txId: spendable.txid, hex: true, ordinals: false }); - try { - psbt.addInput({ - hash: spendable.txid, - index: spendable.n, - nonWitnessUtxo: Buffer.from(tx.hex!, "hex") - }); - - return true; - } catch (e) { - //fail silently - } - } - - return false; +function generateSegwitInput({ utxo, pubKey, network, enableRBF, sighashType }: ProcessInputOptions): SegwitInputType { + const p2wpkh = createTransaction(Buffer.from(pubKey, "hex"), "p2wpkh", network) + if (!p2wpkh || !p2wpkh.output) { + throw new Error("Unable to process Segwit input") + } + + return { + hash: utxo.txid, + index: utxo.n, + sequence: enableRBF ? 0xfffffffd : undefined, + witnessUtxo: { + script: p2wpkh.output, + value: utxo.sats + }, + ...(sighashType ? { sighashType } : undefined) + } } +function generateNestedSegwitInput({ + utxo, + pubKey, + network, + enableRBF, + sighashType +}: ProcessInputOptions): NestedSegwitInputType { + const p2sh = createTransaction(Buffer.from(pubKey, "hex"), "p2sh", network) + if (!p2sh || !p2sh.output || !p2sh.redeem) { + throw new Error("Unable to process Segwit input") + } + + return { + hash: utxo.txid, + index: utxo.n, + sequence: enableRBF ? 0xfffffffd : undefined, + redeemScript: p2sh.redeem.output, + witnessUtxo: { + script: p2sh.output, + value: utxo.sats + }, + ...(sighashType ? { sighashType } : undefined) + } +} + +async function generateLegacyInput({ + utxo, + enableRBF, + sighashType, + rawTx +}: Omit): Promise { + if (!rawTx) { + throw new Error("Unable to process Legacy input") + } + + return { + hash: utxo.txid, + index: utxo.n, + sequence: enableRBF ? 0xfffffffd : undefined, + nonWitnessUtxo: rawTx.toBuffer(), + ...(sighashType ? { sighashType } : undefined) + } +} + +// TODO: replace below interfaces and custom types w/ PsbtInputExtended from bitcoinjs-lib +interface TaprootInputType { + hash: string + index: number + sequence?: number + sighashType?: number + tapInternalKey?: Buffer + witnessUtxo?: { + script: Buffer + value: number + } + nonWitnessUtxo?: Buffer +} + +interface SegwitInputType { + hash: string + index: number + sequence?: number + sighashType?: number + witnessUtxo?: { + script: Buffer + value: number + } + nonWitnessUtxo?: Buffer +} + +interface NestedSegwitInputType { + hash: string + index: number + sequence?: number + sighashType?: number + redeemScript?: Buffer | undefined + witnessUtxo?: { + script: Buffer + value: number + } + nonWitnessUtxo?: Buffer +} + +interface LegacyInputType { + hash: string + index: number + sequence?: number + sighashType?: number + nonWitnessUtxo?: Buffer + witnessUtxo?: never +} + +export type InputType = TaprootInputType | SegwitInputType | NestedSegwitInputType | LegacyInputType + export type CreatePsbtOptions = GetWalletOptions & { - satsPerByte?: number; - ins: any[]; - outs: any[]; -}; + satsPerByte?: number + ins: any[] + outs: any[] + enableRBF: boolean +} + +interface ProcessInputOptions { + utxo: UTXO + pubKey: string + network: Network + enableRBF?: boolean + sighashType?: number + rawTx?: Transaction +} diff --git a/packages/sdk/src/types.d.ts b/packages/sdk/src/types.d.ts index ff658f32..d55f8a8b 100644 --- a/packages/sdk/src/types.d.ts +++ b/packages/sdk/src/types.d.ts @@ -1,19 +1,19 @@ declare interface Window { - unisat: Unisat; - satsConnect: any; - ethereum: MetaMask; + unisat: Unisat + satsConnect: any + ethereum: MetaMask } type Unisat = { - getNetwork: () => Promise; - switchNetwork: (targetNetwork: UnisatNetwork) => Promise; - requestAccounts: () => Promise; - getPublicKey: () => Promise; - signPsbt: (hex: string) => Promise; - signMessage: (message: string) => Promise; -}; + getNetwork: () => Promise + switchNetwork: (targetNetwork: UnisatNetwork) => Promise + requestAccounts: () => Promise + getPublicKey: () => Promise + signPsbt: (hex: string, { autoFinalized }: Record) => Promise + signMessage: (message: string) => Promise +} type MetaMask = { - isMetaMask: boolean; - request: (options: { method: string; params?: any }) => Promise; -}; + isMetaMask: boolean + request: (options: { method: string; params?: any }) => Promise +} diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts index b51a41fc..f588e2cb 100644 --- a/packages/sdk/src/utils/index.ts +++ b/packages/sdk/src/utils/index.ts @@ -1,17 +1,18 @@ -import * as ecc from "@bitcoinerlab/secp256k1"; -import { BIP32Interface } from "bip32"; -import * as bitcoin from "bitcoinjs-lib"; -import ECPairFactory from "ecpair"; +import * as ecc from "@bitcoinerlab/secp256k1" +import { BIP32Interface } from "bip32" +import * as bitcoin from "bitcoinjs-lib" +import ECPairFactory from "ecpair" -import { AddressFormats, AddressTypes } from "../addresses/formats"; -import { Network } from "../config/types"; +import { AddressFormats, AddressTypes } from "../addresses/formats" +import { Network } from "../config/types" +import { CalculateTxFeeOptions, CalculateTxVirtualSizeOptions } from "./types" export function getNetwork(value: Network) { if (value === "mainnet") { - return bitcoin.networks["bitcoin"]; + return bitcoin.networks["bitcoin"] } - return bitcoin.networks[value]; + return bitcoin.networks[value] } export function createTransaction( @@ -20,21 +21,21 @@ export function createTransaction( network: Network | bitcoin.Network, paymentOptions?: bitcoin.Payment ) { - bitcoin.initEccLib(ecc); - const networkObj = typeof network === "string" ? getNetwork(network) : network; + bitcoin.initEccLib(ecc) + const networkObj = typeof network === "string" ? getNetwork(network) : network if (type === "p2tr") { - return bitcoin.payments.p2tr({ internalPubkey: key, network: networkObj, ...paymentOptions }); + return bitcoin.payments.p2tr({ internalPubkey: key, network: networkObj, ...paymentOptions }) } if (type === "p2sh") { return bitcoin.payments.p2sh({ redeem: bitcoin.payments.p2wpkh({ pubkey: key, network: networkObj }), network: networkObj - }); + }) } - return bitcoin.payments[type]({ pubkey: key, network: networkObj }); + return bitcoin.payments[type]({ pubkey: key, network: networkObj }) } export function getDerivationPath(formatType: AddressFormats, account = 0, addressIndex = 0) { @@ -43,8 +44,8 @@ export function getDerivationPath(formatType: AddressFormats, account = 0, addre "nested-segwit": `m/49'/0'/${account}'/0/${addressIndex}`, segwit: `m/84'/0'/${account}'/0/${addressIndex}`, taproot: `m/86'/0'/${account}'/0/${addressIndex}` - }; - return pathFormat[formatType]; + } + return pathFormat[formatType] } export function hdNodeToChild( @@ -53,53 +54,87 @@ export function hdNodeToChild( addressIndex = 0, account = 0 ) { - const fullDerivationPath = getDerivationPath(formatType, account, addressIndex); - - return node.derivePath(fullDerivationPath); -} + const fullDerivationPath = getDerivationPath(formatType, account, addressIndex) -export function calculateTxFeeWithRate( - inputsLength: number, - outputsLength: number, - feeRate = 10, - hasChangeOutput: 0 | 1 = 1 -): number { - const baseTxSize = 10; - const inSize = 180; - const outSize = 34; - - const txSize = baseTxSize + inputsLength * inSize + outputsLength * outSize + hasChangeOutput * outSize; - const fee = txSize * feeRate; - return fee; + return node.derivePath(fullDerivationPath) } export function toXOnly(pubkey: Buffer): Buffer { - return pubkey.subarray(1, 33); + return pubkey.subarray(1, 33) } export function tweakSigner(signer: bitcoin.Signer, opts: any = {}): bitcoin.Signer { - const ECPair = ECPairFactory(ecc); + const ECPair = ECPairFactory(ecc) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - let privateKey: Uint8Array | undefined = signer.privateKey!; + let privateKey: Uint8Array | undefined = signer.privateKey! if (!privateKey) { - throw new Error("Private key is required for tweaking signer!"); + throw new Error("Private key is required for tweaking signer!") } if (signer.publicKey[0] === 3) { - privateKey = ecc.privateNegate(privateKey); + privateKey = ecc.privateNegate(privateKey) } - const tweakedPrivateKey = ecc.privateAdd(privateKey, tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash)); + const tweakedPrivateKey = ecc.privateAdd(privateKey, tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash)) if (!tweakedPrivateKey) { - throw new Error("Invalid tweaked private key!"); + throw new Error("Invalid tweaked private key!") } return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { network: opts.network - }); + }) } export function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { - return bitcoin.crypto.taggedHash("TapTweak", Buffer.concat(h ? [pubKey, h] : [pubKey])); + return bitcoin.crypto.taggedHash("TapTweak", Buffer.concat(h ? [pubKey, h] : [pubKey])) +} + +export function calculateTxFee({ + totalInputs, + totalOutputs, + satsPerByte, + type, + additional: { witnessScripts = [] } = {} +}: CalculateTxFeeOptions): number { + const txWeight = calculateTxVirtualSize({ totalInputs, totalOutputs, type, additional: { witnessScripts } }) + return txWeight * satsPerByte +} + +export function calculateTxVirtualSize({ + totalInputs, + totalOutputs, + type, + additional: { witnessScripts = [] } = {} +}: CalculateTxVirtualSizeOptions) { + const baseWeight = getInputOutputBaseSizeByType(type) + + const inputVBytes = baseWeight.input * totalInputs + const outputVBytes = baseWeight.output * totalOutputs + const baseVBytes = inputVBytes + outputVBytes + baseWeight.txHeader + const additionalVBytes = witnessScripts.reduce((acc, script) => (acc += script.byteLength), 0) || 0 + + const weight = 3 * baseVBytes + (baseVBytes + additionalVBytes) + const vSize = Math.ceil(weight / 4) + + return vSize +} + +export function getInputOutputBaseSizeByType(type: AddressFormats) { + switch (type) { + case "taproot": + return { input: 57.5, output: 43, txHeader: 10.5 } + + case "segwit": + return { input: 68, output: 31, txHeader: 10.5 } + + case "nested-segwit": + return { input: 68, output: 32, txHeader: 10.5 } + + case "legacy": + return { input: 147.5, output: 34, txHeader: 10.5 } + + default: + throw new Error("Invalid type") + } } diff --git a/packages/sdk/src/utils/types.ts b/packages/sdk/src/utils/types.ts new file mode 100644 index 00000000..71c97877 --- /dev/null +++ b/packages/sdk/src/utils/types.ts @@ -0,0 +1,13 @@ +import { AddressFormats } from "../addresses/formats" + +export interface CalculateTxFeeOptions { + totalInputs: number + totalOutputs: number + satsPerByte: number + type: AddressFormats + additional?: { + witnessScripts?: Buffer[] + } +} + +export type CalculateTxVirtualSizeOptions = Omit diff --git a/packages/sdk/src/wallet/Ordit.ts b/packages/sdk/src/wallet/Ordit.ts index 79814090..4c6964c3 100644 --- a/packages/sdk/src/wallet/Ordit.ts +++ b/packages/sdk/src/wallet/Ordit.ts @@ -11,7 +11,7 @@ import { AddressFormats, addressNameToType, generateBuyerPsbt, - generateDummyUtxos, + generateRefundableUTXOs, generateSellerPsbt, getAccountDataFromHdNode, getAddressesFromPublicKey, @@ -152,7 +152,7 @@ export class Ordit { }); } - signPsbt(value: string, { finalized = true, tweak = false }: SignPSBTOptions = {}) { + signPsbt(value: string, { finalize = true, extractTx = true, isRevealTx = false }: SignPSBTOptions = {}) { const networkObj = getNetwork(this.#network); let psbt: bitcoin.Psbt | null = null; @@ -188,7 +188,7 @@ export class Ordit { const address = bitcoin.address.fromOutputScript(script, networkObj); // TODO: improvise the below logic by accepting indexes to sign - if (!tweak || (tweak && this.selectedAddress === address)) { + if (isRevealTx || (!isRevealTx && this.selectedAddress === address)) { inputsToSign.push({ index, publicKey: this.publicKey, @@ -216,7 +216,8 @@ export class Ordit { network: networkObj }); - const signer = tweak ? tweakedSigner : this.#keyPair; + const signer = + input.witnessUtxo?.script && input.tapInternalKey && !input.tapLeafScript ? tweakedSigner : this.#keyPair psbt.signInput(inputsToSign[i].index, signer, inputsToSign[i].sighashTypes); } else { @@ -227,17 +228,16 @@ export class Ordit { } } - const psbtHex = psbt.toHex(); - - if (finalized) { - psbt.finalizeAllInputs(); - - const signedHex = psbt.extractTransaction().toHex(); + // TODO: check if psbt has been signed + if (finalize) { + psbt.finalizeAllInputs() + } - return signedHex; + if (extractTx) { + return psbt.extractTransaction().toHex() } - return psbtHex; + return psbt.toHex() } signMessage(message: string) { @@ -287,8 +287,8 @@ export class Ordit { static instantBuy = { generateBuyerPsbt, generateSellerPsbt, - generateDummyUtxos - }; + generateRefundableUTXOs + } static collection = { publish: publishCollection, @@ -324,6 +324,7 @@ export interface Input { } export interface SignPSBTOptions { - finalized?: boolean; - tweak?: boolean; + extractTx?: boolean + finalize?: boolean + isRevealTx?: boolean }