balanceofsatoshis 24.2.2 → 25.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +8 -5
- package/bos +50 -26
- package/commands/interrogate.js +15 -1
- package/network/get_node_pending.js +329 -0
- package/network/get_pending.js +81 -0
- package/network/get_pending_paths.js +96 -0
- package/network/index.js +2 -0
- package/network/pay.js +95 -7
- package/network/pending_payments.js +105 -0
- package/network/pending_summary.js +209 -0
- package/network/probe.js +63 -8
- package/network/probe_destination.js +118 -19
- package/package.json +6 -7
- package/services/accept_balanced_channel.js +45 -44
- package/services/get_offer_introduction.js +107 -0
- package/services/get_public_peer.js +67 -0
- package/services/index.js +2 -1
- package/services/initiate_balanced_channel.js +70 -53
- package/services/recover_transit_funds.js +27 -20
- package/services/run_server.js +121 -0
- package/services/supporter_details.js +30 -0
- package/telegram/get_telegram_bot.js +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 25.1.0
|
|
4
|
+
|
|
5
|
+
- `pay`, `probe`: Add support for sending to BOLT 12 offers
|
|
6
|
+
- `server`: Add new command to run a BOLT 12 service to receive funds
|
|
7
|
+
|
|
8
|
+
## 25.0.0
|
|
9
|
+
|
|
10
|
+
- `pending`: Add command to show pending channels and in-flight HTLCs
|
|
11
|
+
|
|
12
|
+
### Breaking Changes
|
|
13
|
+
|
|
14
|
+
- Remove `price` command
|
|
15
|
+
|
|
3
16
|
## 24.2.2
|
|
4
17
|
|
|
5
18
|
- `open`: Adhere to BIP 69 when ordering funding outputs with equal amounts
|
package/README.md
CHANGED
|
@@ -141,17 +141,17 @@ bos open-balanced-channel
|
|
|
141
141
|
# Outputs the sum total of local channel liquidity
|
|
142
142
|
bos outbound-liquidity
|
|
143
143
|
|
|
144
|
-
# Pay a payment request (invoice), probing first
|
|
145
|
-
bos pay "payment_request"
|
|
144
|
+
# Pay a payment request (invoice) or offer, probing first
|
|
145
|
+
bos pay "payment_request/offer"
|
|
146
146
|
|
|
147
147
|
# Show channel-connected peers
|
|
148
148
|
bos peers
|
|
149
149
|
|
|
150
|
-
#
|
|
151
|
-
bos
|
|
150
|
+
# Show pending channels and in-flight HTLCs
|
|
151
|
+
bos pending
|
|
152
152
|
|
|
153
153
|
# Test if funds can be sent to a destination
|
|
154
|
-
bos probe "payment_request/public_key"
|
|
154
|
+
bos probe "payment_request/offer/public_key"
|
|
155
155
|
|
|
156
156
|
# Rebalance funds between peers
|
|
157
157
|
bos rebalance
|
|
@@ -165,6 +165,9 @@ bos report
|
|
|
165
165
|
# Send funds using keysend and an optional message to a node
|
|
166
166
|
bos send
|
|
167
167
|
|
|
168
|
+
# Run a server of paid services
|
|
169
|
+
bos server
|
|
170
|
+
|
|
168
171
|
# Connect up to a Telegram bot
|
|
169
172
|
bos telegram
|
|
170
173
|
|
package/bos
CHANGED
|
@@ -14,7 +14,6 @@ const {writeFile} = require('fs');
|
|
|
14
14
|
const importLazy = require('import-lazy')(require);
|
|
15
15
|
|
|
16
16
|
const accounting = importLazy('ln-accounting');
|
|
17
|
-
const fiat = importLazy('@alexbosworth/fiat');
|
|
18
17
|
const lnService = importLazy('ln-service');
|
|
19
18
|
const lnSync = importLazy('ln-sync');
|
|
20
19
|
const paidServices = importLazy('paid-services');
|
|
@@ -1514,13 +1513,14 @@ prog
|
|
|
1514
1513
|
})
|
|
1515
1514
|
|
|
1516
1515
|
// Pay a payment request, probing first
|
|
1517
|
-
.command('pay', 'Pay a payment request, probing first')
|
|
1516
|
+
.command('pay', 'Pay a payment request or offer, probing first')
|
|
1518
1517
|
.help('Blinded path requests are probed to the introduction node, 1 path')
|
|
1519
1518
|
.help('--out can take a tag to make the first hop through any tagged peer')
|
|
1520
1519
|
.help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
|
|
1521
1520
|
.help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
|
|
1522
1521
|
.help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
|
|
1523
|
-
.argument('<request>', 'Payment
|
|
1522
|
+
.argument('<request>', 'Payment request or offer')
|
|
1523
|
+
.argument('[amount]', 'Amount to pay an offer, default: offer amount')
|
|
1524
1524
|
.option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
|
|
1525
1525
|
.option('--avoid-high-fee-routes', 'Avoid evaluating too-high fee routes')
|
|
1526
1526
|
.option('--in <public_key>', 'Route through specific peer of destination')
|
|
@@ -1551,6 +1551,7 @@ prog
|
|
|
1551
1551
|
out: flatten([options.out].filter(n => !!n)),
|
|
1552
1552
|
out_filters: flatten([options.outFilter].filter(n => !!n)),
|
|
1553
1553
|
request: args.request,
|
|
1554
|
+
tokens: args.amount || undefined,
|
|
1554
1555
|
},
|
|
1555
1556
|
responses.returnObject({logger, reject, resolve}));
|
|
1556
1557
|
} catch (err) {
|
|
@@ -1613,41 +1614,41 @@ prog
|
|
|
1613
1614
|
});
|
|
1614
1615
|
})
|
|
1615
1616
|
|
|
1616
|
-
// Get
|
|
1617
|
-
.command('
|
|
1618
|
-
.help('
|
|
1619
|
-
.help('
|
|
1620
|
-
.
|
|
1621
|
-
.option('--
|
|
1622
|
-
.option('--from <provider>', 'Rate provider', priceProviders, 'coindesk')
|
|
1617
|
+
// Get pending channels and in-flight HTLCs
|
|
1618
|
+
.command('pending', 'Show pending channels and in-flight HTLCs')
|
|
1619
|
+
.help('Channels waiting to open or close, forwarding and probing HTLCs')
|
|
1620
|
+
.help('Multiple --node arguments are supported to show items across nodes')
|
|
1621
|
+
.help('Use --verbose to also show the raw details of the pending items')
|
|
1622
|
+
.option('--show-relays <count>', 'Relaying peers to show in paths', INT, 0)
|
|
1623
1623
|
.option('--no-color', 'Mute all colors')
|
|
1624
|
+
.option('--node <node_name>', 'Node to get pending items for', REPEATABLE)
|
|
1624
1625
|
.action((args, options, logger) => {
|
|
1625
|
-
return new Promise((resolve, reject) => {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
resolve
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1626
|
+
return new Promise(async (resolve, reject) => {
|
|
1627
|
+
try {
|
|
1628
|
+
return network.getPending({
|
|
1629
|
+
logger,
|
|
1630
|
+
fs: {getFile: readFile},
|
|
1631
|
+
is_monochrome: !!options.noColor,
|
|
1632
|
+
lnds: (await lnd.getLnds({logger, nodes: options.node})).lnds,
|
|
1633
|
+
max_relays: options.showRelays,
|
|
1634
|
+
},
|
|
1635
|
+
responses.returnOutput({logger, reject, resolve}));
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
return logger.error({err}) && reject();
|
|
1638
|
+
}
|
|
1638
1639
|
});
|
|
1639
1640
|
})
|
|
1640
1641
|
|
|
1641
1642
|
// Determine if a payment request is sendable
|
|
1642
|
-
.command('probe', 'Check if a payment request is sendable')
|
|
1643
|
+
.command('probe', 'Check if a payment request or offer is sendable')
|
|
1643
1644
|
.help('Simulate paying a payment request without actually paying it')
|
|
1644
1645
|
.help('Blinded path requests are probed to the introduction node, 1 path')
|
|
1645
1646
|
.help('--out can take a tag to make the first hop through any tagged peer')
|
|
1646
1647
|
.help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
|
|
1647
1648
|
.help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
|
|
1648
1649
|
.help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
|
|
1649
|
-
.argument('<to>', 'Payment request or node public key')
|
|
1650
|
-
.argument('[amount]', 'Amount to probe, default: request amount')
|
|
1650
|
+
.argument('<to>', 'Payment request, offer, or node public key')
|
|
1651
|
+
.argument('[amount]', 'Amount to probe, default: request or offer amount')
|
|
1651
1652
|
.option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
|
|
1652
1653
|
.option('--avoid-high-fee-routes', 'Avoid evaluating too-high fee routes')
|
|
1653
1654
|
.option('--find-max', 'Find the maximum routeable amount on success route')
|
|
@@ -1923,6 +1924,29 @@ prog
|
|
|
1923
1924
|
});
|
|
1924
1925
|
})
|
|
1925
1926
|
|
|
1927
|
+
// Run a server of paid services
|
|
1928
|
+
.command('server', 'Run a server of paid services')
|
|
1929
|
+
.help('Makes a new service, or resumes one from its service record')
|
|
1930
|
+
.help('Requires LND with support for onion messages')
|
|
1931
|
+
.argument('[service_record]', 'Service record to resume a service', hexMatch)
|
|
1932
|
+
.option('--node <node_name>', 'Saved node to run the server on')
|
|
1933
|
+
.option('--qr', 'Encode the offer into a terminal output QR code')
|
|
1934
|
+
.action((args, options, logger) => {
|
|
1935
|
+
return new Promise(async (resolve, reject) => {
|
|
1936
|
+
try {
|
|
1937
|
+
return services.runServer({
|
|
1938
|
+
logger,
|
|
1939
|
+
is_including_qr: !!options.qr || undefined,
|
|
1940
|
+
lnd: (await lndForNode(logger, options.node)).lnd,
|
|
1941
|
+
record: args.serviceRecord,
|
|
1942
|
+
},
|
|
1943
|
+
responses.returnObject({logger, reject, resolve}));
|
|
1944
|
+
} catch (err) {
|
|
1945
|
+
return logger.error({err}) && reject();
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
})
|
|
1949
|
+
|
|
1926
1950
|
// Get a swap API key or inspect an existing one
|
|
1927
1951
|
.command('swap-api-key', 'Purchase a swap API key or inspect a swap API key')
|
|
1928
1952
|
.visible(false)
|
package/commands/interrogate.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const exitCodeSigint = 130;
|
|
2
|
+
const isForceClose = err => !!err && err.name === 'ExitPromptError';
|
|
3
|
+
|
|
1
4
|
/** Get ask function
|
|
2
5
|
|
|
3
6
|
{}
|
|
@@ -9,6 +12,17 @@ module.exports = ({}) => {
|
|
|
9
12
|
return new Promise(async resolve => {
|
|
10
13
|
const inquirer = (await import('inquirer')).default;
|
|
11
14
|
|
|
12
|
-
return resolve((n, cbk) =>
|
|
15
|
+
return resolve((n, cbk) => {
|
|
16
|
+
return inquirer.prompt([n])
|
|
17
|
+
.then(res => cbk(res))
|
|
18
|
+
.catch(err => {
|
|
19
|
+
// Exit quietly when the user presses ctrl+c
|
|
20
|
+
if (isForceClose(err)) {
|
|
21
|
+
return process.exit(exitCodeSigint);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw err;
|
|
25
|
+
});
|
|
26
|
+
});
|
|
13
27
|
});
|
|
14
28
|
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncMap = require('async/map');
|
|
3
|
+
const {getChannels} = require('ln-service');
|
|
4
|
+
const {getNodeAlias} = require('ln-sync');
|
|
5
|
+
const {getPendingChannels} = require('ln-service');
|
|
6
|
+
const {getWalletInfo} = require('ln-service');
|
|
7
|
+
const {returnResult} = require('asyncjs-util');
|
|
8
|
+
|
|
9
|
+
const getPendingPaths = require('./get_pending_paths');
|
|
10
|
+
const pendingPayments = require('./pending_payments');
|
|
11
|
+
const {sortBy} = require('./../arrays');
|
|
12
|
+
|
|
13
|
+
const flatten = arr => [].concat(...arr);
|
|
14
|
+
const lookupTimeoutMs = 1000 * 10;
|
|
15
|
+
const uniq = arr => Array.from(new Set(arr));
|
|
16
|
+
|
|
17
|
+
/** Get pending channels and in-flight HTLCs for a node
|
|
18
|
+
|
|
19
|
+
Items are sorted oldest first, by their creation time when it is known and
|
|
20
|
+
otherwise by the blocks remaining until they resolve
|
|
21
|
+
|
|
22
|
+
Debug details of pending channels, HTLCs and payments are logged in verbose
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
lnd: <Authenticated LND API Object>
|
|
26
|
+
logger: <Winston Logger Object>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@returns via cbk or Promise
|
|
30
|
+
{
|
|
31
|
+
closing: [{
|
|
32
|
+
[blocks_until_recovery]: <Blocks Until Pending Balance Recovers Number>
|
|
33
|
+
[close_transaction_id]: <Closing Transaction Id Hex String>
|
|
34
|
+
[peer_alias]: <Peer Alias String>
|
|
35
|
+
peer_public_key: <Peer Public Key Hex String>
|
|
36
|
+
[pending_balance]: <Pending Recovery Tokens Number>
|
|
37
|
+
}]
|
|
38
|
+
forwarding: [{
|
|
39
|
+
fee: <Routing Fee Tokens Number>
|
|
40
|
+
[in_peer_alias]: <Inbound Peer Alias String>
|
|
41
|
+
in_peer_public_key: <Inbound Peer Public Key Hex String>
|
|
42
|
+
[out_peer_alias]: <Outbound Peer Alias String>
|
|
43
|
+
out_peer_public_key: <Outbound Peer Public Key Hex String>
|
|
44
|
+
tokens: <Forwarding Tokens Number>
|
|
45
|
+
}]
|
|
46
|
+
opening: [{
|
|
47
|
+
capacity: <Channel Capacity Tokens Number>
|
|
48
|
+
is_partner_initiated: <Peer Is Opening The Channel Bool>
|
|
49
|
+
[peer_alias]: <Peer Alias String>
|
|
50
|
+
peer_public_key: <Peer Public Key Hex String>
|
|
51
|
+
transaction_id: <Funding Transaction Id Hex String>
|
|
52
|
+
}]
|
|
53
|
+
sending: [{
|
|
54
|
+
[fee]: <Routing Fee Tokens Number>
|
|
55
|
+
hops: [{
|
|
56
|
+
[alias]: <Hop Node Alias String>
|
|
57
|
+
public_key: <Hop Node Public Key Hex String>
|
|
58
|
+
}]
|
|
59
|
+
is_rebalance: <Payment Is Going Back To The Node Itself Bool>
|
|
60
|
+
tokens: <Sending Tokens Number>
|
|
61
|
+
}]
|
|
62
|
+
[alias]: <Node Alias String>
|
|
63
|
+
public_key: <Node Identity Public Key Hex String>
|
|
64
|
+
}
|
|
65
|
+
*/
|
|
66
|
+
module.exports = ({lnd, logger}, cbk) => {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
return asyncAuto({
|
|
69
|
+
// Check arguments
|
|
70
|
+
validate: cbk => {
|
|
71
|
+
if (!lnd) {
|
|
72
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetNodePendingItems']);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!logger) {
|
|
76
|
+
return cbk([400, 'ExpectedLoggerToGetNodePendingItems']);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return cbk();
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Get channels to look for in-flight HTLCs
|
|
83
|
+
getChannels: ['validate', ({}, cbk) => getChannels({lnd}, cbk)],
|
|
84
|
+
|
|
85
|
+
// Get the node identity
|
|
86
|
+
getInfo: ['validate', ({}, cbk) => getWalletInfo({lnd}, cbk)],
|
|
87
|
+
|
|
88
|
+
// Get pending channels
|
|
89
|
+
getPending: ['validate', ({}, cbk) => getPendingChannels({lnd}, cbk)],
|
|
90
|
+
|
|
91
|
+
// Derive the HTLCs that are forwarding or sending
|
|
92
|
+
htlcs: ['getChannels', ({getChannels}, cbk) => {
|
|
93
|
+
return cbk(null, pendingPayments({channels: getChannels.channels}));
|
|
94
|
+
}],
|
|
95
|
+
|
|
96
|
+
// Look up the in-flight payments behind the outgoing HTLCs
|
|
97
|
+
getPayments: ['htlcs', ({htlcs}, cbk) => {
|
|
98
|
+
return asyncMap(uniq(htlcs.sending.map(n => n.id)), (id, cbk) => {
|
|
99
|
+
return getPendingPaths({id, lnd, timeout_ms: lookupTimeoutMs}, cbk);
|
|
100
|
+
},
|
|
101
|
+
cbk);
|
|
102
|
+
}],
|
|
103
|
+
|
|
104
|
+
// Log the raw details behind the pending items for debugging
|
|
105
|
+
debug: [
|
|
106
|
+
'getPayments',
|
|
107
|
+
'getPending',
|
|
108
|
+
'htlcs',
|
|
109
|
+
({getPayments, getPending, htlcs}, cbk) =>
|
|
110
|
+
{
|
|
111
|
+
const hasHtlcs = !!htlcs.forwarding.length || !!htlcs.sending.length;
|
|
112
|
+
|
|
113
|
+
// Exit early when there is nothing pending to debug
|
|
114
|
+
if (!hasHtlcs && !getPending.pending_channels.length) {
|
|
115
|
+
return cbk();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
logger.debug({
|
|
119
|
+
forwarding_htlcs: htlcs.forwarding,
|
|
120
|
+
outgoing_htlcs: htlcs.sending,
|
|
121
|
+
payments: getPayments.map(payment => ({
|
|
122
|
+
id: payment.id,
|
|
123
|
+
created_at: payment.created_at,
|
|
124
|
+
paths: payment.paths.map(path => ({
|
|
125
|
+
hops: path.hops.map(hop => `${hop.channel} ${hop.public_key}`),
|
|
126
|
+
timeout: path.timeout,
|
|
127
|
+
tokens: path.tokens,
|
|
128
|
+
})),
|
|
129
|
+
})),
|
|
130
|
+
pending_channels: getPending.pending_channels.map(channel => ({
|
|
131
|
+
blocks_until_expiry: channel.blocks_until_expiry,
|
|
132
|
+
capacity: channel.capacity,
|
|
133
|
+
close_transaction_id: channel.close_transaction_id,
|
|
134
|
+
is_closing: channel.is_closing,
|
|
135
|
+
is_opening: channel.is_opening,
|
|
136
|
+
is_partner_initiated: channel.is_partner_initiated,
|
|
137
|
+
is_timelocked: channel.is_timelocked,
|
|
138
|
+
local_balance: channel.local_balance,
|
|
139
|
+
partner_public_key: channel.partner_public_key,
|
|
140
|
+
pending_balance: channel.pending_balance,
|
|
141
|
+
pending_payments: channel.pending_payments,
|
|
142
|
+
remote_balance: channel.remote_balance,
|
|
143
|
+
timelock_blocks: channel.timelock_blocks,
|
|
144
|
+
timelock_expiration: channel.timelock_expiration,
|
|
145
|
+
transaction_id: channel.transaction_id,
|
|
146
|
+
transaction_vout: channel.transaction_vout,
|
|
147
|
+
})),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return cbk();
|
|
151
|
+
}],
|
|
152
|
+
|
|
153
|
+
// Outgoing HTLCs are matched to the paths of their in-flight payments
|
|
154
|
+
routes: ['getPayments', 'htlcs', ({getPayments, htlcs}, cbk) => {
|
|
155
|
+
const paths = flatten(getPayments.map(payment => {
|
|
156
|
+
return payment.paths.map(path => ({
|
|
157
|
+
path,
|
|
158
|
+
created_at: payment.created_at,
|
|
159
|
+
id: payment.id,
|
|
160
|
+
}));
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
// Every path can only be the route of a single outgoing HTLC
|
|
164
|
+
const {routes} = htlcs.sending.reduce((sum, htlc) => {
|
|
165
|
+
// An HTLC has the hash, first hop, amount and expiry of its route
|
|
166
|
+
const match = sum.unmatched.find(({id, path}) => {
|
|
167
|
+
const [firstHop] = path.hops;
|
|
168
|
+
|
|
169
|
+
const isSameAmount = path.tokens === htlc.tokens;
|
|
170
|
+
const isSameExpiry = path.timeout === htlc.timeout;
|
|
171
|
+
const isSameHash = id === htlc.id;
|
|
172
|
+
const isSameOutPeer = firstHop.public_key === htlc.out_peer;
|
|
173
|
+
|
|
174
|
+
return isSameHash && isSameAmount && isSameExpiry && isSameOutPeer;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Exit early when there are no payment details for the HTLC
|
|
178
|
+
if (!match) {
|
|
179
|
+
const hop = {forward: htlc.tokens, public_key: htlc.out_peer};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
routes: sum.routes.concat({hops: [hop]}),
|
|
183
|
+
unmatched: sum.unmatched,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const route = {
|
|
188
|
+
created_at: match.created_at,
|
|
189
|
+
fee: match.path.fee,
|
|
190
|
+
hops: match.path.hops,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
routes: sum.routes.concat(route),
|
|
195
|
+
unmatched: sum.unmatched.filter(n => n !== match),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
{routes: [], unmatched: paths});
|
|
199
|
+
|
|
200
|
+
return cbk(null, routes);
|
|
201
|
+
}],
|
|
202
|
+
|
|
203
|
+
// Get the aliases of the peers involved in pending items
|
|
204
|
+
getAliases: [
|
|
205
|
+
'getPending',
|
|
206
|
+
'htlcs',
|
|
207
|
+
'routes',
|
|
208
|
+
({getPending, htlcs, routes}, cbk) =>
|
|
209
|
+
{
|
|
210
|
+
const hops = flatten(routes.map(n => n.hops));
|
|
211
|
+
|
|
212
|
+
const peers = []
|
|
213
|
+
.concat(getPending.pending_channels.map(n => n.partner_public_key))
|
|
214
|
+
.concat(htlcs.forwarding.map(n => n.in_peer))
|
|
215
|
+
.concat(htlcs.forwarding.map(n => n.out_peer))
|
|
216
|
+
.concat(hops.map(n => n.public_key));
|
|
217
|
+
|
|
218
|
+
return asyncMap(uniq(peers), (id, cbk) => {
|
|
219
|
+
return getNodeAlias({id, lnd}, cbk);
|
|
220
|
+
},
|
|
221
|
+
cbk);
|
|
222
|
+
}],
|
|
223
|
+
|
|
224
|
+
// Assemble the pending items
|
|
225
|
+
pending: [
|
|
226
|
+
'getAliases',
|
|
227
|
+
'getInfo',
|
|
228
|
+
'getPending',
|
|
229
|
+
'htlcs',
|
|
230
|
+
'routes',
|
|
231
|
+
({getAliases, getInfo, getPending, htlcs, routes}, cbk) =>
|
|
232
|
+
{
|
|
233
|
+
const alias = id => {
|
|
234
|
+
return getAliases.find(n => n.id === id).alias || undefined;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const pending = getPending.pending_channels;
|
|
238
|
+
|
|
239
|
+
// Closes waiting on a timelock are ordered by their remaining blocks
|
|
240
|
+
const timelocked = sortBy({
|
|
241
|
+
array: pending.filter(n => !!n.is_closing && !!n.is_timelocked),
|
|
242
|
+
attribute: 'timelock_blocks',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Closes that are not timelocked yet are the newest closes
|
|
246
|
+
const waiting = pending.filter(n => n.is_closing && !n.is_timelocked);
|
|
247
|
+
|
|
248
|
+
const closes = [].concat(timelocked.sorted).concat(waiting);
|
|
249
|
+
|
|
250
|
+
// Channels that are closing, which may be waiting on funds recovery
|
|
251
|
+
const closing = closes.map(channel => {
|
|
252
|
+
// A timelock may hold the funds for some blocks before recovery
|
|
253
|
+
const blocks = channel.timelock_blocks || Number();
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
blocks_until_recovery: blocks > Number() ? blocks : undefined,
|
|
257
|
+
close_transaction_id: channel.close_transaction_id,
|
|
258
|
+
peer_alias: alias(channel.partner_public_key),
|
|
259
|
+
peer_public_key: channel.partner_public_key,
|
|
260
|
+
pending_balance: channel.pending_balance || undefined,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// HTLCs that are being relayed from one peer to another
|
|
265
|
+
const forwards = sortBy({
|
|
266
|
+
array: htlcs.forwarding,
|
|
267
|
+
attribute: 'timeout',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const forwarding = forwards.sorted.map(forward => ({
|
|
271
|
+
fee: forward.fee,
|
|
272
|
+
in_peer_alias: alias(forward.in_peer),
|
|
273
|
+
in_peer_public_key: forward.in_peer,
|
|
274
|
+
out_peer_alias: alias(forward.out_peer),
|
|
275
|
+
out_peer_public_key: forward.out_peer,
|
|
276
|
+
tokens: forward.tokens,
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
// Channels that are waiting for funding confirmation
|
|
280
|
+
const opens = sortBy({
|
|
281
|
+
array: pending.filter(n => !!n.is_opening),
|
|
282
|
+
attribute: 'blocks_until_expiry',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const opening = opens.sorted.map(channel => ({
|
|
286
|
+
capacity: channel.capacity,
|
|
287
|
+
is_partner_initiated: channel.is_partner_initiated,
|
|
288
|
+
peer_alias: alias(channel.partner_public_key),
|
|
289
|
+
peer_public_key: channel.partner_public_key,
|
|
290
|
+
transaction_id: channel.transaction_id,
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
// Payments with known creation dates are ordered by their dates
|
|
294
|
+
const dated = sortBy({
|
|
295
|
+
array: routes.filter(n => !!n.created_at),
|
|
296
|
+
attribute: 'created_at',
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// HTLCs that have no payment details are the newest payments
|
|
300
|
+
const undated = routes.filter(n => !n.created_at);
|
|
301
|
+
|
|
302
|
+
// Payments or probes that are going out through peers
|
|
303
|
+
const sending = [].concat(dated.sorted).concat(undated).map(route => {
|
|
304
|
+
const [finalHop] = route.hops.slice().reverse();
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
fee: route.fee,
|
|
308
|
+
hops: route.hops.map(hop => ({
|
|
309
|
+
alias: alias(hop.public_key),
|
|
310
|
+
public_key: hop.public_key,
|
|
311
|
+
})),
|
|
312
|
+
is_rebalance: finalHop.public_key === getInfo.public_key,
|
|
313
|
+
tokens: finalHop.forward,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return cbk(null, {
|
|
318
|
+
closing,
|
|
319
|
+
forwarding,
|
|
320
|
+
opening,
|
|
321
|
+
sending,
|
|
322
|
+
alias: getInfo.alias || undefined,
|
|
323
|
+
public_key: getInfo.public_key,
|
|
324
|
+
});
|
|
325
|
+
}],
|
|
326
|
+
},
|
|
327
|
+
returnResult({reject, resolve, of: 'pending'}, cbk));
|
|
328
|
+
});
|
|
329
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncMap = require('async/map');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const {getIcons} = require('./../display');
|
|
6
|
+
const getNodePending = require('./get_node_pending');
|
|
7
|
+
const pendingSummary = require('./pending_summary');
|
|
8
|
+
|
|
9
|
+
const {isArray} = Array;
|
|
10
|
+
|
|
11
|
+
/** Get a summary of pending channels and in-flight HTLCs
|
|
12
|
+
|
|
13
|
+
When there are multiple nodes, pending items are grouped by node
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
fs: {
|
|
17
|
+
getFile: <Read File Contents Function> (path, cbk) => {}
|
|
18
|
+
}
|
|
19
|
+
[is_monochrome]: <Mute Colors Bool>
|
|
20
|
+
lnds: [<Authenticated LND API Object>]
|
|
21
|
+
logger: <Winston Logger Object>
|
|
22
|
+
[max_relays]: <Maximum Relaying Peers of a Path to Show Number>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@returns via cbk or Promise
|
|
26
|
+
<Pending Items Summary String>
|
|
27
|
+
*/
|
|
28
|
+
module.exports = (args, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!args.fs) {
|
|
34
|
+
return cbk([400, 'ExpectedFileSystemMethodsToGetPendingItems']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isArray(args.lnds)) {
|
|
38
|
+
return cbk([400, 'ExpectedArrayOfLndsToGetPendingItems']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!args.lnds.length) {
|
|
42
|
+
return cbk([400, 'ExpectedAnLndToGetPendingItems']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!args.logger) {
|
|
46
|
+
return cbk([400, 'ExpectedLoggerToGetPendingItems']);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (args.max_relays !== undefined && args.max_relays < 0) {
|
|
50
|
+
return cbk([400, 'ExpectedNonNegativeMaxRelaysCountToShow']);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return cbk();
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Get the tag icons for nodes
|
|
57
|
+
getIcons: ['validate', ({}, cbk) => getIcons({fs: args.fs}, cbk)],
|
|
58
|
+
|
|
59
|
+
// Get the pending items for every node
|
|
60
|
+
getNodes: ['validate', ({}, cbk) => {
|
|
61
|
+
return asyncMap(args.lnds, (lnd, cbk) => {
|
|
62
|
+
return getNodePending({lnd, logger: args.logger}, cbk);
|
|
63
|
+
},
|
|
64
|
+
cbk);
|
|
65
|
+
}],
|
|
66
|
+
|
|
67
|
+
// Summarize the pending items
|
|
68
|
+
summary: ['getIcons', 'getNodes', ({getIcons, getNodes}, cbk) => {
|
|
69
|
+
const {summary} = pendingSummary({
|
|
70
|
+
is_monochrome: args.is_monochrome,
|
|
71
|
+
max_relays: args.max_relays,
|
|
72
|
+
nodes: getNodes,
|
|
73
|
+
tagged: getIcons.nodes,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return cbk(null, summary);
|
|
77
|
+
}],
|
|
78
|
+
},
|
|
79
|
+
returnResult({reject, resolve, of: 'summary'}, cbk));
|
|
80
|
+
});
|
|
81
|
+
};
|