UNPKG

paid-services

Version:
184 lines (161 loc) 6.47 kB
const asyncAuto = require('async/auto'); const asyncEach = require('async/each'); const asyncTimesSeries = require('async/timesSeries'); const {createChainAddress} = require('ln-service'); const {fundPsbt} = require('ln-service'); const {decodePsbt} = require('psbt'); const {getUtxos} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const tinysecp = require('tiny-secp256k1'); const {unlockUtxo} = require('ln-service'); const allowedAddressFormats = ['p2tr', 'p2wpkh']; const asOutpoint = n => `${n.transaction_id}:${n.transaction_vout}`; const format = 'p2tr'; const {isArray} = Array; const isNumber = n => !isNaN(n); const sumOf = arr => arr.reduce((sum, n) => sum + n, 0); /** Get fanout funding details { capacity: <Fanout Output Capacity Tokens Number> inputs: [<Transaction Id:Transaction Vout Outpoint String>] lnd: <Authenticated LND API Object> outputs: <Output Count Number> rate: <Fee Rate Tokens Per VBytes Number> } @returns via cbk or Promise { [change]: <Change Output Script Hex String> funding: [<Funding Output Script Hex String>] [overflow]: <Funding Overflow Number> utxos: [{ bip32_derivations: [{ fingerprint: <Public Key Fingerprint Hex String> [leaf_hashes]: <Taproot Leaf Hash Hex String> path: <BIP 32 Child / Hardened Child / Index Derivation Path String> public_key: <Public Key Hex String> }] lock_id: <UTXO Lock Id Hex String> [non_witness_utxo]: <UTXO Spending Transaction Hex String> transaction_id: <Unspent Transaction Id Hex String> transaction_vout: <Unspent Transaction Output Index Number> witness_utxo: { script_pub: <UTXO Output Script Hex String> tokens: <UTXO Tokens Value Number> } }] } */ module.exports = ({capacity, inputs, lnd, outputs, rate}, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Import ECPair library ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp), // Check arguments validate: cbk => { if (!capacity) { return cbk([400, 'ExpectedCapacityToGetFanoutFundingDetails']); } if (!isArray(inputs) || !inputs.length) { return cbk([400, 'ExpectedArrayOfInputsToGetFanoutFundingDetails']); } if (!lnd) { return cbk([400, 'ExpectedAuthenticatedLndToGetFanoutFunding']); } if (!outputs || !isNumber(outputs)) { return cbk([400, 'ExpectedOutputCountToGetFanoutFundingDetails']); } if (!rate) { return cbk([400, 'ExpectedChainFeeRateToGetFanoutFundingDetails']); } return cbk(); }, // Get the full set of UTXOs to quickly check if there are enough funds getInputs: ['validate', ({}, cbk) => getUtxos({lnd}, cbk)], // Filter UTXOs for the inputs that are going to be used spend: ['getInputs', ({getInputs}, cbk) => { // Exit early when a provided input does not match a known UTXO if (!inputs.every(n => getInputs.utxos.map(asOutpoint).includes(n))) { return cbk([400, 'ExpectedKnownUtxosToProposeFanout']); } // Collect the set of inputs that are going to be spent const spending = getInputs.utxos.filter(utxo => { return inputs.includes(asOutpoint(utxo)); }); // Look to see if there is an invalid input const unsupportedUtxo = spending.find(utxo => { return !allowedAddressFormats.includes(utxo.address_format); }); // Exit with error when there is an invalid UTXO selected if (!!unsupportedUtxo) { return cbk([400, 'UnexpectedInputAddressFormatToGetFundingDetails']); } // Exit with error when the UTXOs don't cover the needed output value if (sumOf(spending.map(({tokens}) => tokens)) < capacity * outputs) { return cbk([400, 'ExpectedUtxosWithSufficientFundsToProposeFanout']); } return cbk(null, spending.map(utxo => ({ transaction_id: utxo.transaction_id, transaction_vout: utxo.transaction_vout, }))); }], // After confirming the inputs are ok, make addresses to use for funding getAddresses: ['spend', ({}, cbk) => { return asyncTimesSeries(outputs, (_, cbk) => { return createChainAddress({format, lnd}, cbk); }, cbk); }], // Fund the addresses to populate UTXOs that can be used fund: ['getAddresses', 'spend', ({getAddresses, spend}, cbk) => { return fundPsbt({ lnd, change_format: format, inputs: spend.map(input => ({ transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, })), fee_tokens_per_vbyte: rate, outputs: getAddresses.map(created => ({ address: created.address, tokens: capacity, })), }, cbk); }], // Unlock UTXOs to undo the PSBT funding locks unlock: ['fund', ({fund}, cbk) => { return asyncEach(fund.inputs, (input, cbk) => { return unlockUtxo({ lnd, id: input.lock_id, transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, }, cbk); }, cbk); }], // Put together final funding elements to use for PSBT construction funding: ['ecp', 'fund', ({ecp, fund}, cbk) => { const change = fund.outputs.find(n => n.is_change) || {}; const funding = fund.outputs.filter(n => !n.is_change); const {inputs} = decodePsbt({ecp, psbt: fund.psbt}); // UTXOs have been selected return cbk(null, { change: change.output_script || undefined, funding: funding.map(n => n.output_script), overflow: change.tokens || undefined, utxos: fund.inputs.map((input, vin) => ({ bip32_derivations: inputs[vin].bip32_derivations, lock_id: input.lock_id, non_witness_utxo: inputs[vin].non_witness_utxo, transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, witness_utxo: inputs[vin].witness_utxo, })), }); }], }, returnResult({reject, resolve, of: 'funding'}, cbk)); }); };