diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb8697..e259727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Versions -## Version 3.4.0 +## Version 3.4.1 - `peers`: add --sort to sort returned peers by an attribute diff --git a/arrays/index.js b/arrays/index.js new file mode 100644 index 0000000..7ce9595 --- /dev/null +++ b/arrays/index.js @@ -0,0 +1,3 @@ +const sortBy = require('./sort_by'); + +module.exports = {sortBy}; diff --git a/arrays/sort_by.js b/arrays/sort_by.js new file mode 100644 index 0000000..fad5eff --- /dev/null +++ b/arrays/sort_by.js @@ -0,0 +1,44 @@ +const {isArray} = Array; + +const equalTo = 0; +const greaterThan = 1; +const lessThan = -1; + +/** Sort array by attribute, lowest to highest + + { + array: [] + attribute: + } + + @throws + + + @returns + { + sorted: [] + } +*/ +module.exports = ({array, attribute}) => { + if (!isArray(array)) { + throw new Error('ExpectedArrayToSortByAttribute'); + } + + if (!attribute) { + throw new Error('ExpectedAttributeToSortArrayBy'); + } + + const sorted = array.slice().sort((a, b) => { + if (a[attribute] > b[attribute]) { + return greaterThan; + } + + if (b[attribute] > a[attribute]) { + return lessThan; + } + + return equalTo; + }); + + return {sorted}; +}; diff --git a/encryption/decrypt_payload.js b/encryption/decrypt_payload.js index cdb71fb..e363fc5 100644 --- a/encryption/decrypt_payload.js +++ b/encryption/decrypt_payload.js @@ -44,7 +44,11 @@ module.exports = ({encrypted, secret}) => { const ciphertext = Hex.parse(trim(hex, hex.length - zeroIndex(hex))); - const clear = decrypt({ciphertext}, key, {iv, padding, mode: mode.CFB}); + try { + const clear = decrypt({ciphertext}, key, {iv, padding, mode: mode.CFB}); - return {payload: clear.toString(enc.Utf8)}; + return {payload: clear.toString(enc.Utf8)}; + } catch (err) { + throw new Error('FailedToDecryptCipherTextWithSecretKey'); + } }; diff --git a/lnd/lnd_credentials.js b/lnd/lnd_credentials.js index 54cef5e..8c5b7da 100644 --- a/lnd/lnd_credentials.js +++ b/lnd/lnd_credentials.js @@ -59,11 +59,15 @@ module.exports = ({node}, cbk) => { const [chains, nets] = defaults; - const all = chains.map(chain => nets.map(network => ({chain, network}))); + const all = chains.map(chain => { + return nets.map(network => ({chain, network})) + }); // Find the default macaroon return asyncDetectSeries(flatten(all), ({chain, network}, cbk) => { - const macPath = [].concat(pathToMac).concat([chain, network, macName]); + const macPath = [] + .concat(pathToMac) + .concat([chain, network, macName]); return readFile(join(...[path].concat(macPath)), (_, macaroon) => { return cbk(null, macaroon); @@ -76,11 +80,13 @@ module.exports = ({node}, cbk) => { const {chain, network} = macaroon; - const macPath = [].concat(pathToMac).concat([chain, network, macName]); + const macPath = [] + .concat(pathToMac) + .concat([chain, network, macName]); return readFile(join(...[path].concat(macPath)), (err, macaroon) => { if (!!err) { - return cbk([503, 'FailedToGetMacaroonData', err]); + return cbk([503, 'FailedToGetMacaroonData', {err}]); } return cbk(null, macaroon.toString(base64)); diff --git a/network/get_peers.js b/network/get_peers.js index f1a298b..dfc32cc 100644 --- a/network/get_peers.js +++ b/network/get_peers.js @@ -5,6 +5,7 @@ const {getNode} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {authenticatedLnd} = require('./../lnd'); +const {sortBy} = require('./../arrays'); const defaultSort = 'public_key'; const sumOf = arr => arr.reduce((sum, n) => sum + n); @@ -52,10 +53,6 @@ module.exports = (args, cbk) => { const maxOutbound = args.outbound_liquidity_below; const peerKeys = getChannels.channels.map(n => n.partner_public_key); - const sortBy = (arr, attr) => arr.slice().sort((a,b) => { - return (a[attr] > b[attr]) ? 1 : ((b[attr] > a[attr]) ? -1 : 0); - }); - const peers = await asyncMap(uniq(peerKeys), async publicKey => { const channels = getChannels.channels.filter(channel => { return channel.partner_public_key === publicKey; @@ -75,7 +72,8 @@ module.exports = (args, cbk) => { }; }); - return sortBy(peers, args.sort_by || defaultSort) + return sortBy({array: peers, attribute: args.sort_by || defaultSort}) + .sorted .filter(n => !maxInbound || n.inbound_liquidity < maxInbound) .filter(n => !maxOutbound || n.outbound_liquidity < maxOutbound) .map(n => ({ diff --git a/network/probe_destination.js b/network/probe_destination.js index 6cf2217..c40d6bd 100644 --- a/network/probe_destination.js +++ b/network/probe_destination.js @@ -10,12 +10,16 @@ const {authenticatedLnd} = require('./../lnd'); const executeProbe = require('./execute_probe'); const {findMaxRoutable} = require('./../routing'); const {getInboundPath} = require('./../routing'); +const {sortBy} = require('./../arrays'); +const cltvBuffer = 3; const defaultCltvDelta = 144; const defaultTokens = 10; +const {floor} = Math; const {isArray} = Array; const maxCltvDelta = 144 * 30; const {now} = Date; +const reserveRatio = 0.01; /** Determine if a destination can be paid by probing it @@ -111,18 +115,22 @@ module.exports = (args, cbk) => { const withPeer = channels.filter(n => n.partner_public_key == outPeer); if (!withPeer.length) { - return cbk([404, 'NoActiveChannelWithChosenPeer']); + return cbk([404, 'NoActiveChannelWithOutgoingPeer']); } - const withBalance = withPeer - .filter(n => tokens < n.local_balance - (n.local_reserve || 0)); + const withBalance = withPeer.filter(n => { + const reserve = n.local_reserve || floor(n.capacity * reserveRatio); + + return n.local_balance - tokens > reserve + n.commit_transaction_fee; + }); if (!withBalance.length) { return cbk([404, 'NoChannelWithSufficientBalance']); } - const [channel] = withBalance - .sort((a, b) => a.local_balance < b.local_balance ? -1 : 1); + const attribute = 'local_balance'; + + const [channel] = sortBy({attribute, array: withBalance}).sorted; return cbk(null, channel.id); }], @@ -169,7 +177,7 @@ module.exports = (args, cbk) => { ({getHeight, getInboundPath, getLnd, outgoingChannelId, to}, cbk) => { return executeProbe({ - cltv_delta: to.cltv_delta || defaultCltvDelta, + cltv_delta: (to.cltv_delta || defaultCltvDelta) + cltvBuffer, destination: to.destination, is_strict_hints: !!getInboundPath, lnd: getLnd.lnd, @@ -213,6 +221,8 @@ module.exports = (args, cbk) => { return cbk([400, 'MaxFeeTooLow', {required_fee: probe.route.fee}]); } + args.logger.info({paying: probe.route.hops.map(({channel}) => channel)}); + return payViaRoutes({ id: to.id, lnd: getLnd.lnd, diff --git a/network/send_gift.js b/network/send_gift.js index 063b189..c777e34 100644 --- a/network/send_gift.js +++ b/network/send_gift.js @@ -1,17 +1,18 @@ const asyncAuto = require('async/auto'); -const {authenticatedLndGrpc} = require('ln-service'); const {createInvoice} = require('ln-service'); const {getChannel} = require('ln-service'); const {getChannels} = require('ln-service'); const {getWalletInfo} = require('ln-service'); +const {payViaRoutes} = require('ln-service'); const {routeFromChannels} = require('ln-service'); -const {pay} = require('ln-service'); const {returnResult} = require('asyncjs-util'); -const {lndCredentials} = require('./../lnd'); +const {authenticatedLnd} = require('./../lnd'); +const {channelForGift} = require('./../routing'); +const {giftRoute} = require('./../routing'); +const {sortBy} = require('./../arrays'); const {floor} = Math; -const {isArray} = Array; const minFeeRate = 0; const minReceivableMtokens = BigInt(1000); const mtokPerTok = BigInt(1000); @@ -33,7 +34,7 @@ const reserveRatio = 0.01; module.exports = ({node, to, tokens}, cbk) => { return asyncAuto({ // Credentials - credentials: cbk => lndCredentials({node}, cbk), + getLnd: cbk => authenticatedLnd({node}, cbk), // Check arguments validate: cbk => { @@ -49,51 +50,37 @@ module.exports = ({node, to, tokens}, cbk) => { }, // Lnd - lnd: ['credentials', 'validate', ({credentials}, cbk) => { - const {cert, macaroon, socket} = credentials; - - return cbk(null, authenticatedLndGrpc({cert, macaroon, socket}).lnd); - }], + lnd: ['getLnd', ({getLnd}, cbk) => cbk(null, getLnd.lnd)], // Get channels getChannels: ['lnd', ({lnd}, cbk) => getChannels({lnd}, cbk)], // Peer channel peerChannel: ['getChannels', ({getChannels}, cbk) => { - const {channels} = getChannels; + try { + const {channels} = getChannels; - const withPeer = channels.filter(n => n.partner_public_key === to); + return cbk(null, channelForGift({channels, to, tokens}).id); + } catch (err) { + const {message} = err; - if (!withPeer.length) { - return cbk([400, 'ExpectedDirectChannelWithPeerToGiftTo']); + switch (message) { + case 'NoActiveChannelWithSpecifiedPeer': + return cbk([400, 'SendingGiftRequiresActiveChannelWithPeer']); + + case 'NoActiveChannelWithSufficientLocalBalance': + return cbk([400, 'SendingGiftRequiresChannelWithSufficientBalance']); + + case 'NoActiveChannelWithSufficientRemoteBalance': + return cbk([400, 'SendingGiftRequiresChannelWithSomeRemoteBalance']); + + case 'NoDirectChannelWithSpecifiedPeer': + return cbk([400, 'SendingGiftRequiresDirectChannelWithPeer']); + + default: + return cbk([500, 'UnexpectedErrorDeterminingChannelForGift', {err}]); + } } - - const active = withPeer.filter(n => !!n.is_active); - - if (!active.length) { - return cbk([400, 'ExpectedActiveChannelWithPeerToGiftTo']); - } - - const hasTokens = active.filter(n => n.local_balance > tokens); - - if (!hasTokens.length) { - return cbk([400, 'ExpectedChannelWithAvailableFundsToGift']); - } - - const hasRemoteBalance = hasTokens - .filter(n => n.remote_balance > floor(n.capacity * reserveRatio)); - - if (!hasRemoteBalance.length) { - return cbk([400, 'ExpectedChannelWithSufficientRemoteReserveBalance']); - } - - hasRemoteBalance.sort((a, b) => { - return a.local_balance > b.local_balance ? -1 : 1; - }); - - const [channel] = hasRemoteBalance; - - return cbk(null, channel.id); }], // Get channel policy info @@ -118,66 +105,60 @@ module.exports = ({node, to, tokens}, cbk) => { 'getWallet', ({createInvoice, getChannel, getWallet}, cbk) => { - const channel = getChannel; - const destination = getWallet.public_key; - const height = getWallet.current_block_height; - const invoice = createInvoice; - const mtokens = minReceivableMtokens.toString(); - const mtokensToGive = BigInt(tokens) * mtokPerTok; - - const peerPolicy = channel.policies.find(n => n.public_key === to); - - peerPolicy.base_fee_mtokens = mtokensToGive.toString(); - peerPolicy.fee_rate = minFeeRate; - - const channels = [channel, channel]; - try { - const {route} = routeFromChannels({ - channels, - destination, - height, - mtokens, + const {route} = giftRoute({ + tokens, + channel: getChannel, + destination: getWallet.public_key, + height: getWallet.current_block_height, }); return cbk(null, route); } catch (err) { - return cbk([500, 'FailedToConstructGiftRoute', err]); + const {message} = err; + + switch (message) { + case 'GiftAmountTooLowToSend': + case 'OwnPolicyTooLowToCompleteForward': + case 'PeerPolicyTooLowToCompleteForward': + return cbk([400, 'AmountTooLowToCompleteGiftSend']); + + default: + return cbk([500, 'FailedToConstructGiftRoute', {err}]); + } } }], - // Pay + // Send the gift pay: [ 'createInvoice', 'lnd', 'route', ({createInvoice, lnd, route}, cbk) => { - const path = {id: createInvoice.id, routes: [route]}; + const {id} = createInvoice; - return pay({lnd, path}, (err, res) => { - if (!err) { - return cbk(null, {fee: res.fee}); + return payViaRoutes({id, lnd, routes: [route]}, (err, res) => { + if (!!err) { + const [errCode, errMessage] = err; + + console.log("ERR", err); + + switch (errMessage) { + case 'RejectedUnacceptableFee': + return cbk([400, 'GiftTokensAmountTooLowToSend']); + + default: + return cbk([503, 'UnexpectedErrorSendingGiftTokens', {err}]); + } } - if (!isArray(err)) { - return cbk([503, 'UnexpectedErrorSendingTokens', err]); - } - - const [errCode, errMessage] = err; - - switch (errMessage) { - case 'RejectedUnacceptableFee': - return cbk([400, 'GiftTokensAmountTooLowToSend']); - - default: - return cbk([503, 'UnexpectedErrorSendingGiftTokens', errMessage]); - } + return cbk(null, {gave_tokens: res.fee}); }); }], // Done paying - paid: ['pay', ({pay}, cbk) => cbk(null, {gave_tokens: pay.fee})], + paid: ['pay', ({pay}, cbk) => cbk(null, {gave_tokens: pay.gave_tokens})], }, returnResult({of: 'paid'}, cbk)); }; diff --git a/package-lock.json b/package-lock.json index 890cc45..96a2a9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "balanceofsatoshis", - "version": "3.4.0", + "version": "3.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -304,9 +304,9 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "anymatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.0.3.tgz", - "integrity": "sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.0.tgz", + "integrity": "sha512-Ozz7l4ixzI7Oxj2+cw+p0tVUt27BpaJ+1+q1TCeANWxHpvyn2+Un+YamBdfKu0uh8xLodGhoa1v7595NhKDAuA==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -431,7 +431,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base-x": { "version": "3.0.6", @@ -540,9 +541,9 @@ "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" }, "bitcoinjs-lib": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.4.tgz", - "integrity": "sha512-tSgxnXC3xEXw5DxKGIHNkzz+pxYzsSViT/XfQVIn8vM/b5kXtg3BJuFg6T1h2zhgG+iClzDf+5S/5wxFIddzWg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.5.tgz", + "integrity": "sha512-VAcdXH7cNocWwEF2dAVTozOZtDHB+FVZH9tc9OLbUKzRLiYxb3C5lvAzBmd1QJR3CtPRJELgw049MvM5fs2dow==", "requires": { "@types/node": "10.12.18", "bech32": "^1.1.2", @@ -642,6 +643,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -805,12 +807,12 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "cbor": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.2.1.tgz", - "integrity": "sha512-qIb7O0l8hPl1BcwogGFO0t7sURuo0iSjhgGfOCSU3EGSSEoVdnN4PVUcKA4VW/o9IsuvigSDoOmAqYoWXMpRCw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.3.0.tgz", + "integrity": "sha512-CvzaxQlaJVa88sdtTWvLJ++MbdtPHtZOBBNjm7h3YKUHILMs9nQyD4AC6hvFZy7GBVB3I6bRibJcxeHydyT2IQ==", "requires": { "bignumber.js": "^9.0.0", - "commander": "^2.20.0", + "commander": "^3.0.0", "json-text-sequence": "^0.1", "nofilter": "^1.0.3" } @@ -981,9 +983,9 @@ } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.1.tgz", + "integrity": "sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ==" }, "commondir": { "version": "1.0.1", @@ -1028,7 +1030,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "concat-stream": { "version": "1.6.2", @@ -1244,9 +1247,9 @@ "dev": true }, "dotenv": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", - "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==" + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz", + "integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==" }, "drbg.js": { "version": "1.0.1", @@ -1512,18 +1515,18 @@ "dev": true }, "flow-parser": { - "version": "0.106.1", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.106.1.tgz", - "integrity": "sha512-qLQMmEQOF/DJ/Y+tEhvy7CBme444i7pwNlK4GDCQ+dQ5vx5nOqJ/I5a5PcA8UrBhufp1vxzgOlmURJJQFDR5oQ==", + "version": "0.106.3", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.106.3.tgz", + "integrity": "sha512-V4K3tNmawjup+HySlIW5g3M0pywnTXvb32LeVxRHpQi4e3z2zCZIi+4JqnZWq/19JfYexEAquAbD95sORdOILQ==", "dev": true }, "flow-remove-types": { - "version": "2.106.1", - "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.106.1.tgz", - "integrity": "sha512-NoMxdUXjbNOgKWVZsjjrrQT8EGG9wl1NH7K5VDL6Fr5nyTkiwAcO38HAI7HdkGQNa8eBikDxZg2SDwRIBWjTfA==", + "version": "2.106.3", + "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.106.3.tgz", + "integrity": "sha512-gIy/xMWWtSp3eSA8ZHIEvnxK+b9nZoTdSHPPxohgoVgGjar55uSEorwqEDIE2GCwTE7ty6s/EdO0EuXlENXd+A==", "dev": true, "requires": { - "flow-parser": "^0.106.1", + "flow-parser": "^0.106.3", "pirates": "^3.0.2", "vlq": "^0.2.1" } @@ -1572,7 +1575,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "2.0.7", @@ -1617,6 +1621,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1642,87 +1647,51 @@ "dev": true }, "goldengate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/goldengate/-/goldengate-2.0.0.tgz", - "integrity": "sha512-rfJcOzHqc+q/JO/uJKy7FzrPCwHzlohOY4UiwMT/tcL8ianDUseLo3QUAG7RdNHTbHU1WRS+0ZQjPkxt/XfxbA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/goldengate/-/goldengate-2.0.1.tgz", + "integrity": "sha512-V6wsgjFa8e8cHzXSGgSaC/QGIHuEglItZX2W9UJKbnHNozVgzVIirvYZqaMKg7q7HvBuSo1+Buz95IWxchjoPQ==", "requires": { "@grpc/proto-loader": "0.5.1", "async": "3.1.0", - "asyncjs-util": "1.1.1", + "asyncjs-util": "1.1.2", "bip65": "1.0.3", "bitcoin-ops": "1.4.1", - "bitcoinjs-lib": "5.1.3", + "bitcoinjs-lib": "5.1.5", "bn.js": "5.0.0", - "cbor": "4.2.1", - "grpc": "1.22.2", - "ln-service": "41.1.3", + "cbor": "4.3.0", + "grpc": "1.23.3", + "ln-service": "44.0.1", "pushdata-bitcoin": "1.0.1", "request": "2.88.0", - "varuint-bitcoin": "1.1.0" + "varuint-bitcoin": "1.1.2" }, "dependencies": { - "asyncjs-util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.1.1.tgz", - "integrity": "sha512-a+zTr12g6hK+2/GxnAxj2c7C+NE1bLyGXNn6qCj57h8loaAl9pXEBgu7HM5TOvWsJ73e6HI+zociDnVBqaOUnQ==" - }, - "bitcoinjs-lib": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.3.tgz", - "integrity": "sha512-lvFQGWv9CMFetWewhfpUIbcYThRwo80rzhjPztorS4uMo27DwcdPAMKs9dYSkKpJvEZwPuIrUYSuT/+Hs1pZjg==", - "requires": { - "@types/node": "10.12.18", - "bech32": "^1.1.2", - "bip174": "^1.0.0", - "bip32": "^2.0.4", - "bip66": "^1.1.0", - "bitcoin-ops": "^1.4.0", - "bs58check": "^2.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.3", - "merkle-lib": "^2.0.10", - "pushdata-bitcoin": "^1.0.1", - "randombytes": "^2.0.1", - "tiny-secp256k1": "^1.1.1", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.0.4", - "wif": "^2.0.1" - } - }, "bn.js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.0.0.tgz", "integrity": "sha512-bVwDX8AF+72fIUNuARelKAlQUNtPOfG2fRxorbVvFk4zpHbqLrPdOGfVg5vrKwVzLLePqPBiATaOZNELQzmS0A==" }, - "bolt07": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.4.2.tgz", - "integrity": "sha512-y7xTyZbdGwQPMwl91VCRj9z6Shj3Wwz1L1YyWoSRTA64k+CIBaVsH1rdvJvEPZYbon1WOvnl8nNxcQfRPP37KA==", - "requires": { - "bn.js": "5.0.0" - } - }, "ln-service": { - "version": "41.1.3", - "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-41.1.3.tgz", - "integrity": "sha512-huQVbhiQi3aq7HTYiwhNHj0AvvlG/yUxcItsko0Hbo4Str0MRXxhWqZU/JKNx/KiEDbp7gsxHuWYJhKr1SDfPQ==", + "version": "44.0.1", + "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-44.0.1.tgz", + "integrity": "sha512-r3vMlXT6lt1+w7VdVZi5DNsxocMwfoA9wi86jkjq1ZbTqBp9VxvF5bHHNMEMp10oGyIjZKKchz5UlZOesaIdhw==", "requires": { "@datastructures-js/priority-queue": "1.0.2", "@grpc/proto-loader": "0.5.1", "async": "3.1.0", - "asyncjs-util": "1.1.1", + "asyncjs-util": "1.1.2", "basicauth-middleware": "3.1.0", "bech32": "1.1.3", - "bitcoinjs-lib": "5.1.2", + "bitcoinjs-lib": "5.1.5", "bn.js": "5.0.0", "body-parser": "1.19.0", - "bolt07": "1.4.2", + "bolt07": "1.4.3", "compression": "1.7.4", "cors": "2.8.5", - "dotenv": "8.0.0", + "dotenv": "8.1.0", "express": "4.17.1", - "grpc": "1.22.2", - "is-base64": "0.1.0", + "grpc": "1.23.3", + "is-base64": "1.0.0", "is-hex": "1.1.3", "lodash": "4.17.15", "morgan": "1.9.1", @@ -1730,32 +1699,7 @@ "request": "2.88.0", "safe-compare": "1.1.4", "secp256k1": "3.7.1", - "ws": "7.1.1" - }, - "dependencies": { - "bitcoinjs-lib": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.2.tgz", - "integrity": "sha512-Qa1TY8xaFRaLPD2YunfQX1vhHAh0387SJ/Zu7lNSRyzpg8lDru8gv+w6pqxOkcdj4dm4Fn1JmWb0m8Oy+8TfiA==", - "requires": { - "@types/node": "10.12.18", - "bech32": "^1.1.2", - "bip174": "^1.0.0", - "bip32": "^2.0.4", - "bip66": "^1.1.0", - "bitcoin-ops": "^1.4.0", - "bs58check": "^2.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.3", - "merkle-lib": "^2.0.10", - "pushdata-bitcoin": "^1.0.1", - "randombytes": "^2.0.1", - "tiny-secp256k1": "^1.1.1", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.0.4", - "wif": "^2.0.1" - } - } + "ws": "7.1.2" } } } @@ -1773,10 +1717,11 @@ "dev": true }, "grpc": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.22.2.tgz", - "integrity": "sha512-gaK59oAA5/mlOIn+hQO5JROPoAzsaGRpEMcrAayW5WGETS8QScpBoQ+XBxEWAAF0kbeGIELuGRCVEObKS1SLmw==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.23.3.tgz", + "integrity": "sha512-7vdzxPw9s5UYch4aUn4hyM5tMaouaxUUkwkgJlwbR4AXMxiYZJOv19N2ps2eKiuUbJovo5fnGF9hg/X91gWYjw==", "requires": { + "@types/bytebuffer": "^5.0.40", "lodash.camelcase": "^4.3.0", "lodash.clone": "^4.5.0", "nan": "^2.13.2", @@ -1817,7 +1762,7 @@ } }, "chownr": { - "version": "1.1.1", + "version": "1.1.2", "bundled": true }, "code-point-at": { @@ -1836,6 +1781,13 @@ "version": "1.0.2", "bundled": true }, + "debug": { + "version": "3.2.6", + "bundled": true, + "requires": { + "ms": "^2.1.1" + } + }, "deep-extend": { "version": "0.6.0", "bundled": true @@ -1873,12 +1825,24 @@ "wide-align": "^1.1.0" } }, + "glob": { + "version": "7.1.4", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "has-unicode": { "version": "2.0.1", "bundled": true }, "iconv-lite": { - "version": "0.4.23", + "version": "0.4.24", "bundled": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -1900,7 +1864,7 @@ } }, "inherits": { - "version": "2.0.3", + "version": "2.0.4", "bundled": true }, "ini": { @@ -1957,6 +1921,10 @@ } } }, + "ms": { + "version": "2.1.2", + "bundled": true + }, "needle": { "version": "2.4.0", "bundled": true, @@ -1964,19 +1932,6 @@ "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "bundled": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true - } } }, "node-pre-gyp": { @@ -2008,7 +1963,7 @@ "bundled": true }, "npm-packlist": { - "version": "1.4.1", + "version": "1.4.4", "bundled": true, "requires": { "ignore-walk": "^3.0.1", @@ -2099,24 +2054,10 @@ } }, "rimraf": { - "version": "2.6.3", + "version": "2.7.1", "bundled": true, "requires": { "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "bundled": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } } }, "safe-buffer": { @@ -2132,7 +2073,7 @@ "bundled": true }, "semver": { - "version": "5.7.0", + "version": "5.7.1", "bundled": true }, "set-blocking": { @@ -2140,7 +2081,7 @@ "bundled": true }, "signal-exit": { - "version": "3.0.1", + "version": "3.0.2", "bundled": true }, "string-width": { @@ -2354,6 +2295,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -2435,9 +2377,9 @@ "dev": true }, "is-base64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-base64/-/is-base64-0.1.0.tgz", - "integrity": "sha512-WRRyllsGXJM7ZN7gPTCCQ/6wNPTRDwiWdPK66l5sJzcU/oOzcIcRRf0Rux8bkpox/1yjt0F6VJRsQOIG2qz5sg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-base64/-/is-base64-1.0.0.tgz", + "integrity": "sha512-7McBWBgGDHS8OHvCnPVVxl3HdzitV6XyjM1x7vVxbzXIKJUzxphU5MDL986aZ/oKf/Q+j4Vh4m54Fjwi88n75g==" }, "is-binary-path": { "version": "2.1.0", @@ -2729,6 +2671,13 @@ "commander": "^2.15.1", "jsonparse": "^1.3.1", "lodash.get": "^4.4.2" + }, + "dependencies": { + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + } } }, "jsonparse": { @@ -2762,74 +2711,43 @@ "dev": true }, "ln-accounting": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/ln-accounting/-/ln-accounting-3.0.2.tgz", - "integrity": "sha512-0E1Ddzk29Gw3D4HPWowX7tDgO/QGvR+Cr4jdUUA2pK/cq5R9S4MFfcKk03sZvdswOuOC67YsQnKR0LS8fLjAlQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ln-accounting/-/ln-accounting-3.0.3.tgz", + "integrity": "sha512-iiXCQmrjfxBKUzgmImRlqEcTAlXFTETaIZgycahIS83kBd9Hp24n1Ha+LMd4/FBSJ3L3t7juOx6IeNI4GOneog==", "requires": { "async": "3.1.0", "asyncjs-util": "1.1.2", "json2csv": "4.5.2", - "ln-service": "41.1.4", + "ln-service": "44.0.1", "request": "2.88.0" }, "dependencies": { - "bitcoinjs-lib": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.3.tgz", - "integrity": "sha512-lvFQGWv9CMFetWewhfpUIbcYThRwo80rzhjPztorS4uMo27DwcdPAMKs9dYSkKpJvEZwPuIrUYSuT/+Hs1pZjg==", - "requires": { - "@types/node": "10.12.18", - "bech32": "^1.1.2", - "bip174": "^1.0.0", - "bip32": "^2.0.4", - "bip66": "^1.1.0", - "bitcoin-ops": "^1.4.0", - "bs58check": "^2.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.3", - "merkle-lib": "^2.0.10", - "pushdata-bitcoin": "^1.0.1", - "randombytes": "^2.0.1", - "tiny-secp256k1": "^1.1.1", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.0.4", - "wif": "^2.0.1" - } - }, "bn.js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.0.0.tgz", "integrity": "sha512-bVwDX8AF+72fIUNuARelKAlQUNtPOfG2fRxorbVvFk4zpHbqLrPdOGfVg5vrKwVzLLePqPBiATaOZNELQzmS0A==" }, - "bolt07": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.4.2.tgz", - "integrity": "sha512-y7xTyZbdGwQPMwl91VCRj9z6Shj3Wwz1L1YyWoSRTA64k+CIBaVsH1rdvJvEPZYbon1WOvnl8nNxcQfRPP37KA==", - "requires": { - "bn.js": "5.0.0" - } - }, "ln-service": { - "version": "41.1.4", - "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-41.1.4.tgz", - "integrity": "sha512-DRvYcKy04PnR6K5dn/6SwnvPUToIlnSsA2rvUQeAghyJuHuls82DmbudhY6YufTBFIPlKLoembFllyx5DzhhFQ==", + "version": "44.0.1", + "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-44.0.1.tgz", + "integrity": "sha512-r3vMlXT6lt1+w7VdVZi5DNsxocMwfoA9wi86jkjq1ZbTqBp9VxvF5bHHNMEMp10oGyIjZKKchz5UlZOesaIdhw==", "requires": { "@datastructures-js/priority-queue": "1.0.2", "@grpc/proto-loader": "0.5.1", "async": "3.1.0", - "asyncjs-util": "1.1.1", + "asyncjs-util": "1.1.2", "basicauth-middleware": "3.1.0", "bech32": "1.1.3", - "bitcoinjs-lib": "5.1.3", + "bitcoinjs-lib": "5.1.5", "bn.js": "5.0.0", "body-parser": "1.19.0", - "bolt07": "1.4.2", + "bolt07": "1.4.3", "compression": "1.7.4", "cors": "2.8.5", - "dotenv": "8.0.0", + "dotenv": "8.1.0", "express": "4.17.1", - "grpc": "1.22.2", - "is-base64": "0.1.0", + "grpc": "1.23.3", + "is-base64": "1.0.0", "is-hex": "1.1.3", "lodash": "4.17.15", "morgan": "1.9.1", @@ -2837,22 +2755,15 @@ "request": "2.88.0", "safe-compare": "1.1.4", "secp256k1": "3.7.1", - "ws": "7.1.1" - }, - "dependencies": { - "asyncjs-util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.1.1.tgz", - "integrity": "sha512-a+zTr12g6hK+2/GxnAxj2c7C+NE1bLyGXNn6qCj57h8loaAl9pXEBgu7HM5TOvWsJ73e6HI+zociDnVBqaOUnQ==" - } + "ws": "7.1.2" } } } }, "ln-service": { - "version": "43.2.0", - "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-43.2.0.tgz", - "integrity": "sha512-4PnlMtR621brWpYhvGXCB7+U7D/ws+GtgB8dbUWwEGz7m18goHaq5L+13tYPAuVmbRNFU1sa0BZmNqAHtQOaGg==", + "version": "44.0.2", + "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-44.0.2.tgz", + "integrity": "sha512-sxcCFjk77tSxs0fUQkXo4OxwktLAK93BCum8oxsRll+VNCREjrwvNRcPCwwH+jmRB7dL8k0dnELXtSICjNyqPQ==", "requires": { "@datastructures-js/priority-queue": "1.0.2", "@grpc/proto-loader": "0.5.1", @@ -2860,7 +2771,7 @@ "asyncjs-util": "1.1.2", "basicauth-middleware": "3.1.0", "bech32": "1.1.3", - "bitcoinjs-lib": "5.1.4", + "bitcoinjs-lib": "5.1.5", "bn.js": "5.0.0", "body-parser": "1.19.0", "bolt07": "1.4.3", @@ -2869,7 +2780,7 @@ "dotenv": "8.1.0", "express": "4.17.1", "grpc": "1.23.3", - "is-base64": "0.1.0", + "is-base64": "1.0.0", "is-hex": "1.1.3", "lodash": "4.17.15", "morgan": "1.9.1", @@ -2884,448 +2795,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.0.0.tgz", "integrity": "sha512-bVwDX8AF+72fIUNuARelKAlQUNtPOfG2fRxorbVvFk4zpHbqLrPdOGfVg5vrKwVzLLePqPBiATaOZNELQzmS0A==" - }, - "dotenv": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz", - "integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==" - }, - "grpc": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.23.3.tgz", - "integrity": "sha512-7vdzxPw9s5UYch4aUn4hyM5tMaouaxUUkwkgJlwbR4AXMxiYZJOv19N2ps2eKiuUbJovo5fnGF9hg/X91gWYjw==", - "requires": { - "@types/bytebuffer": "^5.0.40", - "lodash.camelcase": "^4.3.0", - "lodash.clone": "^4.5.0", - "nan": "^2.13.2", - "node-pre-gyp": "^0.13.0", - "protobufjs": "^5.0.3" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.2", - "bundled": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true - }, - "fs-minipass": { - "version": "1.2.6", - "bundled": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.4", - "bundled": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true - }, - "ini": { - "version": "1.3.5", - "bundled": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "bundled": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "bundled": true - } - } - }, - "ms": { - "version": "2.1.2", - "bundled": true - }, - "needle": { - "version": "2.4.0", - "bundled": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.13.0", - "bundled": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true - }, - "npm-packlist": { - "version": "1.4.4", - "bundled": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true - }, - "sax": { - "version": "1.2.4", - "bundled": true - }, - "semver": { - "version": "5.7.1", - "bundled": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true - }, - "tar": { - "version": "4.4.10", - "bundled": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.5", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true - } - } - }, - "protobufjs": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.3.tgz", - "integrity": "sha512-55Kcx1MhPZX0zTbVosMQEO5R6/rikNXd9b6RQK4KSPcrSIIwoXTtebIczUrXlwaSrbz4x8XUVThGPob1n8I4QA==", - "requires": { - "ascli": "~1", - "bytebuffer": "~5", - "glob": "^7.0.5", - "yargs": "^3.10.0" - } - }, - "ws": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", - "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", - "requires": { - "async-limiter": "^1.0.0" - } } } }, @@ -3526,6 +2995,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3536,9 +3006,9 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "minipass": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.4.0.tgz", - "integrity": "sha512-6PmOuSP4NnZXzs2z6rbwzLJu/c5gdzYg1mRI/WIYdx45iiX7T+a4esOzavD6V/KmBzAaopFSTZPZcUx73bqKWA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.5.0.tgz", + "integrity": "sha512-9FwMVYhn6ERvMR8XFdOavRz4QK/VJV8elU1x50vYexf9lslDcWe/f4HBRxCPd185ekRSjU6CfYyJCECa/CQy7Q==", "dev": true, "requires": { "safe-buffer": "^5.1.2", @@ -3810,6 +3280,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -3947,7 +3418,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "2.0.1", @@ -5956,9 +5428,9 @@ "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.2.tgz", + "integrity": "sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==", "dev": true }, "uglify-js": { @@ -5970,6 +5442,15 @@ "requires": { "commander": "~2.20.0", "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true, + "optional": true + } } }, "unicode-length": { @@ -6044,9 +5525,9 @@ } }, "varuint-bitcoin": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.0.tgz", - "integrity": "sha512-jCEPG+COU/1Rp84neKTyDJQr478/hAfVp5xxYn09QEH0yBjbmPeMfuuQIrp+BUD83hybtYZKhr5elV3bvdV1bA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", "requires": { "safe-buffer": "^5.1.1" } @@ -6176,7 +5657,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write-file-atomic": { "version": "3.0.0", @@ -6191,9 +5673,9 @@ } }, "ws": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.1.tgz", - "integrity": "sha512-o41D/WmDeca0BqYhsr3nJzQyg9NF5X8l/UdnFNux9cS3lwB+swm8qGWX5rn+aD6xfBU3rGmtHij7g7x6LxFU3A==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", + "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", "requires": { "async-limiter": "^1.0.0" } diff --git a/package.json b/package.json index cb9a107..2952d8a 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,15 @@ "async": "3.1.0", "asyncjs-util": "1.1.2", "bitcoin-ops": "1.4.1", - "bitcoinjs-lib": "5.1.4", + "bitcoinjs-lib": "5.1.5", "bolt03": "1.2.1", "bolt07": "1.4.3", "caporal": "1.3.0", "crypto-js": "3.1.9-1", - "goldengate": "2.0.0", + "goldengate": "2.0.1", "html2unicode": "1.1.3", - "ln-accounting": "3.0.2", - "ln-service": "43.2.0", + "ln-accounting": "3.0.3", + "ln-service": "44.0.2", "moment": "2.24.0", "request": "2.88.0", "stats-lite": "2.2.0", @@ -46,7 +46,7 @@ "url": "https://github.com/alexbosworth/balanceofsatoshis.git" }, "scripts": { - "test": "tap test/balances/*.js test/fiat/*.js test/network/*.js test/responses/*.js test/routing/*.js" + "test": "tap test/arrays/*.js test/balances/*.js test/encryption/*.js test/fiat/*.js test/network/*.js test/responses/*.js test/routing/*.js" }, - "version": "3.4.0" + "version": "3.4.1" } diff --git a/routing/channel_for_gift.js b/routing/channel_for_gift.js new file mode 100644 index 0000000..2c7be15 --- /dev/null +++ b/routing/channel_for_gift.js @@ -0,0 +1,77 @@ +const {sortBy} = require('./../arrays'); + +const {isArray} = Array; +const reserveValue = n => n.local_reserve || Math.floor(n.capacity * 0.01); + +/** Channel with sufficient balance for a circular gift route + + { + channels: [{ + capacity: + is_active: + local_balance: + [local_reserve]: + partner_public_key: + remote_balance: + }] + to: + tokens: + } + + @throws + + + @returns + { + id: + } +*/ +module.exports = ({channels, to, tokens}) => { + if (!isArray(channels)) { + throw new Error('ExpectedArrayOfChannelsToFindChannelWithBalance'); + } + + if (channels.find(n => !n) !== undefined) { + throw new Error('ExpectedChannelsInArrayOfChannels'); + } + + if (!to) { + throw new Error('ExpectedToPublicKeyToFindChannelWithBalance'); + } + + if (!tokens) { + throw new Error('ExpectedTokensToFindChannelWithSufficientBalance'); + } + + const withPeer = channels.filter(n => n.partner_public_key === to); + + if (!withPeer.length) { + throw new Error('NoDirectChannelWithSpecifiedPeer'); + } + + const active = withPeer.filter(n => !!n.is_active); + + if (!active.length) { + throw new Error('NoActiveChannelWithSpecifiedPeer'); + } + + const hasTokens = active.filter(channel => { + return channel.local_balance - tokens > reserveValue(channel); + }); + + if (!hasTokens.length) { + throw new Error('NoActiveChannelWithSufficientLocalBalance'); + } + + const array = hasTokens.filter(channel => { + return channel.remote_balance + tokens > reserveValue(channel) + }); + + if (!array.length) { + throw new Error('NoActiveChannelWithSufficientRemoteBalance'); + } + + const [{id}] = sortBy({array, attribute: 'local_balance'}).sorted; + + return {id}; +}; diff --git a/routing/gift_route.js b/routing/gift_route.js new file mode 100644 index 0000000..5ae763e --- /dev/null +++ b/routing/gift_route.js @@ -0,0 +1,121 @@ +const {routeFromChannels} = require('ln-service'); + +const defaultBaseFeeMtokens = '1000'; +const defaultFeeRate = 1; +const defaultMin = '1000'; +const feeRateDivisor = BigInt(1e6); +const {isArray} = Array; +const minFeeRate = 0; +const mtokPerTok = BigInt(1e3); + +/** Get a gift route + + { + channel: [{ + id: + policies: [{ + base_fee_mtokens: + cltv_delta: + fee_rate: + min_htlc_mtokens: + public_key: + }] + }] + destination: + height: + tokens: + } + + @throws + + + @returns + { + route: { + fee: + fee_mtokens: + hops: [{ + channel: + channel_capacity: + fee: + fee_mtokens: + forward: + forward_mtokens: + [public_key]: + timeout: + }] + mtokens: + timeout: + tokens: + } + } +*/ +module.exports = ({channel, destination, height, tokens}) => { + if (!channel) { + throw new Error('ExpectedChannelToCalculateGiftRoute'); + } + + if (!destination) { + throw new Error('ExpectedDestinationToCalculateGiftRoute'); + } + + if (!height) { + throw new Error('ExpectedHeightToCalculateGiftRoute'); + } + + if (!tokens) { + throw new Error('ExpectedTokensToCalculateGiftRoute'); + } + + const {policies} = channel; + + if (!isArray(policies) || !!policies.find(n => !n.public_key)) { + throw new Error('ExpectedChannelPoliciesToCalculateGiftRoute'); + } + + const policy = policies.find(n => n.public_key === destination); + + if (!policy) { + throw new Error('ExpectedDestinationPolicyToCalculateGiftRoute'); + } + + const peerPolicy = policies.find(n => n.public_key !== destination); + + if (!peerPolicy) { + throw new Error('ExpectedPeerPolicyToCalculateGiftRoute'); + } + + const minReceivableMtokens = BigInt(policy.min_htlc_mtokens || defaultMin); + const minSendableMtokens = BigInt(peerPolicy.min_htlc_mtokens || defaultMin); + + const mtokens = minReceivableMtokens.toString(); + const mtokensToGive = BigInt(tokens) * mtokPerTok; + + if (minSendableMtokens > minReceivableMtokens) { + throw new Error('PeerPolicyTooLowToCompleteForward'); + } + + const baseFee = BigInt(peerPolicy.base_fee_mtokens || defaultBaseFeeMtokens); + const feeRate = BigInt(peerPolicy.fee_rate || defaultFeeRate); + + const standardFee = (BigInt(mtokens) * feeRate / feeRateDivisor) + baseFee; + + // Make sure that the real fee is going to be lower than the gift + if (standardFee > mtokensToGive) { + throw new Error('GiftAmountTooLowToSend'); + } + + peerPolicy.base_fee_mtokens = mtokensToGive.toString(); + peerPolicy.fee_rate = minFeeRate; + + const channels = [channel, channel]; + + const {route} = routeFromChannels({ + channels, + destination, + height, + mtokens, + }); + + return {route}; +}; diff --git a/routing/index.js b/routing/index.js index e4ecc7e..1843ab5 100644 --- a/routing/index.js +++ b/routing/index.js @@ -1,4 +1,6 @@ +const channelForGift = require('./channel_for_gift'); const findMaxRoutable = require('./find_max_routable'); const getInboundPath = require('./get_inbound_path'); +const giftRoute = require('./gift_route'); -module.exports = {findMaxRoutable, getInboundPath}; +module.exports = {channelForGift, findMaxRoutable, getInboundPath, giftRoute}; diff --git a/routing/is_route_payable.js b/routing/is_route_payable.js index 5c62737..c4bd933 100644 --- a/routing/is_route_payable.js +++ b/routing/is_route_payable.js @@ -66,7 +66,7 @@ module.exports = ({channels, cltv, lnd, tokens}, cbk) => { route: ['getHeight', ({getHeight}, cbk) => { const {route} = routeFromChannels({ channels, - cltv, + cltv_delta: cltv, height: getHeight.current_block_height, mtokens: mtokensFromTokens(tokens), }); diff --git a/swaps/swap_out.js b/swaps/swap_out.js index 958124b..520bdfc 100644 --- a/swaps/swap_out.js +++ b/swaps/swap_out.js @@ -122,6 +122,24 @@ module.exports = (args, cbk) => { return authenticatedLnd({node: args.node}, cbk); }], + // Create a sweep address + createAddress: ['getLnd', 'recover', ({getLnd, recover}, cbk) => { + if (!!recover && recover.sweep_address) { + return cbk(null, {address: recover.sweep_address}); + } + + if (!!args.out_address) { + return cbk(null, {address: args.out_address}); + } + + return createChainAddress({ + format: 'p2wpkh', + is_unused: true, + lnd: getLnd.lnd, + }, + cbk); + }], + // Get channels getChannels: ['getLnd', ({getLnd}, cbk) => { return getChannels({lnd: getLnd.lnd, is_active: true}, cbk); @@ -132,6 +150,11 @@ module.exports = (args, cbk) => { return getWalletInfo({lnd: getLnd.lnd}, cbk); }], + // Get the current block height + getHeight: ['getWalletInfo', ({getWalletInfo}, cbk) => { + return cbk(null, getWalletInfo.current_block_height); + }], + // Figure out which channel to use when swapping channel: ['getChannels', 'getLnd', ({getChannels, getLnd}, cbk) => { if (!!args.recovery) { @@ -855,29 +878,6 @@ module.exports = (args, cbk) => { return cbk(); }], - // Get the current block height - getHeight: ['getWalletInfo', ({getWalletInfo}, cbk) => { - return cbk(null, getWalletInfo.current_block_height); - }], - - // Create a sweep address - createAddress: ['getLnd', 'recover', ({getLnd, recover}, cbk) => { - if (!!recover && recover.sweep_address) { - return cbk(null, {address: recover.sweep_address}); - } - - if (!!args.out_address) { - return cbk(null, {address: args.out_address}); - } - - return createChainAddress({ - format: 'p2wpkh', - is_unused: true, - lnd: getLnd.lnd, - }, - cbk); - }], - // Claim details claim: [ 'findDeposit', diff --git a/test/arrays/test_sort_by.js b/test/arrays/test_sort_by.js new file mode 100644 index 0000000..53a1e57 --- /dev/null +++ b/test/arrays/test_sort_by.js @@ -0,0 +1,45 @@ +const {test} = require('tap'); + +const {sortBy} = require('./../../arrays'); + +const tests = [ + { + args: {}, + description: 'An array is required', + error: 'ExpectedArrayToSortByAttribute', + }, + { + args: {array: []}, + description: 'An attribute to sort by is required', + error: 'ExpectedAttributeToSortArrayBy', + }, + { + args: {array: [{foo: 1}, {foo: 2}, {foo: 3}], attribute: 'foo'}, + description: 'Array is sorted when reversed', + expected: {sorted: [{foo: 1}, {foo: 2}, {foo: 3}]}, + }, + { + args: {array: [{foo: 1}, {foo: 3}, {foo: 2}], attribute: 'foo'}, + description: 'Array is sorted when jumbled', + expected: {sorted: [{foo: 1}, {foo: 2}, {foo: 3}]}, + }, + { + args: {array: [{foo: 3}, {foo: 3}, {foo: 2}], attribute: 'foo'}, + description: 'Array is sorted when equals exist', + expected: {sorted: [{foo: 2}, {foo: 3}, {foo: 3}]}, + }, +]; + +tests.forEach(({args, description, error, expected}) => { + return test(description, ({deepIs, end, throws}) => { + if (!!error) { + throws(() => sortBy(args), new Error(error), 'Got expected error'); + } else { + const {sorted} = sortBy(args); + + deepIs(sorted, expected.sorted, 'Array is sorted as expected'); + } + + return end(); + }); +}); diff --git a/test/encryption/fixtures.json b/test/encryption/fixtures.json new file mode 100644 index 0000000..5ecec65 --- /dev/null +++ b/test/encryption/fixtures.json @@ -0,0 +1,4 @@ +{ + "encrypted": "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", + "secret": "ed5b3aa03fea8c9e653a0dfcd356aa55614a3df89915d8d4ec2989f382e88448" +} diff --git a/test/encryption/test_decrypt_payload.js b/test/encryption/test_decrypt_payload.js new file mode 100644 index 0000000..4c620c2 --- /dev/null +++ b/test/encryption/test_decrypt_payload.js @@ -0,0 +1,53 @@ +const {test} = require('tap'); + +const {decryptPayload} = require('./../../encryption'); +const {encrypted} = require('./fixtures'); +const {secret} = require('./fixtures'); + +const {parse} = JSON; + +const tests = [ + { + args: {}, + description: 'An encrypted payload is required', + error: 'ExpectedEncryptedPayloadToDecrypt', + }, + { + args: {encrypted}, + description: 'A secret key is required', + error: 'ExpectedDecryptionSecretKeyToDecrypt', + }, + { + args: {encrypted, secret: 'ff'}, + description: 'A valid secret key is required', + error: 'FailedToDecryptCipherTextWithSecretKey', + }, + { + args: { + secret, + encrypted: Buffer.from(encrypted, 'base64').toString('hex'), + }, + description: 'Paylaod is decrypted', + expected: { + pair: 'BTCUSD', + price: 4004.14, + timestamp: '2019-01-10T00:00:11.000Z', + }, + }, +]; + +tests.forEach(({args, description, error, expected}) => { + return test(description, async ({end, equal, throws}) => { + if (!!error) { + throws(() => decryptPayload(args), new Error(error), 'Got error'); + } else { + const [{pair, price, timestamp}] = parse(decryptPayload(args).payload); + + equal(pair, expected.pair, 'Got expected pair'); + equal(price, expected.price, 'Got expectd price'); + equal(timestamp, expected.timestamp, 'Got expected timestamp'); + } + + return end(); + }); +}); diff --git a/test/routing/test_channel_for_gift.js b/test/routing/test_channel_for_gift.js new file mode 100644 index 0000000..d5e7f42 --- /dev/null +++ b/test/routing/test_channel_for_gift.js @@ -0,0 +1,117 @@ +const {test} = require('tap'); + +const {channelForGift} = require('./../../routing'); + +const tests = [ + { + args: {}, + description: 'An array of channels is required', + error: 'ExpectedArrayOfChannelsToFindChannelWithBalance', + }, + { + args: {channels: [null]}, + description: 'An array of channels objects is required', + error: 'ExpectedChannelsInArrayOfChannels', + }, + { + args: {channels: []}, + description: 'A to public key is required', + error: 'ExpectedToPublicKeyToFindChannelWithBalance', + }, + { + args: {channels: [], to: 'bob'}, + description: 'Tokens are required', + error: 'ExpectedTokensToFindChannelWithSufficientBalance', + }, + { + args: {channels: [], to: 'bob', tokens: 1}, + description: 'A direct channel is required', + error: 'NoDirectChannelWithSpecifiedPeer', + }, + { + args: {channels: [{partner_public_key: 'bob'}], to: 'bob', tokens: 1}, + description: 'An active channel is required', + error: 'NoActiveChannelWithSpecifiedPeer', + }, + { + args: { + channels: [{is_active: true, partner_public_key: 'bob'}], + to: 'bob', + tokens: 1, + }, + description: 'A channel with enough balance is required', + error: 'NoActiveChannelWithSufficientLocalBalance', + }, + { + args: { + channels: [{ + capacity: 20000, + is_active: true, + local_balance: 10000, + partner_public_key: 'bob', + }], + to: 'bob', + tokens: 500, + }, + description: 'A channel with enough remote balance is required', + error: 'NoActiveChannelWithSufficientRemoteBalance', + }, + { + args: { + channels: [{ + capacity: 20000, + id: 'id', + is_active: true, + local_balance: 10000, + partner_public_key: 'bob', + remote_balance: 10000, + }], + to: 'bob', + tokens: 500, + }, + description: 'A balanced channel is returned', + expected: {id: 'id'}, + }, + { + args: { + channels: [ + { + capacity: 20000, + id: 'id1', + is_active: true, + local_balance: 200, + local_reserve: 100, + partner_public_key: 'bob', + remote_balance: 19800, + }, + { + capacity: 20000, + id: 'id2', + is_active: true, + local_balance: 10000, + local_reserve: 100, + partner_public_key: 'bob', + remote_balance: 10000, + }, + ], + to: 'bob', + tokens: 500, + }, + description: 'A channel with enough remote balance is required', + expected: {id: 'id2'}, + }, +]; + +tests.forEach(({args, description, error, expected}) => { + return test(description, ({end, equal, throws}) => { + if (!!error) { + throws(() => channelForGift(args), new Error(error), 'Got error'); + } else { + const {id} = channelForGift(args); + + equal(id, expected.id, 'Channel id is returned as expected'); + } + + return end(); + }); +}); diff --git a/test/routing/test_gift_route.js b/test/routing/test_gift_route.js new file mode 100644 index 0000000..21cdd9f --- /dev/null +++ b/test/routing/test_gift_route.js @@ -0,0 +1,146 @@ +const {test} = require('tap'); + +const {giftRoute} = require('./../../routing'); + +const tests = [ + { + args: {}, + description: 'A channel is required', + error: 'ExpectedChannelToCalculateGiftRoute', + }, + { + args: {channel: {}}, + description: 'A destination is required', + error: 'ExpectedDestinationToCalculateGiftRoute', + }, + { + args: {channel: {}, destination: 'b'}, + description: 'The current chain tip height is required', + error: 'ExpectedHeightToCalculateGiftRoute', + }, + { + args: {channel: {}, destination: 'b', height: 1}, + description: 'Tokens to gift are required', + error: 'ExpectedTokensToCalculateGiftRoute', + }, + { + args: {channel: {}, destination: 'b', height: 1, tokens: 1}, + description: 'Channel policies array is required', + error: 'ExpectedChannelPoliciesToCalculateGiftRoute', + }, + { + args: {channel: {policies: [{}]}, destination: 'b', height: 1, tokens: 1}, + description: 'Channel policies need public keys', + error: 'ExpectedChannelPoliciesToCalculateGiftRoute', + }, + { + args: { + channel: {policies: [{public_key: 'c'}]}, + destination: 'b', + height: 1, + tokens: 1, + }, + description: 'Channel policies require a destination policy', + error: 'ExpectedDestinationPolicyToCalculateGiftRoute', + }, + { + args: { + channel: {policies: [{public_key: 'b'}]}, + destination: 'b', + height: 1, + tokens: 1, + }, + description: 'Channel policies require the peer policy', + error: 'ExpectedPeerPolicyToCalculateGiftRoute', + }, + { + args: { + channel: { + id: '1x1x1', + policies: [ + {cltv_delta: 1, public_key: 'b'}, + {cltv_delta: 1, min_htlc_mtokens: '100000', public_key: 'c'}, + ], + }, + destination: 'b', + height: 1, + tokens: 1, + }, + description: 'The peer min htlc policy has to be high enough to forward', + error: 'PeerPolicyTooLowToCompleteForward', + }, + { + args: { + channel: { + id: '1x1x1', + policies: [ + {cltv_delta: 1, public_key: 'b'}, + {base_fee_mtokens: '1001', cltv_delta: 1, public_key: 'c'}, + ], + }, + destination: 'b', + height: 1, + tokens: 1, + }, + description: 'The real fee must be lower than the gift fee', + error: 'GiftAmountTooLowToSend', + }, + { + args: { + channel: { + id: '1x1x1', + policies: [ + {cltv_delta: 1, public_key: 'b'}, + {cltv_delta: 1, public_key: 'c'}, + ], + }, + destination: 'b', + height: 1, + tokens: 1, + }, + description: 'Route is created', + expected: { + route: { + fee: 1, + fee_mtokens: '1000', + hops: [ + { + channel: '1x1x1', + channel_capacity: undefined, + fee: 1, + fee_mtokens: '1000', + forward: 1, + forward_mtokens: '1000', + public_key: 'c', + timeout: 41, + }, + { + channel: '1x1x1', + channel_capacity: undefined, + fee: 0, + fee_mtokens: '0', + forward: 1, + forward_mtokens: '1000', + public_key: 'b', + timeout: 41, + }, + ], + mtokens: '2000', + timeout: 42, + tokens: 2, + }, + }, + }, +]; + +tests.forEach(({args, description, error, expected}) => { + return test(description, async ({deepIs, end, throws}) => { + if (!!error) { + throws(() => giftRoute(args), new Error(error), 'Got expected error'); + } else { + deepIs(giftRoute(args).route, expected.route, 'Got expected route'); + } + + return end(); + }); +});