UNPKG

ln-sync

Version:
201 lines (176 loc) 6.82 kB
const asyncAuto = require('async/auto'); const asyncEach = require('async/each'); const asyncReflect = require('async/reflect'); const {fundPsbt} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {signPsbt} = require('ln-service'); const {Transaction} = require('bitcoinjs-lib'); const {unlockUtxo} = require('ln-service'); const adjustFactor = 10; const allowZeroConfirmationInputs = 0; const {ceil} = Math; const dust = 546; const {fromHex} = Transaction; const {isArray} = Array; const sumOf = arr => arr.reduce((sum, n) => sum + n, Number()); /** Find the max amount that can be used for funding outputs given inputs { addresses: [<Send to Address String>] fee_tokens_per_vbyte: <Funding Fee Tokens Per VByte Number> inputs: [{ tokens: <Input Tokens Number> transaction_id: <UTXO Transaction Id Hex String> transaction_vout: <UTXO Transaction Output Index Number> }] lnd: <Authenticated LND API Object> } @returns via cbk or Promise { fee_tokens_per_vbyte: <Effective Final Fee Tokens Per VByte Number> max_tokens: <Maximum Spendable Tokens Number> } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!isArray(args.addresses)) { return cbk([400, 'ExpectedArrayOfAddressesToCalculateMaxFunding']); } if (!args.fee_tokens_per_vbyte) { return cbk([400, 'ExpectedFeeTokensToCalculateMaxFundAmount']); } if (!isArray(args.inputs)) { return cbk([400, 'ExpectedArrayOfInputsToCalculateMaxFundAmount']); } if (!args.lnd) { return cbk([400, 'ExpectedAuthenticatedLndApiToCalcMaxFundAmount']); } return cbk(); }, // Create the template PSBT to figure out what the change is createTemplate: ['validate', ({}, cbk) => { return fundPsbt({ fee_tokens_per_vbyte: args.fee_tokens_per_vbyte, inputs: args.inputs.map(input => ({ transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, })), lnd: args.lnd, min_confirmations: allowZeroConfirmationInputs, outputs: args.addresses.map(address => ({address, tokens: dust})), }, cbk); }], // Unlock the UTXOs from the template unlockTemplate: ['createTemplate', ({createTemplate}, cbk) => { return asyncEach(createTemplate.inputs, (utxo, cbk) => { return unlockUtxo({ id: utxo.lock_id, lnd: args.lnd, transaction_id: utxo.transaction_id, transaction_vout: utxo.transaction_vout, }, cbk); }, cbk); }], // Confirm that the PSBT can be created with the maximal amount normally createMaximumNormal: [ 'createTemplate', 'unlockTemplate', asyncReflect(({createTemplate}, cbk) => { // LND as of 0.13.3 sets its max funding value as a sum of all outputs const max = sumOf(createTemplate.outputs.map(n => n.tokens)); // The maximum transaction will only carry over the non-change outputs const outs = createTemplate.outputs.filter(n => !n.is_change); // Calculate a baseline value for all of the non-change outputs const baseline = sumOf(outs.map(n => n.tokens)); // Fund a PSBT that is the same as the template, but adding the change return fundPsbt({ fee_tokens_per_vbyte: args.fee_tokens_per_vbyte, inputs: args.inputs.map(input => ({ transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, })), lnd: args.lnd, min_confirmations: allowZeroConfirmationInputs, outputs: args.addresses.map((address, i) => ({ address, tokens: !i ? dust + max - baseline : dust, })), }, cbk); })], // Make sure that maximum spend is created, or use adjusted dust value createMaximum: [ 'createMaximumNormal', 'createTemplate', 'unlockTemplate', ({createMaximumNormal, createTemplate}, cbk) => { // Exit early when creating a maximum spend worked as is if (!!createMaximumNormal.value) { return cbk(null, createMaximumNormal.value); } // Repeat calculations for the spend const adjustment = ceil(args.fee_tokens_per_vbyte * adjustFactor); const max = sumOf(createTemplate.outputs.map(n => n.tokens)); const outs = createTemplate.outputs.filter(n => !n.is_change); const baseline = sumOf(outs.map(n => n.tokens)); // In LND 0.15.3 spending P2TR outputs requires adjusted dust value return fundPsbt({ fee_tokens_per_vbyte: args.fee_tokens_per_vbyte, inputs: args.inputs.map(input => ({ transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, })), lnd: args.lnd, min_confirmations: allowZeroConfirmationInputs, outputs: args.addresses.map((address, i) => ({ address, tokens: !i ? dust + max - baseline - adjustment : dust, })), }, cbk); }], // Sign the maximum to get the raw transaction for fee calculation finalizeMaximum: ['createMaximum', ({createMaximum}, cbk) => { return signPsbt({lnd: args.lnd, psbt: createMaximum.psbt}, cbk); }], // Derive the maximum spendable tokens from the final TX max: ['finalizeMaximum', ({finalizeMaximum}, cbk) => { // The fee will be the difference between in tokens and out tokens const inTokens = args.inputs.map(n => n.tokens); // Use the real transaction to confirm the maximum values const tx = fromHex(finalizeMaximum.transaction); const maxTokens = sumOf(tx.outs.map(n => n.value)); const feeTokens = inTokens - maxTokens; return cbk(null, { fee_tokens_per_vbyte: feeTokens / tx.virtualSize(), max_tokens: maxTokens, }); }], // Unlock the maximum PSBT UTXOs unlockMaximum: [ 'createMaximum', 'finalizeMaximum', ({createMaximum}, cbk) => { return asyncEach(createMaximum.inputs, (utxo, cbk) => { return unlockUtxo({ id: utxo.lock_id, lnd: args.lnd, transaction_id: utxo.transaction_id, transaction_vout: utxo.transaction_vout, }, cbk); }, cbk); }], }, returnResult({reject, resolve, of: 'max'}, cbk)); }); };