UNPKG

chaingate

Version:

A complete TypeScript library for connecting to and making transactions on different blockchains

183 lines 7.92 kB
import { UtxoFee } from './UtxoFee'; import { bytesToHex, hexToBytes } from '../../../../InternalUtils/Utils'; import * as btc from '@scure/btc-signer'; import { Transaction } from '../../Transaction'; import { UtxoBroadcastedTransaction } from './UtxoBroadcastedTransaction'; import { toBase, toSatoshi } from './utils'; import { toDecimal } from '../../../../InternalUtils/NumberLike'; export class UtxoTransaction extends Transaction { utils; state; privateKeyProvider; networkParams; addressToNative; constructor(utils, fromAddress, toAddress, amount, networkParams, privateKeyProvider, addressToNative) { super(fromAddress, toAddress, amount); this.utils = utils; this.state = { utxos: [], page: 0, crawled: false }; this.networkParams = networkParams; this.privateKeyProvider = privateKeyProvider; this.addressToNative = addressToNative; } async broadcast(fee) { if (typeof fee === 'string') fee = (this._suggestedFees ?? (await this.buildSuggestedFees()))[fee]; // Param passed is fee level await this.findUtxos(fee.feePerKb); const transaction = this.createTransaction(fee.feePerKb); const txSigned = await this.sign(transaction.vins, transaction.vouts); // Broadcast transaction const broadcastTx = await this.utils.broadcastTransaction(txSigned); // Add vins to spent cache const spentTxoCache = this.utils.getSpentTxoCache(); for (const vin of transaction.vins) { spentTxoCache.push({ txid: vin.txid, n: vin.n, amount: vin.amount, address: this.fromAddress, script: vin.script, }); } // Add vouts to utxo cache const utxoCache = this.utils.getUtxoCache(); for (const [n, vout] of transaction.vouts.entries()) { const script = this.utils.addressToScript(vout.address); utxoCache.push({ txid: broadcastTx.transactionId, n: n, amount: vout.amount, address: vout.address, script: script, }); } return new UtxoBroadcastedTransaction(this.utils, broadcastTx.transactionId); } async fee(fee, unit) { const currencySymbol = this.utils.currencyInfo.symbol; if (typeof fee == 'string') fee = this.utils.amount(toDecimal(fee), currencySymbol); const feeBase = fee.baseAmount; let feePerKb; if (unit == 'satoshi/kB') feePerKb = this.utils.amount(feeBase, 'btc'); else if (unit == 'satoshi/byte') feePerKb = this.utils.amount(toDecimal(feeBase).div(1e8).mul(1000), currencySymbol); else if (unit == `${this.utils.currencyInfo.symbol}/kB`) feePerKb = this.utils.amount(toDecimal(feeBase), currencySymbol); else if (unit == `${this.utils.currencyInfo.symbol}/byte`) feePerKb = this.utils.amount(toDecimal(feeBase).mul(1000), currencySymbol); else throw new Error('Unsupported unit'); return this.feePerKb(feePerKb, null); } async buildSuggestedFees() { const feeRates = await this.utils.getFeeRate(); return { low: await this.feePerKb(feeRates.low.feePerKb, feeRates.low.confirmationTimeSecs), normal: await this.feePerKb(feeRates.normal.feePerKb, feeRates.normal.confirmationTimeSecs), high: await this.feePerKb(feeRates.high.feePerKb, feeRates.high.confirmationTimeSecs), maximum: await this.feePerKb(feeRates.maximum.feePerKb, feeRates.maximum.confirmationTimeSecs), }; } async feePerKb(feePerKb, confirmationTimeSecs) { const currencySymbol = this.utils.currencyInfo.symbol; await this.findUtxos(feePerKb); const selected = this.createTransaction(feePerKb); return new UtxoFee(feePerKb, true, !!selected, confirmationTimeSecs, selected ? this.utils.amount(selected?.feeBase, currencySymbol) : null); } async findUtxos(feePerKb) { // 1) Check if the provided UTXOs already suffice if (this.createTransaction(feePerKb)) { return; } // 2) Otherwise, keep fetching until we either have enough or get no more UTXOs while (!this.state.crawled) { const utxosByAddress = await this.utils.addressUtxos(this.fromAddress, this.state.page); // If no new UTXOs are returned, break out of loop if (utxosByAddress.utxos.length === 0) { this.state.crawled = true; break; } // Push all new UTXOs into the state's UTXOs array for (const utxo of utxosByAddress.utxos) { // If the output is already spent by this wallet, take it out if (this.utils.getSpentTxoCache().find((t) => t.txid === utxo.txid && t.n == utxo.n)) continue; // Otherwise, add to utxos this.state.utxos.push({ txid: utxo.txid, amount: utxo.amount.baseAmount, script: utxo.script, n: utxo.n, }); } // Check again if we now have enough if (this.createTransaction(feePerKb)) return; // Increment the page so next time we fetch the next "page" this.state.page++; } // Add cached UTXOs (used only as a last resort to avoid spending unconfirmed outputs) for (const utxo of this.utils .getUtxoCache() .filter((t) => t.address === this.fromAddress)) { if (this.state.utxos.some((t) => t.txid === utxo.txid && t.n === utxo.n)) continue; this.state.utxos.push({ txid: utxo.txid, amount: utxo.amount, script: utxo.script, n: utxo.n, }); } } createTransaction(feePerKb) { // IMPROVE: Do not allow dust outputs const vins = this.state.utxos.map((utxo) => ({ txid: hexToBytes(utxo.txid), index: utxo.n, witnessUtxo: { script: utxo.script, amount: BigInt(toSatoshi(toDecimal(utxo.amount.toString())).toString()), }, })); const vouts = [ { address: this.addressToNative ? this.addressToNative(this.toAddress) : this.toAddress, amount: BigInt(this.amount.minimalUnitAmount.toString()), }, ]; const selected = btc.selectUTXO(vins, vouts, 'default', { changeAddress: this.addressToNative ? this.addressToNative(this.fromAddress) : this.fromAddress, feePerByte: BigInt(feePerKb.minimalUnitAmount.div(1000).round().toString()), bip69: true, createTx: true, allowLegacyWitnessUtxo: true, network: this.networkParams, }); if (!selected) return null; return { vins: selected.inputs.map((input) => ({ txid: bytesToHex(input.txid, false), amount: toBase(input.witnessUtxo.amount), n: input.index, script: input.witnessUtxo.script, })), vouts: selected.outputs.map((output) => { if (!('address' in output)) throw new Error(); return { amount: toBase(output.amount), address: output.address, }; }), feeBase: toBase(selected.fee), }; } } //# sourceMappingURL=UtxoTransaction.js.map