{"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":[]}