UNPKG

balanceofsatoshis

Version:
384 lines (319 loc) 12.6 kB
const asyncAuto = require('async/auto'); const asyncEach = require('async/each'); const {broadcastTransaction} = require('ln-sync'); const {formatTokens} = require('ln-sync'); const {fundPsbt} = require('ln-service'); const {getChainFeeRate} = require('ln-service'); const {getMaxFundAmount} = require('ln-sync'); const {getNetwork} = require('ln-sync'); const {getUtxos} = require('ln-service'); const {parseAmount} = require('ln-accounting'); const {returnResult} = require('asyncjs-util'); const {signPsbt} = require('ln-service'); const {Transaction} = require('bitcoinjs-lib'); const {unlockUtxo} = require('ln-service'); const allowUnconfirmed = 0; const asBigUnit = n => (n / 1e8).toFixed(8); const asOutpoint = utxo => `${utxo.transaction_id}:${utxo.transaction_vout}`; const asInput = n => ({transaction_id: n.id, transaction_vout: n.vout}); const asUtxo = n => ({id: n.slice(0, 64), vout: Number(n.slice(65))}); const bufferAsHex = buffer => buffer.toString('hex'); const dustValue = 293; const formattedFeeRate = n => n.toFixed(2); const {fromHex} = Transaction; const hasMaxAmount = amounts => !!amounts.find(n => !!n && !!/max/gim.test(n)); const {isArray} = Array; const isOutpoint = n => !!n && /^[0-9A-F]{64}:[0-9]{1,6}$/i.test(n); const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n); const isValidFeeRate = n => !n || Number.isInteger(Number(n)); const minConfs = 1; const sumOf = arr => arr.reduce((sum, n) => sum + n, Number()); const taprootAddressVersion = 1; const txHashAsTxId = hash => hash.reverse().toString('hex'); /** Fund and sign a transaction { addresses: [<Address String>] amounts: [<Amount String>] ask: <Ask Function> spend: [<Coin Outpoint String>] [fee_tokens_per_vbyte]: <Fee Tokens Per Virtual Byte Number> [is_broadcast]: <Broadcast Signed Transaction Bool> is_dry_run: <Release Locks on Transaction Bool> [is_selecting_utxos]: <Interactively Select UTXOs to Spend Bool> lnd: <Authenticated LND API Object> logger: <Winston Logger Object> utxos: [<Unspent Transaction Outpoint String>] } @returns via cbk or Promise { signed_transaction: <Hex Encoded Raw Transaction String> } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!args.lnd) { return cbk([400, 'ExpectedAuthenticatedLndToFundTransaction']); } if (!isArray(args.amounts)) { return cbk([400, 'ExpectedAddressesToFundTransaction']); } if (!isArray(args.addresses)) { return cbk([400, 'ExpectedAddressesToFundTransaction']); } if (!args.addresses.length) { return cbk([400, 'ExpectedAddressToSendFundsToInTransaction']); } if (args.addresses.length !== args.amounts.length) { return cbk([400, 'ExpectedAmountOfFundsToSendToAddress']); } if (!!args.addresses.find(n => isPublicKey(n))) { return cbk([400, 'ExpectedFundPayingToAddressesNotPublicKeys']); } if (!args.ask) { return cbk([400, 'ExpectedAskFunctionToFundTransaction']); } if (!isValidFeeRate(args.fee_tokens_per_vbyte)) { return cbk([400, 'ExpectedIntegerFeeRateForFundingTransaction']); } if (!!args.is_broadcast && !!args.is_dry_run) { return cbk([400, 'BroadcastingSignedTxUnsupportedInDryRun']); } if (!isArray(args.utxos)) { return cbk([400, 'ExpectedArrayOfUtxosToSpendToFundTransaction']); } if (args.utxos.find(n => !isOutpoint(n))) { return cbk([400, 'ExpectedOutpointFormattedUtxoToFundTransaction']); } if (!!args.utxos.length && !!args.is_selecting_utxos) { return cbk([400, 'ExpectedEitherSelectUtxosOrExplicitUtxosNotBoth']); } return cbk(); }, // Get the current fee rate getFee: ['validate', ({}, cbk) => getChainFeeRate({lnd: args.lnd}, cbk)], // Get the network name getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)], // Derive a list of outputs to guide input selection outputs: ['validate', ({}, cbk) => { // Exit early when the amount is open ended and thus depends on inputs if (hasMaxAmount(args.amounts)) { return cbk(); } try { const outputs = args.addresses.map((address, i) => { const {tokens} = parseAmount({amount: args.amounts[i]}); return {address, tokens}; }); return cbk(null, outputs); } catch (err) { return cbk([400, err.message]); } }], // Get UTXOs to use for input selection and final fee rate calculation getUtxos: ['validate', ({}, cbk) => getUtxos({lnd: args.lnd}, cbk)], // Select inputs to spend utxos: ['getUtxos', 'outputs', ({getUtxos, outputs}, cbk) => { // Exit early when UTXOs are all specified already if (!!args.utxos.length) { return cbk(null, args.utxos); } // Exit early when not selecting UTXOs interactively if (!args.is_selecting_utxos) { return cbk(null, []); } // Only selecting confirmed utxos is supported const utxos = getUtxos.utxos.filter(n => !!n.confirmation_count); // Make sure there are some UTXOs to select if (!utxos.length) { return cbk([400, 'WalletHasZeroConfirmedUtxos']); } return args.ask({ choices: utxos.map(utxo => ({ name: `${asBigUnit(utxo.tokens)} ${asOutpoint(utxo)}`, value: asOutpoint(utxo), })), loop: false, message: 'Select UTXOs to spend', name: 'inputs', type: 'checkbox', validate: input => { // A selection is required if (!input.length) { return false; } const tokens = sumOf(input.map(utxo => { return utxos.find(n => asOutpoint(n) === utxo.value).tokens; })); // Exit early when the amount is open ended if (hasMaxAmount(args.amounts)) { return true; } const amounts = outputs.map(n => n.tokens); const missingTok = asBigUnit(sumOf(amounts) - tokens); if (tokens < sumOf(amounts)) { return `Selected ${asBigUnit(tokens)}, need ${missingTok} more`; } return true; } }, ({inputs}) => cbk(null, inputs)); }], // Calculate the maximum possible amount to fund for selected inputs getMax: [ 'getFee', 'getUtxos', 'outputs', 'utxos', ({getFee, getUtxos, outputs, utxos}, cbk) => { // Exit early when the amount is not open ended if (!hasMaxAmount(args.amounts)) { return cbk(null, {}); } // Because of anchor channel requirements, don't allow open ended max if (!utxos.length) { return cbk([400, 'MaxAmountOnlySupportedWhenUtxosSpecified']); } const feeRate = args.fee_tokens_per_vbyte || getFee.tokens_per_vbyte; // Find the local UTXOs in order to get the input values const spend = utxos.map(outpoint => { return getUtxos.utxos.find(n => asOutpoint(n) === outpoint); }); // Make sure that all inputs are known if (spend.filter(n => !n).length) { const knownUtxos = spend.filter(n => !!n); return cbk([400, 'UnknownInputSelected', {known: knownUtxos}]); } return getMaxFundAmount({ addresses: args.addresses, fee_tokens_per_vbyte: feeRate, inputs: spend.map(utxo => ({ tokens: utxo.tokens, transaction_id: utxo.transaction_id, transaction_vout: utxo.transaction_vout, })), lnd: args.lnd, }, cbk); }], // Parse amounts and put together the final set of outputs finalOutputs: ['getMax', ({getMax}, cbk) => { try { const outputs = args.addresses.map((address, i) => { const amount = args.amounts[i]; const variables = {max: getMax.max_tokens}; return {address, tokens: parseAmount({amount, variables}).tokens}; }); return cbk(null, outputs); } catch (err) { return cbk([400, err.message]); } }], // Create a funded PSBT fund: [ 'finalOutputs', 'getFee', 'getNetwork', 'utxos', ({finalOutputs, getFee, getNetwork, utxos}, cbk) => { const inputs = utxos.map(asUtxo).map(asInput); const feeRate = args.fee_tokens_per_vbyte || getFee.tokens_per_vbyte; if (!!finalOutputs.filter(n => n.tokens < dustValue).length) { return cbk([400, 'ExpectedNonDustAmountValueForFundingAmount']); } args.logger.info({ send_to: finalOutputs.map(({address, tokens}) => ({ [address]: formatTokens({tokens}).display, })), requested_fee_rate: feeRate, }); return fundPsbt({ fee_tokens_per_vbyte: feeRate, inputs: !!inputs.length ? inputs : undefined, lnd: args.lnd, min_confirmations: !!inputs.length ? allowUnconfirmed : undefined, outputs: finalOutputs, }, cbk); }], // Sign the funded PSBT sign: ['fund', ({fund}, cbk) => { const [change] = fund.outputs.filter(n => !!n.is_change); const total = sumOf(fund.outputs.map(n => n.tokens)); const tokens = !!change ? change.tokens : undefined; args.logger.info({ change: !!tokens ? formatTokens({tokens}).display : undefined, sum_of_outputs: formatTokens({tokens: total}).display, spending_utxos: fund.inputs.map(asOutpoint), }); return signPsbt({lnd: args.lnd, psbt: fund.psbt}, cbk); }], // Unlock the locked UTXOs in a dry run scenario unlock: ['fund', 'sign', ({fund}, cbk) => { // Exit early and keep UTXOs locked when not a dry run if (!args.is_dry_run) { return cbk(); } return asyncEach(fund.inputs, (input, cbk) => { return unlockUtxo({ id: input.lock_id, lnd: args.lnd, transaction_id: input.transaction_id, transaction_vout: input.transaction_vout, }, cbk); }, cbk); }], // Final funded transaction funded: ['getUtxos', 'sign', ({getUtxos, sign}, cbk) => { // Match the inputs of the tx up to the wallet outputs const tx = fromHex(sign.transaction); // Find the UTXOs that are being spent in the final transaction const spending = tx.ins.map(input => { const outpoint = asOutpoint({ transaction_id: txHashAsTxId(input.hash), transaction_vout: input.index, }); return getUtxos.utxos.find(n => asOutpoint(n) === outpoint); }); // Make sure the spending UTXOs are known if (spending.filter(n => !n).length) { return cbk([503, 'ExpectedSpendingKnownUtxosForFundedTx']); } const inputsValue = sumOf(spending.map(n => n.tokens)); const outputsValue = sumOf(tx.outs.map(n => n.value)); const feeTotal = inputsValue - outputsValue; return cbk(null, { fee_tokens_per_vbyte: formattedFeeRate(feeTotal / tx.virtualSize()), signed_transaction: sign.transaction, }); }], // Broadcast the signed transaction broadcast: ['funded', ({funded}, cbk) => { // Exit early when not broadcasting the transaction if (!args.is_broadcast) { return cbk(null, { fee_tokens_per_vbyte: funded.fee_tokens_per_vbyte, signed_transaction: funded.signed_transaction, }); } args.logger.info({ fee_tokens_per_vbyte: funded.fee_tokens_per_vbyte, signed_transaction: funded.signed_transaction, }); return broadcastTransaction({ lnd: args.lnd, logger: args.logger, transaction: funded.signed_transaction, }, cbk); }] }, returnResult({reject, resolve, of: 'broadcast'}, cbk)); }); };