UNPKG

@arkade-os/sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

202 lines (201 loc) 7.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OnchainWallet = void 0; exports.selectCoins = selectCoins; const payment_1 = require("@scure/btc-signer/payment"); const networks_1 = require("../networks"); const onchain_1 = require("../providers/onchain"); const btc_signer_1 = require("@scure/btc-signer"); const anchor_1 = require("../utils/anchor"); const txSizeEstimator_1 = require("../utils/txSizeEstimator"); /** * Onchain Bitcoin wallet implementation for traditional Bitcoin transactions. * * This wallet handles regular Bitcoin transactions on the blockchain without * using the Ark protocol. It supports P2TR (Pay-to-Taproot) addresses and * provides basic Bitcoin wallet functionality. * * @example * ```typescript * const wallet = new OnchainWallet(identity, 'mainnet'); * const balance = await wallet.getBalance(); * const txid = await wallet.send({ * address: 'bc1...', * amount: 50000 * }); * ``` */ class OnchainWallet { constructor(identity, network, provider) { this.identity = identity; const pubkey = identity.xOnlyPublicKey(); if (!pubkey) { throw new Error("Invalid configured public key"); } this.provider = provider || new onchain_1.EsploraProvider(onchain_1.ESPLORA_URL[network]); this.network = (0, networks_1.getNetwork)(network); this.onchainP2TR = (0, payment_1.p2tr)(pubkey, undefined, this.network); } get address() { return this.onchainP2TR.address || ""; } async getCoins() { return this.provider.getCoins(this.address); } async getBalance() { const coins = await this.getCoins(); const onchainConfirmed = coins .filter((coin) => coin.status.confirmed) .reduce((sum, coin) => sum + coin.value, 0); const onchainUnconfirmed = coins .filter((coin) => !coin.status.confirmed) .reduce((sum, coin) => sum + coin.value, 0); const onchainTotal = onchainConfirmed + onchainUnconfirmed; return onchainTotal; } async send(params) { if (params.amount <= 0) { throw new Error("Amount must be positive"); } if (params.amount < OnchainWallet.DUST_AMOUNT) { throw new Error("Amount is below dust limit"); } const coins = await this.getCoins(); let feeRate = params.feeRate; if (!feeRate) { feeRate = await this.provider.getFeeRate(); } if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) { feeRate = OnchainWallet.MIN_FEE_RATE; } // Ensure fee is an integer by rounding up const estimatedFee = Math.ceil(174 * feeRate); const totalNeeded = params.amount + estimatedFee; // Select coins const selected = selectCoins(coins, totalNeeded); // Create transaction let tx = new btc_signer_1.Transaction(); // Add inputs for (const input of selected.inputs) { tx.addInput({ txid: input.txid, index: input.vout, witnessUtxo: { script: this.onchainP2TR.script, amount: BigInt(input.value), }, tapInternalKey: this.onchainP2TR.tapInternalKey, }); } // Add payment output tx.addOutputAddress(params.address, BigInt(params.amount), this.network); // Add change output if needed if (selected.changeAmount > 0n) { tx.addOutputAddress(this.address, selected.changeAmount, this.network); } // Sign inputs and Finalize tx = await this.identity.sign(tx); tx.finalize(); // Broadcast const txid = await this.provider.broadcastTransaction(tx.hex); return txid; } async bumpP2A(parent) { const parentVsize = parent.vsize; let child = new btc_signer_1.Transaction({ allowUnknownInputs: true, allowLegacyWitnessUtxo: true, version: 3, }); child.addInput((0, anchor_1.findP2AOutput)(parent)); // throws if not found const childVsize = txSizeEstimator_1.TxWeightEstimator.create() .addKeySpendInput(true) .addP2AInput() .addP2TROutput() .vsize().value; const packageVSize = parentVsize + Number(childVsize); let feeRate = await this.provider.getFeeRate(); if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) { feeRate = OnchainWallet.MIN_FEE_RATE; } const fee = Math.ceil(feeRate * packageVSize); if (!fee) { throw new Error(`invalid fee, got ${fee} with vsize ${packageVSize}, feeRate ${feeRate}`); } // Select coins const coins = await this.getCoins(); const selected = selectCoins(coins, fee, true); for (const input of selected.inputs) { child.addInput({ txid: input.txid, index: input.vout, witnessUtxo: { script: this.onchainP2TR.script, amount: BigInt(input.value), }, tapInternalKey: this.onchainP2TR.tapInternalKey, }); } child.addOutputAddress(this.address, anchor_1.P2A.amount + selected.changeAmount, this.network); // Sign inputs and Finalize child = await this.identity.sign(child); for (let i = 1; i < child.inputsLength; i++) { child.finalizeIdx(i); } try { await this.provider.broadcastTransaction(parent.hex, child.hex); } catch (error) { console.error(error); } finally { return [parent.hex, child.hex]; } } } exports.OnchainWallet = OnchainWallet; OnchainWallet.MIN_FEE_RATE = 1; // sat/vbyte OnchainWallet.DUST_AMOUNT = 546; // sats /** * Select coins to reach a target amount, prioritizing those closer to expiry * @param coins List of coins to select from * @param targetAmount Target amount to reach in satoshis * @param forceChange If true, ensure the coin selection will require a change output * @returns Selected coins and change amount, or null if insufficient funds */ function selectCoins(coins, targetAmount, forceChange = false) { if (isNaN(targetAmount)) { throw new Error("Target amount is NaN, got " + targetAmount); } if (targetAmount < 0) { throw new Error("Target amount is negative, got " + targetAmount); } if (targetAmount === 0) { return { inputs: [], changeAmount: 0n }; } // Sort coins by amount (descending) const sortedCoins = [...coins].sort((a, b) => b.value - a.value); const selectedCoins = []; let selectedAmount = 0; // Select coins until we have enough for (const coin of sortedCoins) { selectedCoins.push(coin); selectedAmount += coin.value; if (forceChange ? selectedAmount > targetAmount : selectedAmount >= targetAmount) { break; } } if (selectedAmount === targetAmount) { return { inputs: selectedCoins, changeAmount: 0n }; } if (selectedAmount < targetAmount) { throw new Error("Insufficient funds"); } const changeAmount = BigInt(selectedAmount - targetAmount); return { inputs: selectedCoins, changeAmount, }; }