UNPKG

lightning

Version:
287 lines (234 loc) 9.36 kB
const asyncAuto = require('async/auto'); const {returnResult} = require('asyncjs-util'); const {Psbt} = require('bitcoinjs-lib'); const {Transaction} = require('bitcoinjs-lib'); const {isLnd} = require('./../../lnd_requests'); const asOutpoint = n => `${n.transaction_id}:${n.transaction_vout}`; const defaultChangeType = () => 'CHANGE_ADDRESS_TYPE_P2TR'; const defaultConfirmationTarget = 6; const expirationAsDate = epoch => new Date(Number(epoch) * 1e3).toISOString(); const {fromBuffer} = Transaction; const hexFromBuffer = buffer => buffer.toString('hex'); const {isArray} = Array; const {isBuffer} = Buffer; const isKnownChangeFormat = format => !format || format === 'p2tr'; const method = 'fundPsbt'; const notSupported = /unknown.*walletrpc.WalletKit/; const psbtFromHex = hex => Psbt.fromBuffer(Buffer.from(hex, 'hex')); const strategy = type => !type ? undefined : `STRATEGY_${type.toUpperCase()}`; const type = 'wallet'; const txIdFromBuffer = buffer => buffer.slice().reverse().toString('hex'); const txIdFromHash = hash => hash.reverse().toString('hex'); /** Lock and optionally select inputs to a partially signed transaction Specify outputs or PSBT with the outputs encoded If there are no inputs passed, internal UTXOs will be selected and locked `utxo_selection` methods: 'largest', 'random' `change_format` options: `p2tr` (only one change type is supported) Requires `onchain:write` permission Requires LND built with `walletrpc` tag This method is not supported in LND 0.11.1 and below Specifying 0 for `min_confirmations` is not supported in LND 0.13.0 and below `utxo_selection` is not supported in LND 0.17.5 and below { [change_format]: <Change Address Address Format String> [fee_tokens_per_vbyte]: <Chain Fee Tokens Per Virtual Byte Number> [inputs]: [{ transaction_id: <Unspent Transaction Id Hex String> transaction_vout: <Unspent Transaction Output Index Number> }] lnd: <Authenticated LND API Object> [min_confirmations]: <Spend UTXOs With Minimum Confirmations Number> [outputs]: [{ address: <Chain Address String> tokens: <Send Tokens Tokens Number> }] [target_confirmations]: <Confirmations To Wait Number> [psbt]: <Existing PSBT Hex String> [utxo_selection]: <Select UTXOs Using Method String> } @returns via cbk or Promise { inputs: [{ [lock_expires_at]: <UTXO Lock Expires At ISO 8601 Date String> [lock_id]: <UTXO Lock Id Hex String> transaction_id: <Unspent Transaction Id Hex String> transaction_vout: <Unspent Transaction Output Index Number> }] outputs: [{ is_change: <Spends To a Generated Change Output Bool> output_script: <Output Script Hex String> tokens: <Send Tokens Tokens Number> }] psbt: <Unsigned PSBT Hex String> } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!isKnownChangeFormat(args.change_format)) { return cbk([400, 'ExpectedKnownChangeFormatToFundPsbt']); } if (!isLnd({method, type, lnd: args.lnd})) { return cbk([400, 'ExpectedAuthenticatedLndToFundPsbt']); } if (!args.outputs && !args.psbt) { return cbk([400, 'ExpectedEitherOutputsOrPsbtToFundPsbt']); } if (!!args.outputs && !isArray(args.outputs)) { return cbk([400, 'ExpectedArrayOfOutputsToFundPsbt']); } if (!!args.outputs && !!args.psbt) { return cbk([400, 'ExpectedOnlyOutputsOrPsbtToFundPsbt']); } return cbk(); }, // Fee setting for the funded PSBT fee: ['validate', ({}, cbk) => { if (!!args.fee_tokens_per_vbyte) { return cbk(null, {fee_tokens_per_vbyte: args.fee_tokens_per_vbyte}); } if (!!args.target_confirmations) { return cbk(null, {target_confirmations: args.target_confirmations}); } return cbk(null, {target_confirmations: defaultConfirmationTarget}); }], // Raw inputs to send to inputs: ['validate', ({}, cbk) => { if (!args.inputs) { return cbk(null, []); } const inputs = args.inputs.map(input => ({ output_index: input.transaction_vout, txid_bytes: Buffer.from(input.transaction_id, 'hex').reverse(), })); return cbk(null, inputs); }], // Minimum confirmations for UTXOs to select minConfs: ['validate', ({}, cbk) => { if (args.min_confirmations === Number()) { return cbk(null, args.min_confirmations); } return cbk(null, args.min_confirmations || undefined); }], // Raw outputs to send to outputs: ['validate', ({}, cbk) => { if (!args.outputs) { return cbk(); } const outputs = args.outputs.reduce((sum, n) => { sum[n.address] = n.tokens.toString(); return sum; }, {}); return cbk(null, outputs); }], // Raw funding details funding: ['inputs', 'outputs', ({inputs, outputs}, cbk) => { if (!outputs) { return cbk(); } return cbk(null, {inputs, outputs}); }], // Fund the PSBT fund: ['fee', 'funding', 'minConfs', ({fee, funding, minConfs}, cbk) => { return args.lnd[type][method]({ change_type: defaultChangeType(args.change_type), coin_selection_strategy: strategy(args.utxo_selection), min_confs: minConfs !== undefined ? minConfs : undefined, psbt: !!args.psbt ? Buffer.from(args.psbt, 'hex') : undefined, raw: funding || undefined, sat_per_vbyte: fee.fee_tokens_per_vbyte || undefined, spend_unconfirmed: minConfs === Number() || undefined, target_conf: fee.target_confirmations || undefined, }, (err, res) => { if (!!err && notSupported.test(err.details)) { return cbk([501, 'FundPsbtMethodNotSupported']); } if (!!err) { return cbk([503, 'UnexpectedErrorFundingTransaction', {err}]); } if (!res) { return cbk([503, 'ExpectedResultOfTransactionFunding']); } if (res.change_output_index === undefined) { return cbk([503, 'ExpectedFundingChangeOutputIndexNumber']); } if (!isBuffer(res.funded_psbt)) { return cbk([503, 'ExpectedFundedTransactionPsbt']); } if (!isArray(res.locked_utxos)) { return cbk([503, 'ExpectedArrayOfUtxoLocksForFundedTransaction']); } if (!!res.locked_utxos.filter(n => !n).length) { return cbk([503, 'ExpectedNonEmptyLockedUtxosForFundedPsbt']); } if (!!res.locked_utxos.find(n => !n.outpoint)) { return cbk([503, 'ExpectedOutpointInLockedUtxosForFundedPsbt']); } return cbk(null, { change_output_index: res.change_output_index, locked_utxos: res.locked_utxos, psbt: hexFromBuffer(res.funded_psbt), }); }); }], // Derive the raw transaction from the funded PSBT tx: ['fund', ({fund}, cbk) => { const {psbt} = fund; try { const tx = psbtFromHex(psbt).data.globalMap.unsignedTx.toBuffer(); return cbk(null, fromBuffer(tx)); } catch (err) { return cbk([503, 'FailedToDecodePsbtInFundPsbtResponse', {err}]); } }], // Derive the final funding inputs for the transaction fundingInputs: ['fund', 'tx', ({fund, tx}, cbk) => { // Locks are reservations on inputs to prevent double-spending const locks = fund.locked_utxos.map(utxo => ({ expires_at: expirationAsDate(utxo.expiration), id: hexFromBuffer(utxo.id), transaction_id: txIdFromBuffer(utxo.outpoint.txid_bytes), transaction_vout: utxo.outpoint.output_index, })); // The funding inputs are encoded in the PSBT's unsigned tx const funding = tx.ins.map(({hash, index}) => ({ transaction_id: txIdFromHash(hash), transaction_vout: index, })); // Include relevant UTXO locks with inputs const inputs = funding.map(input => { const lock = locks.find(n => asOutpoint(n) === asOutpoint(input)); return { lock_expires_at: !!lock ? lock.expires_at : undefined, lock_id: !!lock ? lock.id : undefined, transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, }; }); return cbk(null, inputs); }], // Final funded PSBT funded: [ 'fund', 'fundingInputs', 'tx', ({fund, fundingInputs, tx}, cbk) => { return cbk(null, { inputs: fundingInputs, outputs: tx.outs.map(({script, value}, index) => ({ is_change: index === fund.change_output_index, output_script: hexFromBuffer(script), tokens: value, })), psbt: fund.psbt, }); }], }, returnResult({reject, resolve, of: 'funded'}, cbk)); }); };