UNPKG

balanceofsatoshis

Version:
429 lines (372 loc) 13.5 kB
const {address} = require('bitcoinjs-lib'); const asyncAuto = require('async/auto'); const {connectPeer} = require('ln-sync'); const {formatTokens} = require('ln-sync'); const {getPeers} = require('ln-service'); const {getPublicKey} = require('ln-service'); const {maintainUtxoLocks} = require('ln-sync'); const {makePeerRequest} = require('paid-services'); const {networks} = require('bitcoinjs-lib'); const {parsePaymentRequest} = require('ln-service'); const {pay} = require('ln-service'); const {payments} = require('bitcoinjs-lib'); const {prepareForChannelProposal} = require('ln-service'); const {reserveTransitFunds} = require('ln-sync'); const {returnResult} = require('asyncjs-util'); const {signTransaction} = require('ln-service'); const {Transaction} = require('bitcoinjs-lib'); const {waitForPendingOpen} = require('ln-sync'); const {balancedChannelKeyTypes} = require('./service_key_types'); const acceptRequestIdType = '0'; const bufferAsHex = buffer => buffer.toString('hex'); const defaultMaxFeeMtokens = '9000'; const {fromBech32} = address; const {fromHex} = Transaction; const fundingAmount = (capacity, rate) => (capacity + (190 * rate)) / 2; const fundingFee = rate => Math.ceil(190 / 2 * rate); const giveTokens = capacity => Math.ceil(capacity / 2); const hexAsBuffer = hex => Buffer.from(hex, 'hex'); const idAsHash = id => Buffer.from(id, 'hex').reverse(); const interval = 1000 * 15; const {isArray} = Array; const multiSigKeyFamily = 0; const networkBitcoin = 'btc'; const networkRegtest = 'btcregtest'; const networkTestnet = 'btctestnet'; const numAsHex = num => num.toString(16); const paddedHexNumber = n => n.length % 2 ? `0${n}` : n; const {p2ms} = payments; const {p2pkh} = payments; const p2pTimeoutMs = 1000 * 90; const {p2wsh} = payments; const relockIntervalMs = 1000 * 20; const times = 60 * 6; const {toOutputScript} = address; const transitKeyFamily = 805; /** Accept a balanced channel { accept_request: <Accept Balanced Request BOLT 11 Payment Request String> ask: <Ask Function> capacity: <Channel Capacity Tokens Number> fee_rate: <Chain Fee Rate Tokens Per VByte Number> lnd: <Authenticated LND API Object> logger: <Winston Logger Object> multisig_key_index: <MultiSig Key Index Number> network: <Network Name String> partner_public_key: <Partner Public Key Hex String> remote_multisig_key: <Remote MultiSig Public Key Hex String> remote_tx_id: <Remote Transit Transaction Id Hex String> remote_tx_vout: <Remote Transit Transaction Output Index Number> } @returns via cbk or Promise { transaction_id: <Funding Transaction Id Hex String> transaction_vout: <Funding Transaction Output Index Number> transactions: [<Hex Transaction String>] } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!args.accept_request) { return cbk([400, 'ExpectedAcceptRequestToAcceptBalancedChannel']); } if (!args.ask) { return cbk([400, 'ExpectedAskFunctionToAcceptBalancedChannel']); } if (!args.capacity) { return cbk([400, 'ExpectedCapacityToAcceptBalancedChannel']); } if (!args.fee_rate) { return cbk([400, 'ExpectedChainFeeRateToAcceptBalancedChannel']); } if (!args.lnd) { return cbk([400, 'ExpectedAuthenticatedLndToAcceptBalancedChannel']); } if (!args.logger) { return cbk([400, 'ExpectedWinstonLoggerToAcceptBalancedChannel']); } if (args.multisig_key_index === undefined) { return cbk([400, 'ExpectedMultiSigKeyIndexToAcceptBalancedChannel']); } if (!args.network) { return cbk([400, 'ExpectedNetworkNameToAcceptBalancedChannel']); } if (!args.remote_multisig_key) { return cbk([400, 'ExpectedRemoteMultiSigPublicKeyToAcceptChannel']); } if (!args.partner_public_key) { return cbk([400, 'ExpectedPartnerPublicKeyToAcceptBalancedChannel']); } return cbk(); }, // Connect to the peer connect: ['validate', ({}, cbk) => { return connectPeer({id: args.partner_public_key, lnd: args.lnd}, cbk); }], // Derive the multisig 2:2 key getMultiSigKey: ['validate', ({}, cbk) => { return getPublicKey({ family: multiSigKeyFamily, index: args.multisig_key_index, lnd: args.lnd, }, cbk); }], // BitcoinJS Network network: ['validate', ({}, cbk) => { switch (args.network) { case networkBitcoin: return cbk(null, networks.bitcoin); case networkRegtest: return cbk(null, networks.regtest); case networkTestnet: return cbk(null, networks.testnet); default: return cbk([400, 'UnsupportedNetworkForAcceptingBalancedChannel']); } }], // Get connected peers getPeers: ['connect', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)], // Make sure that the requesting peer is connected confirmPeer: ['getPeers', ({getPeers}, cbk) => { const connected = getPeers.peers.map(n => n.public_key); if (!connected.includes(args.partner_public_key)) { return cbk([ 400, 'ExpectedPeerConnectedToAcceptBalancedChannel', {key: args.partner_public_key}, ]); } return cbk(); }], // Ask for a signed transit transaction askForTransit: ['confirmPeer', ({}, cbk) => { return reserveTransitFunds({ ask: args.ask, lnd: args.lnd, logger: args.logger, rate: args.fee_rate, tokens: fundingAmount(args.capacity, args.fee_rate), }, cbk); }], // Create the funding transaction to sign with the transit key fundingTx: [ 'askForTransit', 'getMultiSigKey', ({askForTransit, getMultiSigKey}, cbk) => { if (!!askForTransit.inputs) { // Maintain a lock on the UTXOs until the tx confirms maintainUtxoLocks({ id: askForTransit.id, inputs: askForTransit.inputs, interval: relockIntervalMs, lnd: args.lnd, }, () => {}); } args.logger.info({refund_transaction: askForTransit.refund}); const keys = [getMultiSigKey.public_key, args.remote_multisig_key]; const tx = new Transaction(); const redeem = p2ms({ m: keys.length, pubkeys: keys.sort().map(hexAsBuffer), }); const remoteFunding = { id: args.remote_tx_id, vout: args.remote_tx_vout, }; const multiSig = p2wsh({redeem}); const script = multiSig.output; // The inputs to the funding tx are the transit transactions const inputs = [askForTransit, remoteFunding].map(({id, vout}) => ({ hash: idAsHash(id), index: vout, sequence: Number(), })); // Sort the inputs for BIP 69 deterministic encoding inputs.sort((a, b) => { const aHash = bufferAsHex(a.hash); const bHash = bufferAsHex(b.hash); return aHash.localeCompare(bHash) || a.index - b.index; }); // Add the inputs to the channel funding transaction inputs.forEach(n => tx.addInput(n.hash, n.index, n.sequence)); // The output to the channel funding is capacity paid to the 2:2 addr tx.addOutput(script, args.capacity); args.logger.info({ funding_tx_id: tx.getId(), waiting_for_full_channel_proposal: true, }); return cbk(null, { pending_channel_id: bufferAsHex(multiSig.hash), transaction: tx.toHex(), transaction_id: tx.getId(), transaction_vout: tx.outs.findIndex(n => n.script.equals(script)), }); }], // Sign the funding transaction spending the transit funds signFunding: [ 'askForTransit', 'fundingTx', 'network', ({askForTransit, fundingTx, network}, cbk) => { const feeRate = args.fee_rate; const tokens = giveTokens(args.capacity) + fundingFee(args.fee_rate); const tx = fromHex(fundingTx.transaction); // Find the input index where the funding outpoint is being spent const fundingVin = tx.ins.findIndex(input => { if (input.index !== askForTransit.vout) { return false; } return input.hash.equals(idAsHash(askForTransit.id)); }); // Sign the channel funding transaction return signTransaction({ lnd: args.lnd, inputs: [{ key_family: transitKeyFamily, key_index: askForTransit.index, output_script: askForTransit.output, output_tokens: tokens, sighash: Transaction.SIGHASH_ALL, vin: fundingVin, witness_script: askForTransit.script, }], transaction: fundingTx.transaction, }, cbk); }], // Prepare for an incoming channel prepareForProposal: ['fundingTx', 'signFunding', ({fundingTx}, cbk) => { return prepareForChannelProposal({ id: fundingTx.pending_channel_id, key_index: args.multisig_key_index, lnd: args.lnd, remote_key: args.remote_multisig_key, transaction_id: fundingTx.transaction_id, transaction_vout: fundingTx.transaction_vout, }, cbk); }], // Derive the accept records to communicate accept details acceptRecords: [ 'askForTransit', 'getMultiSigKey', 'signFunding', ({askForTransit, getMultiSigKey, signFunding}, cbk) => { const [fundingSignature] = signFunding.signatures; return cbk(null, [ { type: balancedChannelKeyTypes.multisig_public_key, value: getMultiSigKey.public_key, }, { type: balancedChannelKeyTypes.transit_tx_id, value: askForTransit.id, }, { type: balancedChannelKeyTypes.transit_tx_vout, value: paddedHexNumber(numAsHex(askForTransit.vout)), }, { type: balancedChannelKeyTypes.funding_signature, value: fundingSignature, }, { type: balancedChannelKeyTypes.transit_public_key, value: askForTransit.key, }, ]); }], // Try using p2p communication to accept the request p2pAcceptRequest: [ 'acceptRecords', 'prepareForProposal', ({acceptRecords}, cbk) => { return makePeerRequest({ lnd: args.lnd, records: [].concat(acceptRecords).concat({ type: acceptRequestIdType, value: parsePaymentRequest({request: args.accept_request}).id, }), timeout: p2pTimeoutMs, to: args.partner_public_key, type: balancedChannelKeyTypes.accept_request, }, err => { // Exit early when the request had an issue, fail back to TLV payment if (!!err) { return cbk(null, {is_accepted: false}); } return cbk(null, {is_accepted: true}); }); }], // Pay the accept balanced channel request to send accept messages payAcceptRequest: [ 'acceptRecords', 'p2pAcceptRequest', 'prepareForProposal', ({acceptRecords, p2pAcceptRequest}, cbk) => { // Exit early when the peer accepted over p2p comms if (!!p2pAcceptRequest.is_accepted) { return cbk(); } return pay({ lnd: args.lnd, max_fee_mtokens: defaultMaxFeeMtokens, messages: acceptRecords, request: args.accept_request, }, cbk); }], // Wait for a pending channel waitForPendingChannel: [ 'askForTransit', 'fundingTx', 'payAcceptRequest', ({askForTransit, fundingTx}, cbk) => { return waitForPendingOpen({ interval, times, capacity: args.capacity, lnd: args.lnd, local_balance: giveTokens(args.capacity), partner_public_key: args.partner_public_key, transaction_id: fundingTx.transaction_id, transaction_vout: fundingTx.transaction_vout, }, err => { if (!!err) { return cbk(err); } return cbk(null, { transaction_id: fundingTx.transaction_id, transaction_vout: fundingTx.transaction_vout, transactions: [askForTransit.transaction], }); }); }], // The peer should broadcast their funding peerBroadcast: ['waitForPendingChannel', ({}, cbk) => { const tokens = giveTokens(args.capacity) + fundingFee(args.fee_rate); args.logger.info({ peer_transaction_id: args.remote_tx_id, paying: formatTokens({tokens}).display, out_index: args.remote_tx_vout, }); return cbk(); }], }, returnResult({reject, resolve, of: 'waitForPendingChannel'}, cbk)); }); };