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

361 lines (360 loc) 14.8 kB
"use strict"; 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.CustomUtxoTransaction = void 0; exports.signCustomUtxoTransaction = signCustomUtxoTransaction; 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"); // --------------------------------------------------------------------------- // Custom transaction class // --------------------------------------------------------------------------- /** * A UTXO transaction with caller-defined inputs and outputs. * * The fee is implicitly the difference between the sum of input amounts and * the sum of output amounts. */ class CustomUtxoTransaction { /** @internal */ constructor(params) { this.sent = false; this.explorer = params.explorer; this.networkParams = params.networkParams; this.inputList = [...params.inputs]; this.outputList = [...params.outputs]; this.getPrivateKey = params.getPrivateKey; this._signTransaction = params.signTransaction; } // ------------------------------------------------------------------------- // Accessors // ------------------------------------------------------------------------- /** Returns the inputs that will be spent. */ inputs() { return this.inputList; } /** Returns the outputs that will be created. */ outputs() { return this.outputList; } /** * Returns the implied fee (sum of inputs minus sum of outputs). * * A negative value means the outputs exceed the inputs — the transaction * would be invalid. */ fee() { const totalIn = this.inputList.reduce((sum, i) => sum + i.amount.min(), 0n); const totalOut = this.outputList.reduce((sum, o) => sum + o.amount.min(), 0n); return this.explorer.amountFromSat(totalIn - totalOut); } /** * Returns the estimated virtual size (vbytes) of this transaction. * * Returns `null` if the transaction has no inputs or no outputs. */ estimatedSizeBytes() { if (this.inputList.length === 0 || this.outputList.length === 0) return null; const result = this.runSelection(1n); if (!result) return null; return Math.ceil(result.weight / 4); } // ------------------------------------------------------------------------- // Mutation // ------------------------------------------------------------------------- /** * Appends an input to the transaction. * * @throws {@link TransactionAlreadySentError} if already broadcast. */ addInput(input) { this.guardNotSent(); this.inputList.push(input); } /** * Removes an input identified by its transaction ID and output index. * * @returns `true` if the input was found and removed. * @throws {@link TransactionAlreadySentError} if already broadcast. */ removeInput(txid, index) { this.guardNotSent(); const idx = this.inputList.findIndex((i) => i.txid === txid && i.index === index); if (idx === -1) return false; this.inputList.splice(idx, 1); return true; } /** * Appends an output to the transaction. * * @throws {@link TransactionAlreadySentError} if already broadcast. */ addOutput(output) { this.guardNotSent(); this.outputList.push(output); } /** * Removes an output by its position in the outputs list. * * @returns `true` if the index was valid and the output was removed. * @throws {@link TransactionAlreadySentError} if already broadcast. */ removeOutput(index) { this.guardNotSent(); if (index < 0 || index >= this.outputList.length) return false; this.outputList.splice(index, 1); return true; } // ------------------------------------------------------------------------- // Change address & fee estimation // ------------------------------------------------------------------------- /** * Sets a change address and computes the change output automatically. * * Appends a change output whose amount equals the remaining value after * subtracting all other outputs and the estimated fee. If the change * amount is below the dust threshold, no change output is added and * the remainder goes entirely to fees. * * @param address - The address to receive the change. * @param fee - A tier object from {@link recommendedFees} or an explicit fee rate. * @throws {@link TransactionAlreadySentError} if already broadcast. * @throws {Error} if inputs or outputs are empty, or if outputs exceed inputs. */ setChangeAddress(address, fee) { this.guardNotSent(); const feePerByte = fee.feePerKbSat / 1000n || 1n; if (this.inputList.length === 0) { throw new Error('Cannot set change address: no inputs.'); } const result = this.runSelection(feePerByte, address); if (!result) { throw new Error('Cannot compute change: outputs exceed inputs at the given fee rate.'); } // If selectUTXO added a change output, append it to our output list. if (result.change) { // The change output is the last one appended by selectUTXO. const changeOutput = result.outputs[result.outputs.length - 1]; if ('address' in changeOutput) { this.outputList.push({ address: changeOutput.address, amount: this.explorer.amountFromSat(changeOutput.amount), }); } } } /** * Returns recommended fees for each priority tier. * * Fee rates are fetched from the network. For each tier, the estimated fee * is computed from the current inputs and outputs. * * Returns `null` if the transaction has no inputs or no outputs. */ async recommendedFees() { if (this.inputList.length === 0 || this.outputList.length === 0) return null; const totalIn = this.inputList.reduce((sum, i) => sum + i.amount.min(), 0n); const feeRateResult = await this.explorer.getFeeRate(); const tiers = ['low', 'normal', 'high', 'maximum']; const results = {}; for (const tier of tiers) { const entry = feeRateResult[tier]; const feePerKbSat = BigInt(new decimal_js_1.default(entry.feePerKb).toFixed(0)); const feePerByte = feePerKbSat / 1000n || 1n; const selected = this.runSelection(feePerByte); const estimatedFeeSat = selected ? (selected.fee ?? null) : null; const enoughFunds = estimatedFeeSat !== null && totalIn >= (estimatedFeeSat ?? 0n); const result = { feePerKbSat, estimatedFeeSat, estimatedConfirmationSecs: entry.confirmationTimeSecs, enoughFunds, }; if (!enoughFunds) { result.missingFunds = this.estimateMissingFunds(feePerByte, totalIn); } results[tier] = result; } return results; } /** * Estimates the satoshis missing for the transaction to succeed at a given * fee rate by re-running selection with a synthetic high-value input. Returns * `null` when the gap cannot be estimated. */ estimateMissingFunds(feePerByte, totalIn) { const totalOut = this.outputList.reduce((sum, o) => sum + o.amount.min(), 0n); const syntheticScript = (0, utils_1.hexToBytes)(this.inputList[0].script); const dummyAmount = totalOut + 100000000000n; const vins = this.inputList.map((vin) => ({ txid: (0, utils_1.hexToBytes)(vin.txid), index: vin.index, witnessUtxo: { script: (0, utils_1.hexToBytes)(vin.script), amount: vin.amount.min() }, })); vins.push({ txid: new Uint8Array(32), index: 0, witnessUtxo: { script: syntheticScript, amount: dummyAmount }, }); const vouts = this.outputList.map((vout) => ({ address: vout.address, amount: vout.amount.min(), })); const selected = btc.selectUTXO(vins, vouts, 'all', { changeAddress: this.outputList[0].address, feePerByte, bip69: false, createTx: false, allowLegacyWitnessUtxo: true, network: this.networkParams, }); if (!selected) return null; const estimatedFee = selected.fee ?? 0n; const needed = totalOut + estimatedFee; return needed > totalIn ? needed - totalIn : null; } // ------------------------------------------------------------------------- // Sign & broadcast // ------------------------------------------------------------------------- /** * Signs the transaction and broadcasts it to the network. * * @throws {@link TransactionAlreadySentError} if already broadcast. * @throws {Error} if outputs exceed inputs. */ async signAndBroadcast() { if (this.sent) { throw new errors_1.TransactionAlreadySentError(); } const impliedFee = this.fee().min(); if (impliedFee < 0n) { throw new Error(`Transaction outputs exceed inputs. ` + `Reduce output amounts or add more inputs.`); } const privateKey = await this.getPrivateKey(); const rawTx = this._signTransaction(this.inputList, this.outputList, privateKey, this.networkParams); privateKey.fill(0); const { txId } = await this.explorer.broadcastTransaction((0, utils_1.bytesToHex)(rawTx)); this.sent = true; // Update local UTXO cache: mark inputs as spent. const cache = this.explorer.global.utxoCache; for (const input of this.inputList) { cache.markSpent(input.txid, input.index, txId); } return new BroadcastedUtxoTransaction_1.BroadcastedUtxoTransaction(txId, this.explorer); } // ------------------------------------------------------------------------- // Internal helpers // ------------------------------------------------------------------------- guardNotSent() { if (this.sent) throw new errors_1.TransactionAlreadySentError(); } /** * Runs selectUTXO with the 'all' strategy so every input is included. * Uses the current outputs as-is. Returns null if selection fails. */ runSelection(feePerByte, changeAddress) { if (this.inputList.length === 0 || this.outputList.length === 0) return null; const vins = this.inputList.map((vin) => ({ txid: (0, utils_1.hexToBytes)(vin.txid), index: vin.index, witnessUtxo: { script: (0, utils_1.hexToBytes)(vin.script), amount: vin.amount.min(), }, })); const vouts = this.outputList.map((vout) => ({ address: vout.address, amount: vout.amount.min(), })); // Use the first output address as a dummy change address when none is provided. // The change address only affects the output script size estimation, which is // negligible for size calculations. const resolvedChangeAddress = changeAddress ?? this.outputList[0].address; return (btc.selectUTXO(vins, vouts, 'all', { changeAddress: resolvedChangeAddress, feePerByte, bip69: false, createTx: false, allowLegacyWitnessUtxo: true, network: this.networkParams, }) ?? null); } } exports.CustomUtxoTransaction = CustomUtxoTransaction; // --------------------------------------------------------------------------- // Signing implementations (one per UTXO family) // --------------------------------------------------------------------------- /** * Standard UTXO signing (BTC, LTC, DOGE, tBTC). * @internal */ function signCustomUtxoTransaction(inputs, outputs, privateKey, networkParams) { const txVins = inputs.map((vin) => ({ txid: (0, utils_1.hexToBytes)(vin.txid), index: vin.index, witnessUtxo: { script: (0, utils_1.hexToBytes)(vin.script), amount: vin.amount.min(), }, })); const txVouts = outputs.map((vout) => ({ script: btc_signer_1.OutScript.encode(btc.Address(networkParams).decode(vout.address)), amount: vout.amount.min(), })); const transaction = new btc.Transaction({ allowLegacyWitnessUtxo: true }); for (const vin of txVins) transaction.addInput(vin); for (const vout of txVouts) transaction.addOutput(vout); for (let i = 0; i < transaction.inputsLength; i++) { transaction.signIdx(privateKey, i); } transaction.finalize(); return (0, utils_1.hexToBytes)(transaction.hex); }