balanceofsatoshis 25.2.2 → 25.3.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 +8 -1
- package/balances/get_liquidity.js +1 -13
- package/bos +1 -4
- package/chain/anchor_outcome.js +23 -0
- package/chain/anchor_output.js +86 -0
- package/chain/anchor_output_script.js +43 -0
- package/chain/close_fee_payer.js +48 -0
- package/chain/close_resolutions.js +114 -0
- package/chain/closing_fees.js +125 -18
- package/chain/contract_outputs.js +30 -0
- package/chain/funded_outputs.js +58 -0
- package/chain/funding_keys.js +59 -0
- package/chain/get_block_transactions.js +100 -0
- package/chain/get_channel_closes.js +151 -52
- package/chain/get_close_from_spend.js +125 -0
- package/chain/get_close_transactions.js +116 -0
- package/chain/get_closes_in_blocks.js +81 -0
- package/chain/get_spending_transaction.js +90 -0
- package/chain/input_weight.js +43 -0
- package/chain/is_contract_spend.js +26 -0
- package/chain/link_payment_stages.js +119 -0
- package/chain/payment_outcome.js +28 -0
- package/chain/resolution_fee.js +86 -0
- package/chain/split_fee.js +40 -0
- package/chain/split_utxos.js +167 -54
- package/chain/transaction_fee.js +56 -0
- package/chain/utxo_split_plan.js +142 -0
- package/package.json +2 -3
- package/chain/get_channel_resolution.js +0 -249
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const {scriptAsScriptElements} = require('@alexbosworth/blockchain');
|
|
2
|
+
|
|
3
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
4
|
+
const fundingScriptElementsCount = 5;
|
|
5
|
+
const {isBuffer} = Buffer;
|
|
6
|
+
const OP_2 = 82;
|
|
7
|
+
const OP_CHECKMULTISIG = 174;
|
|
8
|
+
const publicKeyByteLength = 33;
|
|
9
|
+
|
|
10
|
+
/** Derive the funding public keys from the witness of the funding output spend
|
|
11
|
+
|
|
12
|
+
A close transaction spends the funding output with a 2 of 2 multisig witness
|
|
13
|
+
script that commits to both funding public keys.
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
[witness]: [<Witness Stack Element Hex String>]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@returns
|
|
20
|
+
{
|
|
21
|
+
[keys]: [<Funding Public Key Hex String>]
|
|
22
|
+
}
|
|
23
|
+
*/
|
|
24
|
+
module.exports = ({witness}) => {
|
|
25
|
+
// Exit early when there is no witness script to look at
|
|
26
|
+
if (!witness || !witness.length) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const [script] = [].concat(witness).reverse();
|
|
31
|
+
|
|
32
|
+
// Witness elements are always hex, a malformed script has no elements
|
|
33
|
+
const {elements} = scriptAsScriptElements({script});
|
|
34
|
+
|
|
35
|
+
// Exit early when the script is not shaped like a 2 of 2 multisig
|
|
36
|
+
if (!elements || elements.length !== fundingScriptElementsCount) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [op2, key1, key2, op2Again, opCheckMultisig] = elements;
|
|
41
|
+
|
|
42
|
+
// Exit early when the script is not a 2 of 2 multisig
|
|
43
|
+
if (op2 !== OP_2 || op2Again !== OP_2) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (opCheckMultisig !== OP_CHECKMULTISIG) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const keys = [key1, key2];
|
|
52
|
+
|
|
53
|
+
// Exit early when the keys are not public keys
|
|
54
|
+
if (!!keys.find(n => !isBuffer(n) || n.length !== publicKeyByteLength)) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {keys: keys.map(bufferAsHex)};
|
|
59
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {getBlock} = require('ln-service');
|
|
3
|
+
const {queryTransactions} = require('@alexbosworth/blockchain');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
const defaultTimeoutMs = 1000 * 10;
|
|
7
|
+
const {isArray} = Array;
|
|
8
|
+
const isHash = n => /^[0-9A-F]{64}$/i.test(n);
|
|
9
|
+
|
|
10
|
+
/** Get transactions in a block by their ids using the LND chain backend
|
|
11
|
+
|
|
12
|
+
When the block is not retrieved before the timeout, no txs are returned
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
height: <Block Height Number>
|
|
16
|
+
ids: [<Transaction Id Hex String>]
|
|
17
|
+
lnd: <Authenticated LND API Object>
|
|
18
|
+
[timeout]: <Stop Waiting For The Block After Milliseconds Number>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@returns via cbk or Promise
|
|
22
|
+
{
|
|
23
|
+
transactions: [{
|
|
24
|
+
id: <Transaction Id Hex String>
|
|
25
|
+
transaction: <Raw Transaction Hex String>
|
|
26
|
+
}]
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = (args, cbk) => {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
return asyncAuto({
|
|
32
|
+
// Check arguments
|
|
33
|
+
validate: cbk => {
|
|
34
|
+
if (!args.height) {
|
|
35
|
+
return cbk([400, 'ExpectedBlockHeightToGetBlockTransactions']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!isArray(args.ids) || !args.ids.length) {
|
|
39
|
+
return cbk([400, 'ExpectedTransactionIdsToGetBlockTransactions']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!!args.ids.find(n => !isHash(n))) {
|
|
43
|
+
return cbk([400, 'ExpectedTransactionIdHexStringsToGetBlockTxs']);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!args.lnd) {
|
|
47
|
+
return cbk([400, 'ExpectedLndToGetBlockTransactions']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return cbk();
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Get the block from LND, giving up when it takes too long
|
|
54
|
+
getBlock: ['validate', ({}, cbk) => {
|
|
55
|
+
let isFinished = false;
|
|
56
|
+
|
|
57
|
+
const finished = (err, res) => {
|
|
58
|
+
// Exit early when the result was already returned
|
|
59
|
+
if (isFinished) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
isFinished = true;
|
|
64
|
+
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
|
|
67
|
+
return cbk(err, res);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Stop waiting when the block is not retrieved in time
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
return finished(null, {});
|
|
73
|
+
},
|
|
74
|
+
args.timeout || defaultTimeoutMs);
|
|
75
|
+
|
|
76
|
+
return getBlock({height: args.height, lnd: args.lnd}, finished);
|
|
77
|
+
}],
|
|
78
|
+
|
|
79
|
+
// Find the transactions in the block
|
|
80
|
+
transactions: ['getBlock', ({getBlock}, cbk) => {
|
|
81
|
+
// Exit early when there is no block to look through
|
|
82
|
+
if (!getBlock.block) {
|
|
83
|
+
return cbk(null, {transactions: []});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const {transactions} = queryTransactions({
|
|
88
|
+
block: getBlock.block,
|
|
89
|
+
ids: args.ids,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return cbk(null, {transactions});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return cbk([503, 'UnexpectedErrorParsingBlockTransactions', {err}]);
|
|
95
|
+
}
|
|
96
|
+
}],
|
|
97
|
+
},
|
|
98
|
+
returnResult({reject, resolve, of: 'transactions'}, cbk));
|
|
99
|
+
});
|
|
100
|
+
};
|
|
@@ -3,47 +3,54 @@ const asyncMapSeries = require('async/mapSeries');
|
|
|
3
3
|
const {getChainTransactions} = require('ln-service');
|
|
4
4
|
const {getClosedChannels} = require('ln-service');
|
|
5
5
|
const {getHeight} = require('ln-service');
|
|
6
|
-
const {getNetwork} = require('ln-sync');
|
|
7
|
-
const {getNode} = require('ln-service');
|
|
8
6
|
const {getNodeAlias} = require('ln-sync');
|
|
7
|
+
const {getPendingChannels} = require('ln-service');
|
|
9
8
|
const {returnResult} = require('asyncjs-util');
|
|
10
9
|
|
|
10
|
+
const anchorOutput = require('./anchor_output');
|
|
11
|
+
const closeResolutions = require('./close_resolutions');
|
|
11
12
|
const closingFees = require('./closing_fees');
|
|
12
|
-
const
|
|
13
|
+
const contractOutputs = require('./contract_outputs');
|
|
14
|
+
const getCloseTransactions = require('./get_close_transactions');
|
|
13
15
|
|
|
14
16
|
const defaultLimit = 20;
|
|
17
|
+
const flatten = arr => [].concat(...arr);
|
|
15
18
|
|
|
16
19
|
/** Get the last channel close outcomes
|
|
17
20
|
|
|
18
21
|
{
|
|
19
22
|
[limit]: <Limit Number>
|
|
20
23
|
lnd: <Authenticated LND API Object>
|
|
21
|
-
request: <Request Function>
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
@returns via cbk or Promise
|
|
25
27
|
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
closes: [{
|
|
29
|
+
peer_public_key: <Peer Public Key Hex String>
|
|
30
|
+
[peer_alias]: <Peer Alias Strring>
|
|
31
|
+
[is_local_force_close]: <Channel Was Locally Force Closed Bool>
|
|
32
|
+
[is_cooperative_close]: <Channel Was Cooperatively Closed Bool>
|
|
33
|
+
[is_remote_force_close]: <Channel was Remotely Force Closed Bool>
|
|
34
|
+
[peer_opened_channel]: <Peer Opened the Channel Bool>
|
|
35
|
+
[peer_closed_channel]: <Peer Closed the Channel Bool>
|
|
36
|
+
blocks_since_close: <Count of Blocks Since Close Number>
|
|
37
|
+
capacity: <Channel Capacity Tokens Number>
|
|
38
|
+
[channel_id]: <Channel Id String>
|
|
39
|
+
channel_open: <Channel Funding Outpoint String>
|
|
40
|
+
channel_close: <Channel Close Transaction Id Hex String>
|
|
41
|
+
[channel_balance_spend]: <Channel Balance Spent In Tx Id Hex String>
|
|
42
|
+
[channel_resolutions]: [{
|
|
43
|
+
[transaction_id]: <Latest Resolving Transaction Id Hex String>
|
|
44
|
+
type: <Resolution Type String>
|
|
45
|
+
[value]: <Value Number>
|
|
46
|
+
}]
|
|
47
|
+
[is_breach_close]: <Channel Was Breach Closed Bool>
|
|
48
|
+
[closing_fee_paid]: <Closing Fees Paid Related To Channel Tokens Number>
|
|
49
|
+
[is_closing_fee_unknown]: <Closing Fees Paid Are Not Known Bool>
|
|
41
50
|
}]
|
|
42
|
-
[is_breach_close]: <Channel Was Breach Closed Bool>
|
|
43
|
-
[closing_fee_paid]: <Closing Fees Paid Related To Channel Tokens Number>
|
|
44
51
|
}
|
|
45
52
|
*/
|
|
46
|
-
module.exports = ({limit, lnd
|
|
53
|
+
module.exports = ({limit, lnd}, cbk) => {
|
|
47
54
|
return new Promise((resolve, reject) => {
|
|
48
55
|
return asyncAuto({
|
|
49
56
|
// Check arguments
|
|
@@ -52,10 +59,6 @@ module.exports = ({limit, lnd, request}, cbk) => {
|
|
|
52
59
|
return cbk([400, 'ExpectedLndToGetChannelCloses']);
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
if (!request) {
|
|
56
|
-
return cbk([400, 'ExpectedRequestFunctionToGetChannelCloses']);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
62
|
return cbk();
|
|
60
63
|
},
|
|
61
64
|
|
|
@@ -65,20 +68,14 @@ module.exports = ({limit, lnd, request}, cbk) => {
|
|
|
65
68
|
// Get the current height
|
|
66
69
|
getHeight: ['validate', ({}, cbk) => getHeight({lnd}, cbk)],
|
|
67
70
|
|
|
68
|
-
// Get
|
|
69
|
-
|
|
71
|
+
// Get pending channels, their outputs may share resolving transactions
|
|
72
|
+
getPending: ['validate', ({}, cbk) => getPendingChannels({lnd}, cbk)],
|
|
70
73
|
|
|
71
74
|
// Get on-chain transactions
|
|
72
75
|
getTx: ['validate', ({}, cbk) => getChainTransactions({lnd}, cbk)],
|
|
73
76
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
'getClosed',
|
|
77
|
-
'getHeight',
|
|
78
|
-
'getNetwork',
|
|
79
|
-
'getTx',
|
|
80
|
-
async ({getClosed, getHeight, getNetwork, getTx}) =>
|
|
81
|
-
{
|
|
77
|
+
// Select the most recent closed channels
|
|
78
|
+
getChannels: ['getClosed', ({getClosed}, cbk) => {
|
|
82
79
|
const closedChannels = getClosed.channels
|
|
83
80
|
.reverse()
|
|
84
81
|
.filter(channel => !!channel.close_transaction_id)
|
|
@@ -86,30 +83,133 @@ module.exports = ({limit, lnd, request}, cbk) => {
|
|
|
86
83
|
|
|
87
84
|
const channels = closedChannels.slice(Number(), limit || defaultLimit);
|
|
88
85
|
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
return cbk(null, channels);
|
|
87
|
+
}],
|
|
88
|
+
|
|
89
|
+
// Get the aliases of the closed channel peers
|
|
90
|
+
getAliases: ['getChannels', ({getChannels}, cbk) => {
|
|
91
|
+
return asyncMapSeries(getChannels, (channel, cbk) => {
|
|
92
|
+
return getNodeAlias({lnd, id: channel.partner_public_key}, cbk);
|
|
93
|
+
},
|
|
94
|
+
cbk);
|
|
95
|
+
}],
|
|
96
|
+
|
|
97
|
+
// Get close transactions that are not in the wallet from LND
|
|
98
|
+
getCloseTxs: ['getChannels', 'getTx', ({getChannels, getTx}, cbk) => {
|
|
99
|
+
return getCloseTransactions({
|
|
100
|
+
lnd,
|
|
101
|
+
channels: getChannels,
|
|
102
|
+
transactions: getTx.transactions,
|
|
103
|
+
},
|
|
104
|
+
cbk);
|
|
105
|
+
}],
|
|
106
|
+
|
|
107
|
+
// Channel closes
|
|
108
|
+
closes: [
|
|
109
|
+
'getAliases',
|
|
110
|
+
'getChannels',
|
|
111
|
+
'getClosed',
|
|
112
|
+
'getCloseTxs',
|
|
113
|
+
'getHeight',
|
|
114
|
+
'getPending',
|
|
115
|
+
'getTx',
|
|
116
|
+
({
|
|
117
|
+
getAliases,
|
|
118
|
+
getChannels,
|
|
119
|
+
getClosed,
|
|
120
|
+
getCloseTxs,
|
|
121
|
+
getHeight,
|
|
122
|
+
getPending,
|
|
123
|
+
getTx,
|
|
124
|
+
},
|
|
125
|
+
cbk) =>
|
|
126
|
+
{
|
|
127
|
+
// Close transactions found by LND are added to the wallet transactions
|
|
128
|
+
const transactions = []
|
|
129
|
+
.concat(getTx.transactions)
|
|
130
|
+
.concat(getCloseTxs.transactions);
|
|
131
|
+
|
|
132
|
+
// Close tx outputs and HTLC tx outputs belong to closed channels
|
|
133
|
+
const closedContractOutputs = getClosed.channels.map(channel => {
|
|
134
|
+
return contractOutputs({
|
|
92
135
|
close_transaction_id: channel.close_transaction_id,
|
|
93
|
-
|
|
94
|
-
network: getNetwork.network,
|
|
95
|
-
transactions: getTx.transactions.filter(n => !!n.transaction),
|
|
136
|
+
payments: channel.close_payments,
|
|
96
137
|
});
|
|
138
|
+
});
|
|
97
139
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
140
|
+
// Pending close tx outputs and HTLC outputs belong to closing channels
|
|
141
|
+
const pendingContractOutputs = getPending.pending_channels.map(n => {
|
|
142
|
+
return contractOutputs({
|
|
143
|
+
close_transaction_id: n.close_transaction_id,
|
|
144
|
+
payments: n.pending_payments || [],
|
|
101
145
|
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const allContractOutputs = flatten([]
|
|
149
|
+
.concat(closedContractOutputs)
|
|
150
|
+
.concat(pendingContractOutputs));
|
|
102
151
|
|
|
152
|
+
const closes = getChannels.map((channel, i) => {
|
|
153
|
+
const {alias} = getAliases[i];
|
|
103
154
|
const currentHeight = getHeight.current_block_height;
|
|
104
|
-
const isRemoteForceClose = channel.is_remote_force_close;
|
|
105
155
|
const init = `${channel.transaction_id}:${channel.transaction_vout}`;
|
|
156
|
+
const isRemoteForceClose = channel.is_remote_force_close;
|
|
157
|
+
|
|
158
|
+
// Outputs of this channel are not shared with other channels
|
|
159
|
+
const ownContractOutputs = contractOutputs({
|
|
160
|
+
close_transaction_id: channel.close_transaction_id,
|
|
161
|
+
payments: channel.close_payments,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const otherContractOutputs = allContractOutputs.filter(output => {
|
|
165
|
+
return !ownContractOutputs.find(own => {
|
|
166
|
+
return own.id === output.id && own.vout === output.vout;
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const closeTx = transactions
|
|
171
|
+
.filter(n => !!n.transaction)
|
|
172
|
+
.find(n => n.id === channel.close_transaction_id);
|
|
173
|
+
|
|
174
|
+
// Our anchor output is found in the close tx when it is not reported
|
|
175
|
+
const anchor = anchorOutput({
|
|
176
|
+
anchor_vout: channel.anchor_vout,
|
|
177
|
+
close_balance_spent_by: channel.close_balance_spent_by,
|
|
178
|
+
close_balance_vout: channel.close_balance_vout,
|
|
179
|
+
close_payments: channel.close_payments,
|
|
180
|
+
close_transaction_id: channel.close_transaction_id,
|
|
181
|
+
is_cooperative_close: channel.is_cooperative_close,
|
|
182
|
+
transaction: !!closeTx ? closeTx.transaction : undefined,
|
|
183
|
+
});
|
|
106
184
|
|
|
107
185
|
const closingFeePaid = closingFees({
|
|
186
|
+
transactions,
|
|
187
|
+
anchor_is_confirmed: channel.anchor_is_confirmed,
|
|
188
|
+
anchor_is_pending: channel.anchor_is_pending,
|
|
189
|
+
anchor_spent_by: channel.anchor_spent_by,
|
|
190
|
+
anchor_vout: anchor.vout,
|
|
108
191
|
capacity: channel.capacity,
|
|
109
192
|
close_balance_spent_by: channel.close_balance_spent_by,
|
|
193
|
+
close_payments: channel.close_payments,
|
|
110
194
|
close_transaction_id: channel.close_transaction_id,
|
|
195
|
+
is_cooperative_close: channel.is_cooperative_close,
|
|
196
|
+
is_partner_closed: channel.is_partner_closed,
|
|
111
197
|
is_partner_initiated: channel.is_partner_initiated,
|
|
112
|
-
|
|
198
|
+
other_contract_outputs: otherContractOutputs,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const isFeeUnknown = closingFeePaid.fees === undefined;
|
|
202
|
+
|
|
203
|
+
const {resolutions} = closeResolutions({
|
|
204
|
+
transactions,
|
|
205
|
+
anchor_is_confirmed: channel.anchor_is_confirmed,
|
|
206
|
+
anchor_is_pending: channel.anchor_is_pending,
|
|
207
|
+
anchor_spent_by: channel.anchor_spent_by,
|
|
208
|
+
anchor_vout: anchor.vout,
|
|
209
|
+
close_balance_spent_by: channel.close_balance_spent_by,
|
|
210
|
+
close_balance_vout: channel.close_balance_vout,
|
|
211
|
+
close_payments: channel.close_payments,
|
|
212
|
+
close_transaction_id: channel.close_transaction_id,
|
|
113
213
|
});
|
|
114
214
|
|
|
115
215
|
return {
|
|
@@ -118,6 +218,7 @@ module.exports = ({limit, lnd, request}, cbk) => {
|
|
|
118
218
|
is_local_force_close: channel.is_local_force_close || undefined,
|
|
119
219
|
is_cooperative_close: channel.is_cooperative_close || undefined,
|
|
120
220
|
is_remote_force_close: isRemoteForceClose || undefined,
|
|
221
|
+
peer_opened_channel: channel.is_partner_initiated || undefined,
|
|
121
222
|
peer_closed_channel: channel.is_partner_closed || undefined,
|
|
122
223
|
blocks_since_close: currentHeight - channel.close_confirm_height,
|
|
123
224
|
capacity: channel.capacity,
|
|
@@ -125,16 +226,14 @@ module.exports = ({limit, lnd, request}, cbk) => {
|
|
|
125
226
|
channel_open: init,
|
|
126
227
|
channel_close: channel.close_transaction_id,
|
|
127
228
|
channel_balance_spend: channel.close_balance_spent_by || undefined,
|
|
128
|
-
channel_resolutions: resolutions
|
|
229
|
+
channel_resolutions: !resolutions.length ? undefined : resolutions,
|
|
129
230
|
is_breach_close: channel.is_breach_close || undefined,
|
|
130
231
|
closing_fee_paid: closingFeePaid.fees || undefined,
|
|
232
|
+
is_closing_fee_unknown: isFeeUnknown || undefined,
|
|
131
233
|
};
|
|
132
234
|
});
|
|
133
|
-
}],
|
|
134
235
|
|
|
135
|
-
|
|
136
|
-
closes: ['getSpends', ({getSpends}, cbk) => {
|
|
137
|
-
return cbk(null, {closes: getSpends.slice().reverse()});
|
|
236
|
+
return cbk(null, {closes: closes.reverse()});
|
|
138
237
|
}],
|
|
139
238
|
},
|
|
140
239
|
returnResult({reject, resolve, of :'closes'}, cbk));
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {componentsOfTransaction} = require('@alexbosworth/blockchain');
|
|
3
|
+
const {idForTransaction} = require('@alexbosworth/blockchain');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
const getSpendingTransaction = require('./get_spending_transaction');
|
|
7
|
+
|
|
8
|
+
const {isArray} = Array;
|
|
9
|
+
|
|
10
|
+
/** Get a close transaction by looking up the spend of its funding output
|
|
11
|
+
|
|
12
|
+
When the spend is not found or is not the close tx, nothing is returned
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
close_confirm_height: <Close Confirmed At Height Number>
|
|
16
|
+
close_transaction_id: <Close Transaction Id Hex String>
|
|
17
|
+
lnd: <Authenticated LND API Object>
|
|
18
|
+
transaction_id: <Funding Transaction Id Hex String>
|
|
19
|
+
transaction_vout: <Funding Transaction Output Index Number>
|
|
20
|
+
transactions: [{
|
|
21
|
+
id: <Transaction Id Hex String>
|
|
22
|
+
[transaction]: <Raw Transaction Hex String>
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@returns via cbk or Promise
|
|
27
|
+
{
|
|
28
|
+
[id]: <Close Transaction Id Hex String>
|
|
29
|
+
[transaction]: <Raw Close Transaction Hex String>
|
|
30
|
+
}
|
|
31
|
+
*/
|
|
32
|
+
module.exports = (args, cbk) => {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
return asyncAuto({
|
|
35
|
+
// Check arguments
|
|
36
|
+
validate: cbk => {
|
|
37
|
+
if (!args.close_confirm_height) {
|
|
38
|
+
return cbk([400, 'ExpectedCloseConfirmHeightToGetCloseFromSpend']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!args.close_transaction_id) {
|
|
42
|
+
return cbk([400, 'ExpectedCloseTransactionIdToGetCloseFromSpend']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!args.lnd) {
|
|
46
|
+
return cbk([400, 'ExpectedLndToGetCloseFromSpend']);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!args.transaction_id) {
|
|
50
|
+
return cbk([400, 'ExpectedFundingTransactionIdToGetCloseFromSpend']);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (args.transaction_vout === undefined) {
|
|
54
|
+
return cbk([400, 'ExpectedFundingOutputIndexToGetCloseFromSpend']);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isArray(args.transactions)) {
|
|
58
|
+
return cbk([400, 'ExpectedTransactionsToGetCloseFromSpend']);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return cbk();
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Find the funding output to look for the spend of
|
|
65
|
+
fundingOutput: ['validate', ({}, cbk) => {
|
|
66
|
+
const [fundingTx] = args.transactions
|
|
67
|
+
.filter(n => !!n.transaction)
|
|
68
|
+
.filter(n => n.id === args.transaction_id);
|
|
69
|
+
|
|
70
|
+
// Exit early when the funding transaction is not known
|
|
71
|
+
if (!fundingTx) {
|
|
72
|
+
return cbk();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const {outputs} = componentsOfTransaction({
|
|
76
|
+
transaction: fundingTx.transaction,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return cbk(null, outputs[args.transaction_vout]);
|
|
80
|
+
}],
|
|
81
|
+
|
|
82
|
+
// Get the transaction that spent the funding output
|
|
83
|
+
getSpend: ['fundingOutput', ({fundingOutput}, cbk) => {
|
|
84
|
+
// Exit early when the funding output script is not known
|
|
85
|
+
if (!fundingOutput) {
|
|
86
|
+
return cbk();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return getSpendingTransaction({
|
|
90
|
+
lnd: args.lnd,
|
|
91
|
+
min_height: args.close_confirm_height,
|
|
92
|
+
output_script: fundingOutput.script,
|
|
93
|
+
transaction_id: args.transaction_id,
|
|
94
|
+
transaction_vout: args.transaction_vout,
|
|
95
|
+
},
|
|
96
|
+
(err, res) => {
|
|
97
|
+
// Exit early when LND can't find the spend, leaving it unknown
|
|
98
|
+
if (!!err) {
|
|
99
|
+
return cbk();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return cbk(null, res.transaction);
|
|
103
|
+
});
|
|
104
|
+
}],
|
|
105
|
+
|
|
106
|
+
// The spend is the close transaction when it has the close tx id
|
|
107
|
+
close: ['getSpend', ({getSpend}, cbk) => {
|
|
108
|
+
// Exit early when the spend was not found
|
|
109
|
+
if (!getSpend) {
|
|
110
|
+
return cbk(null, {});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const {id} = idForTransaction({transaction: getSpend});
|
|
114
|
+
|
|
115
|
+
// Exit early when the spend is not the expected close tx
|
|
116
|
+
if (id !== args.close_transaction_id) {
|
|
117
|
+
return cbk(null, {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return cbk(null, {id, transaction: getSpend});
|
|
121
|
+
}],
|
|
122
|
+
},
|
|
123
|
+
returnResult({reject, resolve, of: 'close'}, cbk));
|
|
124
|
+
});
|
|
125
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncMapSeries = require('async/mapSeries');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const getCloseFromSpend = require('./get_close_from_spend');
|
|
6
|
+
const getClosesInBlocks = require('./get_closes_in_blocks');
|
|
7
|
+
|
|
8
|
+
const {isArray} = Array;
|
|
9
|
+
|
|
10
|
+
/** Get close transactions that are missing from the wallet transactions
|
|
11
|
+
|
|
12
|
+
A close transaction spends the channel funding output. When the funding
|
|
13
|
+
transaction is in the wallet, LND can find the close transaction by looking
|
|
14
|
+
for the spend of the funding output.
|
|
15
|
+
|
|
16
|
+
When the spend is not found that way, the close transaction is looked for in
|
|
17
|
+
the block that it was confirmed in using the LND chain backend.
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
channels: [{
|
|
21
|
+
[close_confirm_height]: <Close Confirmed At Height Number>
|
|
22
|
+
close_transaction_id: <Close Transaction Id Hex String>
|
|
23
|
+
transaction_id: <Funding Transaction Id Hex String>
|
|
24
|
+
transaction_vout: <Funding Transaction Output Index Number>
|
|
25
|
+
}]
|
|
26
|
+
lnd: <Authenticated LND API Object>
|
|
27
|
+
transactions: [{
|
|
28
|
+
id: <Transaction Id Hex String>
|
|
29
|
+
[transaction]: <Raw Transaction Hex String>
|
|
30
|
+
}]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@returns via cbk or Promise
|
|
34
|
+
{
|
|
35
|
+
transactions: [{
|
|
36
|
+
id: <Close Transaction Id Hex String>
|
|
37
|
+
transaction: <Raw Close Transaction Hex String>
|
|
38
|
+
}]
|
|
39
|
+
}
|
|
40
|
+
*/
|
|
41
|
+
module.exports = (args, cbk) => {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
return asyncAuto({
|
|
44
|
+
// Check arguments
|
|
45
|
+
validate: cbk => {
|
|
46
|
+
if (!isArray(args.channels)) {
|
|
47
|
+
return cbk([400, 'ExpectedChannelsToGetCloseTransactions']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!args.lnd) {
|
|
51
|
+
return cbk([400, 'ExpectedLndToGetCloseTransactions']);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!isArray(args.transactions)) {
|
|
55
|
+
return cbk([400, 'ExpectedTransactionsToGetCloseTransactions']);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return cbk();
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// Channels with a close tx missing from the wallet and a height to look
|
|
62
|
+
missing: ['validate', ({}, cbk) => {
|
|
63
|
+
const knownIds = args.transactions
|
|
64
|
+
.filter(n => !!n.transaction)
|
|
65
|
+
.map(n => n.id);
|
|
66
|
+
|
|
67
|
+
const channels = args.channels
|
|
68
|
+
.filter(n => !!n.close_confirm_height)
|
|
69
|
+
.filter(n => !knownIds.includes(n.close_transaction_id));
|
|
70
|
+
|
|
71
|
+
return cbk(null, channels);
|
|
72
|
+
}],
|
|
73
|
+
|
|
74
|
+
// Look for the close transactions as spends of the funding outputs
|
|
75
|
+
getSpends: ['missing', ({missing}, cbk) => {
|
|
76
|
+
return asyncMapSeries(missing, (channel, cbk) => {
|
|
77
|
+
return getCloseFromSpend({
|
|
78
|
+
close_confirm_height: channel.close_confirm_height,
|
|
79
|
+
close_transaction_id: channel.close_transaction_id,
|
|
80
|
+
lnd: args.lnd,
|
|
81
|
+
transaction_id: channel.transaction_id,
|
|
82
|
+
transaction_vout: channel.transaction_vout,
|
|
83
|
+
transactions: args.transactions,
|
|
84
|
+
},
|
|
85
|
+
cbk);
|
|
86
|
+
},
|
|
87
|
+
cbk);
|
|
88
|
+
}],
|
|
89
|
+
|
|
90
|
+
// Find the remaining close transactions in their confirmation blocks
|
|
91
|
+
getBlockTxs: ['getSpends', 'missing', ({getSpends, missing}, cbk) => {
|
|
92
|
+
const found = getSpends.filter(n => !!n.id).map(n => n.id);
|
|
93
|
+
|
|
94
|
+
const remaining = missing.filter(channel => {
|
|
95
|
+
return !found.includes(channel.close_transaction_id);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return getClosesInBlocks({channels: remaining, lnd: args.lnd}, cbk);
|
|
99
|
+
}],
|
|
100
|
+
|
|
101
|
+
// Close transactions that were found
|
|
102
|
+
transactions: [
|
|
103
|
+
'getBlockTxs',
|
|
104
|
+
'getSpends',
|
|
105
|
+
({getBlockTxs, getSpends}, cbk) =>
|
|
106
|
+
{
|
|
107
|
+
const transactions = []
|
|
108
|
+
.concat(getSpends.filter(n => !!n.transaction))
|
|
109
|
+
.concat(getBlockTxs.transactions);
|
|
110
|
+
|
|
111
|
+
return cbk(null, {transactions});
|
|
112
|
+
}],
|
|
113
|
+
},
|
|
114
|
+
returnResult({reject, resolve, of: 'transactions'}, cbk));
|
|
115
|
+
});
|
|
116
|
+
};
|