UNPKG

@xlink-network/xlink-sdk

Version:
1 lines 7.86 kB
{"version":3,"sources":["../src/bitcoinUtils/prepareTransaction.ts"],"sourcesContent":["import {\n estimateTransactionVSizeAfterSign,\n EstimationOutput,\n getOutputDustThreshold,\n UnsupportedInputTypeError,\n} from \"@c4/btc-utils\"\nimport * as btc from \"@scure/btc-signer\"\nimport { max, sum } from \"../utils/bigintHelpers\"\nimport { sumUTXO, UTXOSpendable } from \"./bitcoinHelpers\"\nimport { Recipient as _Recipient } from \"./createTransaction\"\nimport {\n InsufficientBitcoinBalanceError,\n UnsupportedBitcoinInput,\n} from \"./errors\"\n\nexport type BitcoinRecipient = _Recipient\n\nexport type ReselectSpendableUTXOsFn = (\n satsToSend: bigint,\n pinnedUTXOs: UTXOSpendable[],\n lastTimeSelectedUTXOs: UTXOSpendable[],\n) => Promise<UTXOSpendable[]>\n\nexport interface BitcoinTransactionPrepareResult {\n inputs: Array<UTXOSpendable>\n recipients: Array<BitcoinRecipient>\n changeAmount: bigint\n fee: bigint\n estimatedVSize: number\n}\n\nexport async function prepareTransaction(txInfo: {\n recipients: Array<BitcoinRecipient>\n changeAddressScriptPubKey: Uint8Array\n opReturnData?: Uint8Array[]\n selectedUTXOs?: Array<UTXOSpendable>\n feeRate: bigint\n reselectSpendableUTXOs: ReselectSpendableUTXOsFn\n}): Promise<BitcoinTransactionPrepareResult> {\n const {\n recipients,\n changeAddressScriptPubKey,\n opReturnData = [],\n selectedUTXOs = [],\n feeRate,\n reselectSpendableUTXOs,\n } = txInfo\n\n const newRecipients = await Promise.all(\n recipients.map(async (r): Promise<_Recipient> => {\n const dustThreshold = await getOutputDustThresholdForOutput(\n r.addressScriptPubKey,\n )\n\n return {\n ...r,\n satsAmount: max([r.satsAmount, dustThreshold]),\n }\n }),\n )\n const newRecipientAddresses = newRecipients\n .map(r => r.addressScriptPubKey)\n .concat(changeAddressScriptPubKey)\n\n const satsToSend = sum(newRecipients.map(r => r.satsAmount))\n\n let lastSelectedUTXOs = selectedUTXOs.slice()\n let lastSelectedUTXOSatsInTotal = sumUTXO(lastSelectedUTXOs)\n\n // Calculate fee\n let calculatedFee = await calculateFee({\n recipientAddressScriptPubKeys: newRecipientAddresses,\n opReturnData,\n selectedUTXOs: lastSelectedUTXOs,\n feeRate,\n })\n\n let loopTimes = 0\n while (lastSelectedUTXOSatsInTotal < satsToSend + calculatedFee.fee) {\n const newSatsToSend = satsToSend + calculatedFee.fee\n\n const newSelectedUTXOs = await reselectSpendableUTXOs(\n newSatsToSend,\n selectedUTXOs,\n lastSelectedUTXOs,\n )\n\n const newSelectedUTXOSatsInTotal = sumUTXO(newSelectedUTXOs)\n\n // Check if selected UTXO satoshi amount has changed since last iteration\n // If it hasn't, there is insufficient balance\n if (newSelectedUTXOSatsInTotal < lastSelectedUTXOSatsInTotal) {\n throw new InsufficientBitcoinBalanceError()\n }\n\n lastSelectedUTXOSatsInTotal = newSelectedUTXOSatsInTotal\n lastSelectedUTXOs = newSelectedUTXOs\n\n // Re-calculate fee\n calculatedFee = await calculateFee({\n recipientAddressScriptPubKeys: newRecipientAddresses,\n opReturnData,\n selectedUTXOs: newSelectedUTXOs,\n feeRate,\n })\n\n loopTimes++\n if (loopTimes > 500) {\n // Exit after max 500 iterations\n throw new InsufficientBitcoinBalanceError()\n }\n }\n\n const changeOutputDustThreshold = await getOutputDustThresholdForOutput(\n changeAddressScriptPubKey,\n )\n const changeAmount =\n lastSelectedUTXOSatsInTotal - sum([satsToSend, calculatedFee.fee])\n\n let finalChangeAmount: bigint\n let finalFeeAmount: bigint\n if (changeAmount < changeOutputDustThreshold) {\n finalChangeAmount = 0n\n finalFeeAmount = lastSelectedUTXOSatsInTotal - satsToSend\n } else {\n finalChangeAmount = changeAmount\n finalFeeAmount = calculatedFee.fee\n }\n\n return {\n inputs: lastSelectedUTXOs,\n recipients: newRecipients,\n changeAmount: finalChangeAmount,\n fee: finalFeeAmount,\n estimatedVSize: calculatedFee.estimatedVSize,\n }\n}\n\nasync function getOutputDustThresholdForOutput(\n outputAddressScriptPubKey: Uint8Array,\n): Promise<bigint> {\n return BigInt(\n Math.ceil(\n getOutputDustThreshold({\n scriptPubKey: outputAddressScriptPubKey,\n }),\n ),\n )\n}\n\n/**\n * https://github.com/bitcoin-dot-org/developer.bitcoin.org/blob/813ba3fb5eae85cfdfffe91d12f2df653ea8b725/devguide/transactions.rst?plain=1#L314\n * https://github.com/bitcoin/bitcoin/blob/2ffaa927023f5dc2a7b8d6cfeb4f4810e573b18c/src/policy/policy.h#L57\n */\nconst DEFAULT_MIN_RELAY_TX_FEE = 1000n\n\nexport async function calculateFee(info: {\n recipientAddressScriptPubKeys: Uint8Array[]\n opReturnData: Uint8Array[]\n selectedUTXOs: Array<UTXOSpendable>\n feeRate: bigint\n extraSize?: number\n}): Promise<{\n fee: bigint\n estimatedVSize: number\n}> {\n const outputs: EstimationOutput[] = [\n ...info.recipientAddressScriptPubKeys.map(r => ({\n scriptPubKey: r,\n })),\n ...info.opReturnData.map(data => ({\n scriptPubKey: btc.Script.encode([\"RETURN\", data]),\n })),\n ]\n\n try {\n const vSize = estimateTransactionVSizeAfterSign({\n inputs: info.selectedUTXOs,\n outputs,\n })\n\n const txSize = BigInt(Math.ceil(vSize))\n\n return {\n estimatedVSize: vSize,\n fee: max([info.feeRate * txSize, DEFAULT_MIN_RELAY_TX_FEE]),\n }\n } catch (e) {\n if (e instanceof UnsupportedInputTypeError) {\n const input = e.cause as UTXOSpendable\n throw new UnsupportedBitcoinInput(input.txId, input.index)\n }\n throw e\n }\n}\n"],"mappings":"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","names":[]}