UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

222 lines 8.4 kB
import { fromBase64 } from '@btc-vision/bitcoin'; import { TransactionFactory } from '../transaction/TransactionFactory.js'; import { Wallet } from '../keypair/Wallet.js'; /** * Allows to fetch UTXO data from any OPNET node */ export class OPNetLimitedProvider { opnetAPIUrl; utxoPath = 'address/utxos'; rpc = 'json-rpc'; constructor(opnetAPIUrl) { this.opnetAPIUrl = opnetAPIUrl; } /** * Fetches UTXO data from the OPNET node * @param {FetchUTXOParams} settings - The settings to fetch UTXO data * @returns {Promise<UTXO[]>} - The UTXOs fetched * @throws {Error} - If UTXOs could not be fetched */ async fetchUTXO(settings) { if (settings.usePendingUTXO === undefined) { settings.usePendingUTXO = true; } if (settings.optimized === undefined) { settings.optimized = true; } const params = { method: 'GET', headers: { 'Content-Type': 'application/json', }, }; const url = `${this.opnetAPIUrl}/api/v1/${this.utxoPath}?address=${settings.address}&optimize=${settings.optimized ?? false}`; const resp = await fetch(url, params); if (!resp.ok) { throw new Error(`Failed to fetch UTXO data: ${resp.statusText}`); } const fetchedData = (await resp.json()); const rawTransactions = fetchedData.raw ?? []; const allUtxos = settings.usePendingUTXO ? [...fetchedData.confirmed, ...fetchedData.pending] : fetchedData.confirmed; const unspentUTXOs = []; for (const utxo of allUtxos) { if (fetchedData.spentTransactions.some((spent) => spent.transactionId === utxo.transactionId && spent.outputIndex === utxo.outputIndex)) { continue; } unspentUTXOs.push(utxo); } if (unspentUTXOs.length === 0) { throw new Error('No UTXO found'); } const meetCriteria = unspentUTXOs.filter((utxo) => { return BigInt(utxo.value) >= settings.minAmount; }); if (meetCriteria.length === 0) { throw new Error('No UTXO found (minAmount)'); } const finalUTXOs = []; let currentAmount = 0n; const amountRequested = settings.requestedAmount; const rawCache = new Map(); for (const utxo of meetCriteria) { const utxoValue = BigInt(utxo.value); if (utxoValue <= 0n) { continue; } const rawIndex = Number(utxo.raw); if (!Number.isInteger(rawIndex) || rawIndex < 0 || rawIndex >= rawTransactions.length) { throw new Error(`Invalid raw index for UTXO ${utxo.transactionId}:${utxo.outputIndex}`); } const rawHex = rawTransactions[rawIndex]; if (!rawHex) { throw new Error(`Invalid raw index ${rawIndex} - not found in raw transactions array`); } let nonWitnessUtxo = rawCache.get(rawIndex); if (nonWitnessUtxo === undefined) { nonWitnessUtxo = fromBase64(rawHex); rawCache.set(rawIndex, nonWitnessUtxo); } currentAmount += utxoValue; finalUTXOs.push({ transactionId: utxo.transactionId, outputIndex: utxo.outputIndex, value: utxoValue, scriptPubKey: utxo.scriptPubKey, nonWitnessUtxo, }); if (currentAmount > amountRequested) { break; } } rawCache.clear(); return finalUTXOs; } /** * Fetches UTXO data from the OPNET node for multiple addresses * @param {FetchUTXOParamsMultiAddress} settings - The settings to fetch UTXO data * @returns {Promise<UTXO[]>} - The UTXOs fetched * @throws {Error} - If UTXOs could not be fetched */ async fetchUTXOMultiAddr(settings) { const promises = []; for (const address of settings.addresses) { const params = { address: address, minAmount: settings.minAmount, requestedAmount: settings.requestedAmount, optimized: settings.optimized, usePendingUTXO: settings.usePendingUTXO, }; const promise = this.fetchUTXO(params).catch(() => { return []; }); promises.push(promise); } const utxos = await Promise.all(promises); const all = utxos.flat(); const finalUTXOs = []; let currentAmount = 0n; for (let i = 0; i < all.length; i++) { const utxo = all[i]; if (currentAmount >= settings.requestedAmount) { break; } currentAmount += utxo.value; finalUTXOs.push(utxo); } return finalUTXOs; } /** * Broadcasts a transaction to the OPNET node * @param {string} transaction - The transaction to broadcast * @param {boolean} psbt - Whether the transaction is a PSBT * @returns {Promise<BroadcastResponse>} - The response from the OPNET node */ async broadcastTransaction(transaction, psbt) { const params = [transaction, psbt]; const result = await this.rpcMethod('btc_sendRawTransaction', params); if (!result) { return; } return result; } /** * Splits UTXOs into smaller UTXOs * @param {Wallet} wallet - The wallet to split UTXOs * @param {Network} network - The network to split UTXOs * @param {number} splitInputsInto - The number of UTXOs to split into * @param {bigint} amountPerUTXO - The amount per UTXO * @returns {Promise<BroadcastResponse | { error: string }>} - The response from the OPNET node or an error */ async splitUTXOs(wallet, network, splitInputsInto, amountPerUTXO) { const utxoSetting = { addresses: [wallet.p2wpkh, wallet.p2tr], minAmount: 330n, requestedAmount: 1000000000000000n, }; const utxos = await this.fetchUTXOMultiAddr(utxoSetting); if (!utxos || !utxos.length) return { error: 'No UTXOs found' }; const amount = BigInt(splitInputsInto) * amountPerUTXO; const fundingTransactionParameters = { amount: amount, feeRate: 500, from: wallet.p2tr, utxos: utxos, signer: wallet.keypair, network, to: wallet.p2tr, splitInputsInto, priorityFee: 0n, gasSatFee: 330n, mldsaSigner: null, }; const transactionFactory = new TransactionFactory(); const fundingTx = await transactionFactory.createBTCTransfer(fundingTransactionParameters); const broadcastResponse = await this.broadcastTransaction(fundingTx.tx, false); if (!broadcastResponse) return { error: 'Could not broadcast transaction' }; return broadcastResponse; } /** * Fetches to the OPNET node * @param {string} method * @param {unknown[]} paramsMethod * @returns {Promise<unknown>} */ async rpcMethod(method, paramsMethod) { const params = { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ jsonrpc: '2.0', method: method, params: paramsMethod, id: 1, }), }; const url = `${this.opnetAPIUrl}/api/v1/${this.rpc}`; const resp = await fetch(url, params); if (!resp.ok) { throw new Error(`Failed to fetch to rpc: ${resp.statusText}`); } const fetchedData = (await resp.json()); if (!fetchedData) { throw new Error('No data fetched'); } const result = fetchedData.result; if (!result) { throw new Error('No rpc parameters found'); } if ('error' in result) { throw new Error(`Error in fetching to rpc ${result.error}`); } return result; } } //# sourceMappingURL=OPNetLimitedProvider.js.map