UNPKG

chaingate

Version:

Multi-chain cryptocurrency SDK for TypeScript — unified API for Bitcoin, Ethereum, Litecoin, Dogecoin, Bitcoin Cash, Polygon, Arbitrum, and any EVM-compatible chain. Create wallets, query balances, send transactions, and manage tokens and NFTs across UTXO

378 lines (377 loc) 15.4 kB
"use strict"; /** * Abstract base class for UTXO-family transactions (BTC/LTC/DOGE/BCH). * * Contains all shared logic: fee estimation, UTXO gathering/selection, * sign-and-broadcast flow, and local UTXO cache management. * * Subclasses only need to implement {@link signTransaction} (network-specific * signing) and a static `create()` factory. * * @internal */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseUtxoTransaction = void 0; exports.buildRecommendedFees = buildRecommendedFees; exports.estimateMissingFunds = estimateMissingFunds; const decimal_js_1 = __importDefault(require("decimal.js")); const btc = __importStar(require("@scure/btc-signer")); const btc_signer_1 = require("@scure/btc-signer"); const errors_1 = require("../../errors"); const BroadcastedUtxoTransaction_1 = require("./BroadcastedUtxoTransaction"); const utils_1 = require("../../utils"); // --------------------------------------------------------------------------- // Abstract base class // --------------------------------------------------------------------------- class BaseUtxoTransaction { /** @internal */ constructor(params) { this.sent = false; this.explorer = params.explorer; this.fromAddress = params.fromAddress; this.toAddress = params.toAddress; this.valueSat = params.valueSat; this.networkParams = params.networkParams; this.feeRates = params.feeRates; this.getPrivateKey = params.getPrivateKey; this.state = params.state; this.currentFeePerKbSat = params.feeRates.normal.feePerKbSat; } // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- /** * Returns all recommended fee tiers (low, normal, high, maximum). */ recommendedFees() { return this.feeRates; } /** * Returns whether the wallet has enough funds to cover the transfer plus fee * at the current fee rate. */ enoughFunds() { return !!this.selectUtxos(this.currentFeePerKbSat); } /** * Returns the estimated virtual size (vbytes) of this transaction at the * current fee rate, or `null` if there are not enough funds. */ estimatedSizeBytes() { const selected = this.selectUtxos(this.currentFeePerKbSat); if (!selected) return null; return Math.ceil(selected.weight / 4); } /** * Sets the fee for this transaction. * * Pass a tier object from {@link recommendedFees} or an object with a * custom `feePerKbSat` value in satoshis per kilobyte. * * @throws {@link TransactionAlreadySentError} if the transaction has already been sent. */ setFee(fee) { if (this.sent) { throw new errors_1.TransactionAlreadySentError(); } this.currentFeePerKbSat = fee.feePerKbSat; } /** * Signs the transaction with the wallet's private key and broadcasts it to the network. * * @throws {@link TransactionAlreadySentError} if the transaction has already been sent. * @throws {@link NotEnoughFundsError} if the wallet does not have enough funds. */ async signAndBroadcast() { if (this.sent) { throw new errors_1.TransactionAlreadySentError(); } if (!this.enoughFunds()) { throw new errors_1.NotEnoughFundsError(); } // Gather UTXOs and select inputs/outputs. await this.findUtxos(this.currentFeePerKbSat); const selected = this.selectUtxos(this.currentFeePerKbSat); if (!selected) { throw new errors_1.NotEnoughFundsError(); } // Build and sign the raw transaction. const privateKey = await this.getPrivateKey(); const rawTx = this.signTransaction(selected.inputs, selected.outputs, privateKey); privateKey.fill(0); // Broadcast. const { txId } = await this.explorer.broadcastTransaction((0, utils_1.bytesToHex)(rawTx)); this.sent = true; // Update local UTXO cache: mark spent inputs, add change outputs. const cache = this.explorer.global.utxoCache; for (const input of selected.inputs) { cache.markSpent(input.txid, input.n, txId); } const fromScript = btc_signer_1.OutScript.encode(btc.Address(this.networkParams).decode(this.fromAddress)); for (let i = 0; i < selected.outputs.length; i++) { if (selected.outputs[i].address === this.fromAddress) { cache.addUnspent(this.fromAddress, { txid: txId, n: i, amount: this.explorer.amountFromSat(selected.outputs[i].amount), script: fromScript, }); } } return new BroadcastedUtxoTransaction_1.BroadcastedUtxoTransaction(txId, this.explorer); } // ------------------------------------------------------------------------- // UTXO gathering // ------------------------------------------------------------------------- /** Fetches UTXOs page by page until we have enough to cover the tx, or all are fetched. */ async findUtxos(feePerKbSat) { if (this.selectUtxos(feePerKbSat)) return; while (!this.state.crawled) { const result = await this.explorer.getUtxosByAddress(this.fromAddress, this.state.page.toString()); if (result.utxos.length === 0) { this.state.crawled = true; break; } const cache = this.explorer.global.utxoCache; for (const utxo of result.utxos) { // Skip duplicates. if (this.state.utxos.some((u) => u.txid === utxo.txid && u.n === utxo.n)) continue; // Skip UTXOs that have been spent by a local broadcast. if (cache.isSpent(utxo.txid, utxo.n)) continue; this.state.utxos.push({ txid: utxo.txid, amount: utxo.amount, n: utxo.n, script: (0, utils_1.hexToBytes)(utxo.script), }); } if (this.selectUtxos(feePerKbSat)) return; this.state.page++; } } // ------------------------------------------------------------------------- // UTXO selection via @scure/btc-signer // ------------------------------------------------------------------------- /** Attempts to select UTXOs and compute outputs. Returns null if insufficient. */ selectUtxos(feePerKbSat) { if (this.state.utxos.length === 0) return null; const vins = this.state.utxos.map((utxo) => ({ txid: (0, utils_1.hexToBytes)(utxo.txid), index: utxo.n, witnessUtxo: { script: utxo.script, amount: utxo.amount.min(), }, })); const vouts = [{ address: this.toAddress, amount: this.valueSat }]; const feePerByte = feePerKbSat / 1000n || 1n; const selected = btc.selectUTXO(vins, vouts, 'default', { changeAddress: this.fromAddress, feePerByte, bip69: true, createTx: true, allowLegacyWitnessUtxo: true, network: this.networkParams, }); if (!selected) return null; const inputs = selected.inputs.map((input) => ({ txid: (0, utils_1.bytesToHex)(input.txid), amount: this.explorer.amountFromSat(input.witnessUtxo.amount), n: input.index ?? 0, script: input.witnessUtxo.script, })); const outputs = selected.outputs.map((output) => { if (!('address' in output)) { // change output — should not happen with createTx but handle gracefully return { address: this.fromAddress, amount: output.amount }; } return { address: output.address, amount: output.amount }; }); return { inputs, outputs, fee: selected.fee ?? 0n, weight: selected.weight }; } } exports.BaseUtxoTransaction = BaseUtxoTransaction; // --------------------------------------------------------------------------- // Fee tier computation (runs once during create) // --------------------------------------------------------------------------- /** * Builds recommended fee tiers by fetching UTXOs and running selection for each tier. * Shared by all UTXO-family transaction `create()` factories. * @internal */ async function buildRecommendedFees(apiResponse, explorer, fromAddress, toAddress, valueSat, networkParams, state) { const tiers = ['low', 'normal', 'high', 'maximum']; const results = {}; for (const tier of tiers) { const entry = apiResponse[tier]; const feePerKbSat = BigInt(new decimal_js_1.default(entry.feePerKb).toFixed(0)); // Try to gather UTXOs and select to compute the actual fee. const tempTx = new TempUtxoSelector(explorer, fromAddress, toAddress, valueSat, networkParams, state); await tempTx.findUtxos(feePerKbSat); const selected = tempTx.selectUtxos(feePerKbSat); const result = { feePerKbSat, estimatedFeeSat: selected ? selected.fee : null, estimatedConfirmationSecs: entry.confirmationTimeSecs, enoughFunds: !!selected, }; if (!selected) { result.missingFunds = estimateMissingFunds({ utxos: state.utxos, fromAddress, toAddress, valueSat, feePerKbSat, networkParams, }); } results[tier] = result; } return results; } /** * Estimates the satoshis missing for a transaction to succeed at a given fee * rate by re-running selection with a synthetic high-value input that uses the * same script type as the sender's address. Returns `null` when the gap cannot * be estimated. * @internal */ function estimateMissingFunds(params) { const { utxos, fromAddress, toAddress, valueSat, feePerKbSat, networkParams } = params; const totalAvailable = utxos.reduce((sum, u) => sum + u.amount.min(), 0n); const fromScript = btc_signer_1.OutScript.encode(btc.Address(networkParams).decode(fromAddress)); const dummyAmount = valueSat + 100000000000n; const vins = [ ...utxos.map((utxo) => ({ txid: (0, utils_1.hexToBytes)(utxo.txid), index: utxo.n, witnessUtxo: { script: utxo.script, amount: utxo.amount.min() }, })), { txid: new Uint8Array(32), index: 0, witnessUtxo: { script: fromScript, amount: dummyAmount }, }, ]; const vouts = [{ address: toAddress, amount: valueSat }]; const feePerByte = feePerKbSat / 1000n || 1n; const selected = btc.selectUTXO(vins, vouts, 'all', { changeAddress: fromAddress, feePerByte, bip69: true, createTx: false, allowLegacyWitnessUtxo: true, network: networkParams, }); if (!selected) return null; const totalNeeded = valueSat + (selected.fee ?? 0n); return totalNeeded > totalAvailable ? totalNeeded - totalAvailable : null; } /** * Lightweight helper used only during fee estimation. * Shares the UtxoApiState with the main transaction so UTXOs fetched * during fee estimation are reused when signing. */ class TempUtxoSelector { constructor(explorer, fromAddress, toAddress, valueSat, networkParams, state) { this.explorer = explorer; this.fromAddress = fromAddress; this.toAddress = toAddress; this.valueSat = valueSat; this.networkParams = networkParams; this.state = state; } async findUtxos(feePerKbSat) { if (this.selectUtxos(feePerKbSat)) return; while (!this.state.crawled) { const result = await this.explorer.getUtxosByAddress(this.fromAddress, this.state.page.toString()); if (result.utxos.length === 0) { this.state.crawled = true; break; } const cache = this.explorer.global.utxoCache; for (const utxo of result.utxos) { if (this.state.utxos.some((u) => u.txid === utxo.txid && u.n === utxo.n)) continue; if (cache.isSpent(utxo.txid, utxo.n)) continue; this.state.utxos.push({ txid: utxo.txid, amount: utxo.amount, n: utxo.n, script: (0, utils_1.hexToBytes)(utxo.script), }); } if (this.selectUtxos(feePerKbSat)) return; this.state.page++; } } selectUtxos(feePerKbSat) { if (this.state.utxos.length === 0) return null; const vins = this.state.utxos.map((utxo) => ({ txid: (0, utils_1.hexToBytes)(utxo.txid), index: utxo.n, witnessUtxo: { script: utxo.script, amount: utxo.amount.min() }, })); const vouts = [{ address: this.toAddress, amount: this.valueSat }]; const feePerByte = feePerKbSat / 1000n || 1n; const selected = btc.selectUTXO(vins, vouts, 'default', { changeAddress: this.fromAddress, feePerByte, bip69: true, createTx: true, allowLegacyWitnessUtxo: true, network: this.networkParams, }); if (!selected) return null; return { fee: selected.fee ?? 0n }; } }