mirror of
https://github.com/alexbosworth/balanceofsatoshis.git
synced 2026-08-13 12:33:37 +02:00
update dependencies
This commit is contained in:
parent
004357e846
commit
9927706f2b
20 changed files with 882 additions and 789 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Versions
|
||||
|
||||
## Version 3.4.0
|
||||
## Version 3.4.1
|
||||
|
||||
- `peers`: add --sort to sort returned peers by an attribute
|
||||
|
||||
|
|
|
|||
3
arrays/index.js
Normal file
3
arrays/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const sortBy = require('./sort_by');
|
||||
|
||||
module.exports = {sortBy};
|
||||
44
arrays/sort_by.js
Normal file
44
arrays/sort_by.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const {isArray} = Array;
|
||||
|
||||
const equalTo = 0;
|
||||
const greaterThan = 1;
|
||||
const lessThan = -1;
|
||||
|
||||
/** Sort array by attribute, lowest to highest
|
||||
|
||||
{
|
||||
array: [<Array Element Object>]
|
||||
attribute: <Attribute String>
|
||||
}
|
||||
|
||||
@throws
|
||||
<Error>
|
||||
|
||||
@returns
|
||||
{
|
||||
sorted: [<Sorted Element Object>]
|
||||
}
|
||||
*/
|
||||
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};
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
};
|
||||
|
|
|
|||
802
package-lock.json
generated
802
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
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"
|
||||
}
|
||||
|
|
|
|||
77
routing/channel_for_gift.js
Normal file
77
routing/channel_for_gift.js
Normal file
|
|
@ -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: <Channel Capacity Tokens Number>
|
||||
is_active: <Channel is Active Bool>
|
||||
local_balance: <Local Balance Tokens Number>
|
||||
[local_reserve]: <Local Reserve Tokens Number>
|
||||
partner_public_key: <Peer Public Key Hex String>
|
||||
remote_balance: <Remote Balance Tokens Number>
|
||||
}]
|
||||
to: <Channel With Peer Public Key Hex String>
|
||||
tokens: <Tokens To Send Number>
|
||||
}
|
||||
|
||||
@throws
|
||||
<Error>
|
||||
|
||||
@returns
|
||||
{
|
||||
id: <Standard Format Channel Id String>
|
||||
}
|
||||
*/
|
||||
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};
|
||||
};
|
||||
121
routing/gift_route.js
Normal file
121
routing/gift_route.js
Normal file
|
|
@ -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: <Channel Id String>
|
||||
policies: [{
|
||||
base_fee_mtokens: <Base Fee Millitokens String>
|
||||
cltv_delta: <CLTV Delta Number>
|
||||
fee_rate: <Fee Rate Number>
|
||||
min_htlc_mtokens: <Minimum HTLC Tokens Number>
|
||||
public_key: <Forwarding Public Key Hex String>
|
||||
}]
|
||||
}]
|
||||
destination: <Destination Public Key Hex String>
|
||||
height: <Current Best Tip Block Height Number>
|
||||
tokens: <Tokens to Gift Number>
|
||||
}
|
||||
|
||||
@throws
|
||||
<Error>
|
||||
|
||||
@returns
|
||||
{
|
||||
route: {
|
||||
fee: <Total Fee Tokens To Pay Number>
|
||||
fee_mtokens: <Total Fee Millitokens To Pay String>
|
||||
hops: [{
|
||||
channel: <Standard Format Channel Id String>
|
||||
channel_capacity: <Channel Capacity Tokens Number>
|
||||
fee: <Fee Number>
|
||||
fee_mtokens: <Fee Millitokens String>
|
||||
forward: <Forward Tokens Number>
|
||||
forward_mtokens: <Forward Millitokens String>
|
||||
[public_key]: <Public Key Hex String>
|
||||
timeout: <Timeout Block Height Number>
|
||||
}]
|
||||
mtokens: <Total Fee-Inclusive Millitokens String>
|
||||
timeout: <Timeout Block Height Number>
|
||||
tokens: <Total Fee-Inclusive Tokens Number>
|
||||
}
|
||||
}
|
||||
*/
|
||||
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};
|
||||
};
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
45
test/arrays/test_sort_by.js
Normal file
45
test/arrays/test_sort_by.js
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
4
test/encryption/fixtures.json
Normal file
4
test/encryption/fixtures.json
Normal file
File diff suppressed because one or more lines are too long
53
test/encryption/test_decrypt_payload.js
Normal file
53
test/encryption/test_decrypt_payload.js
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
117
test/routing/test_channel_for_gift.js
Normal file
117
test/routing/test_channel_for_gift.js
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
146
test/routing/test_gift_route.js
Normal file
146
test/routing/test_gift_route.js
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue