paid-services 3.1.2 → 3.5.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/.prettierignore +2 -0
- package/CHANGELOG.md +18 -0
- package/capacity/accept_capacity_change.js +284 -0
- package/capacity/ask_for_change_details.js +538 -0
- package/capacity/ask_for_decrease.js +185 -0
- package/capacity/assemble_replacement_tx.js +171 -0
- package/capacity/capacity_change_requests.js +85 -0
- package/capacity/change_channel_capacity.js +189 -0
- package/capacity/encode_change_request.js +62 -0
- package/capacity/encode_sign_capacity_response.js +32 -0
- package/capacity/fee_for_replacement.js +108 -0
- package/capacity/finalize_capacity_replacement.js +67 -0
- package/capacity/get_capacity_change_requests.js +99 -0
- package/capacity/get_capacity_replacement.js +372 -0
- package/capacity/get_replacement_signature.js +232 -0
- package/capacity/index.js +4 -0
- package/capacity/initiate_capacity_change.js +507 -0
- package/capacity/interim_replacement_psbt.js +57 -0
- package/capacity/is_open_witness_script.js +40 -0
- package/capacity/parse_accept_request.js +58 -0
- package/capacity/parse_accept_response.js +52 -0
- package/capacity/parse_capacity_change_request.js +108 -0
- package/capacity/parse_sign_capacity_request.js +59 -0
- package/capacity/propose_capacity_change.js +364 -0
- package/capacity/sign_capacity_replacement.js +119 -0
- package/capacity/witness_script_from_close_txs.js +50 -0
- package/index.js +6 -0
- package/p2p/encode_peer_request.js +59 -0
- package/p2p/encode_peer_response.js +44 -0
- package/p2p/encode_response_code.js +49 -0
- package/p2p/index.js +4 -0
- package/p2p/make_peer_request.js +151 -0
- package/p2p/parse_request_message.js +103 -0
- package/p2p/parse_response_code.js +60 -0
- package/p2p/return_failure_response.js +43 -0
- package/p2p/return_success_response.js +47 -0
- package/p2p/service_peer_requests.js +108 -0
- package/package.json +8 -4
- package/service_types.json +16 -0
- package/test/capacity/test_assemble_replacement_tx.js +46 -0
- package/test/capacity/test_capacity_change_requests.js +55 -0
- package/test/capacity/test_encode_change_request.js +54 -0
- package/test/capacity/test_encode_sign_capacity_response.js +47 -0
- package/test/capacity/test_fee_for_replacement.js +40 -0
- package/test/capacity/test_finalize_capacity_replacement.js +45 -0
- package/test/capacity/test_interim_replacement_psbt.js +44 -0
- package/test/capacity/test_is_open_witness_script.js +40 -0
- package/test/capacity/test_parse_accept_request.js +42 -0
- package/test/capacity/test_parse_capacity_change_request.js +52 -0
- package/test/capacity/test_parse_sign_capacity_request.js +49 -0
- package/test/capacity/test_witness_script_from_close_txs.js +53 -0
- package/test/integration/test_accept_capacity_change.js +189 -0
- package/test/integration/test_change_channel_capacity.js +203 -0
- package/test/integration/test_decrease_channel_capacity.js +216 -0
- package/test/integration/test_increase_channel_capacity.js +201 -0
- package/test/integration/test_service_peer_requests.js +59 -0
- package/test/integration/test_sign_capacity_replacement.js +142 -0
- package/test/p2p/test_encode_peer_request.js +53 -0
- package/test/p2p/test_encode_peer_response.js +38 -0
- package/test/p2p/test_encode_response_code.js +48 -0
- package/test/trades/test_decode_basic_trade.js +40 -0
- package/test/trades/test_decode_open_trade.js +50 -0
- package/test/trades/test_encode_open_trade.js +35 -0
- package/test/trades/test_encode_trade.js +37 -0
- package/trades/accept_trade.js +52 -0
- package/trades/create_trade.js +124 -44
- package/trades/decode_basic_trade.js +45 -0
- package/trades/decode_open_trade.js +69 -0
- package/trades/decode_trade.js +48 -66
- package/trades/decode_trade_secret.js +89 -0
- package/trades/encode_open_trade.js +89 -0
- package/trades/encode_trade.js +48 -33
- package/trades/encode_trade_secret.js +50 -0
- package/trades/finalize_trade_secret.js +116 -0
- package/trades/find_trade.js +314 -0
- package/trades/manage_trade.js +46 -35
- package/trades/network_from_network_record.js +8 -2
- package/trades/network_record_from_network.js +36 -0
- package/trades/network_record_from_request.js +6 -0
- package/trades/node_as_record.js +98 -0
- package/trades/record_as_node.js +113 -0
- package/trades/request_trade_by_id.js +89 -0
- package/trades/service_trade_requests.js +184 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const {address} = require('bitcoinjs-lib');
|
|
2
|
+
const asyncAuto = require('async/auto');
|
|
3
|
+
const {createChainAddress} = require('ln-service');
|
|
4
|
+
const {getNetwork} = require('ln-sync');
|
|
5
|
+
const {networks} = require('bitcoinjs-lib');
|
|
6
|
+
const {returnResult} = require('asyncjs-util');
|
|
7
|
+
|
|
8
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
9
|
+
const dust = 550;
|
|
10
|
+
const isNumber = n => !isNaN(n);
|
|
11
|
+
const {toOutputScript} = address;
|
|
12
|
+
|
|
13
|
+
/** Ask for details about a decrease
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
ask: <Ask Function>
|
|
17
|
+
lnd: <Authenticated LND API Object>
|
|
18
|
+
max: <Maximum Possible Decrease Number>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@returns via cbk or Promise
|
|
22
|
+
{
|
|
23
|
+
[address]: <Send Funds to Address String>
|
|
24
|
+
is_final: <Decrease is Final Decrease Bool>
|
|
25
|
+
[output]: <Output Script Hex String>
|
|
26
|
+
tokens: <Withdraw Tokens Number>
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = ({ask, lnd, max}, cbk) => {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
return asyncAuto({
|
|
32
|
+
// Check arguments
|
|
33
|
+
validate: cbk => {
|
|
34
|
+
if (!ask) {
|
|
35
|
+
return cbk([400, 'ExpectedAskFunctionToAskForDecrease']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!lnd) {
|
|
39
|
+
return cbk([400, 'ExpectedAuthenticatedLndToAskForDecrease']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!max) {
|
|
43
|
+
return cbk([400, 'ExpectedMaximumAvaialbleTok'])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cbk();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Ask for the amount to decrease
|
|
50
|
+
askForAmount: ['validate', ({}, cbk) => {
|
|
51
|
+
return ask({
|
|
52
|
+
default: '0',
|
|
53
|
+
name: 'amount',
|
|
54
|
+
message: `How much do you want to spend out (max: ${max})?`,
|
|
55
|
+
type: 'input',
|
|
56
|
+
validate: input => {
|
|
57
|
+
if (!isNumber(input) || !Number.isInteger(Number(input))) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!!Number(input) && Number(input) < dust) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!!Number(input) && Number(input) > max) {
|
|
66
|
+
return `The maximum possible to decrease is ${max}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
({amount}) => cbk(null, amount));
|
|
73
|
+
}],
|
|
74
|
+
|
|
75
|
+
// Get network name
|
|
76
|
+
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
|
|
77
|
+
|
|
78
|
+
// Ask if withdrawing to an external address
|
|
79
|
+
askForExternal: ['askForAmount', ({askForAmount}, cbk) => {
|
|
80
|
+
if (!Number(askForAmount)) {
|
|
81
|
+
return cbk();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return ask({
|
|
85
|
+
default: false,
|
|
86
|
+
name: 'spend',
|
|
87
|
+
message: `Spend ${askForAmount} to an external address?`,
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
},
|
|
90
|
+
({spend}) => cbk(null, spend));
|
|
91
|
+
}],
|
|
92
|
+
|
|
93
|
+
// Ask for an external address
|
|
94
|
+
askForAddress: [
|
|
95
|
+
'askForAmount',
|
|
96
|
+
'askForExternal',
|
|
97
|
+
({askForAmount, askForExternal}, cbk) =>
|
|
98
|
+
{
|
|
99
|
+
// Exit early when the withdraw address is internal
|
|
100
|
+
if (askForExternal !== true) {
|
|
101
|
+
return cbk();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ask({
|
|
105
|
+
name: 'address',
|
|
106
|
+
message: `Address to spend ${askForAmount} to?`,
|
|
107
|
+
type: 'input',
|
|
108
|
+
validate: input => !!input,
|
|
109
|
+
},
|
|
110
|
+
({address}) => cbk(null, {address}));
|
|
111
|
+
}],
|
|
112
|
+
|
|
113
|
+
// Ask if this is the last output
|
|
114
|
+
askForAddition: [
|
|
115
|
+
'askForAddress',
|
|
116
|
+
'askForExternal',
|
|
117
|
+
({askForExternal}, cbk) =>
|
|
118
|
+
{
|
|
119
|
+
// Exit early when the spend was to an internal addess
|
|
120
|
+
if (!askForExternal) {
|
|
121
|
+
return cbk();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return ask({
|
|
125
|
+
default: false,
|
|
126
|
+
name: 'add',
|
|
127
|
+
message: 'Add another spend to a different address?',
|
|
128
|
+
type: 'confirm',
|
|
129
|
+
},
|
|
130
|
+
({add}) => cbk(null, add));
|
|
131
|
+
}],
|
|
132
|
+
|
|
133
|
+
// Create a decrease address to withdraw funds out to
|
|
134
|
+
createAddress: ['askForExternal', ({askForExternal}, cbk) => {
|
|
135
|
+
// Exit early when the withdraw address is external
|
|
136
|
+
if (askForExternal !== false) {
|
|
137
|
+
return cbk();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return createChainAddress({lnd}, cbk);
|
|
141
|
+
}],
|
|
142
|
+
|
|
143
|
+
// Final decrease output details
|
|
144
|
+
decrease: [
|
|
145
|
+
'askForAddition',
|
|
146
|
+
'askForAddress',
|
|
147
|
+
'askForAmount',
|
|
148
|
+
'createAddress',
|
|
149
|
+
'getNetwork',
|
|
150
|
+
({
|
|
151
|
+
askForAddition,
|
|
152
|
+
askForAddress,
|
|
153
|
+
askForAmount,
|
|
154
|
+
createAddress,
|
|
155
|
+
getNetwork,
|
|
156
|
+
},
|
|
157
|
+
cbk) =>
|
|
158
|
+
{
|
|
159
|
+
const {address} = (createAddress || askForAddress || {});
|
|
160
|
+
|
|
161
|
+
// Exit early when not decreasing to an address
|
|
162
|
+
if (!address) {
|
|
163
|
+
return cbk(null, {is_final: true, tokens: Number(askForAmount)});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Make sure the address is valid
|
|
167
|
+
try {
|
|
168
|
+
toOutputScript(address, networks[getNetwork.bitcoinjs]);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return cbk([400, 'FailedToParseAddress', {err}]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const output = toOutputScript(address, networks[getNetwork.bitcoinjs]);
|
|
174
|
+
|
|
175
|
+
return cbk(null, {
|
|
176
|
+
address,
|
|
177
|
+
is_final: !askForAddition,
|
|
178
|
+
output: bufferAsHex(output),
|
|
179
|
+
tokens: Number(askForAmount),
|
|
180
|
+
});
|
|
181
|
+
}],
|
|
182
|
+
},
|
|
183
|
+
returnResult({reject, resolve, of: 'decrease'}, cbk));
|
|
184
|
+
});
|
|
185
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const {address} = require('bitcoinjs-lib');
|
|
2
|
+
const {networks} = require('bitcoinjs-lib');
|
|
3
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
4
|
+
|
|
5
|
+
const isOpenWitnessScript = require('./is_open_witness_script');
|
|
6
|
+
|
|
7
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
8
|
+
const {fromHex} = Transaction;
|
|
9
|
+
const hashFromTxId = id => Buffer.from(id, 'hex').reverse();
|
|
10
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
11
|
+
const {isArray} = Array;
|
|
12
|
+
const sequence = 0;
|
|
13
|
+
const {toOutputScript} = address;
|
|
14
|
+
|
|
15
|
+
/** Put together the replacement transaction
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
[add_funds_transaction_id]: <Add Funds Transaction Id Hex String>
|
|
19
|
+
[add_funds_transaction_vout]: <Add Funds Transaction Output Index Number>
|
|
20
|
+
bitcoinjs_network: <BitcoinJs Network Name String>
|
|
21
|
+
close_transaction: <Force Closed Transaction Hex String>
|
|
22
|
+
decrease: [{
|
|
23
|
+
output: <Output Script Hex Encoded String>
|
|
24
|
+
tokens: <Decrease By Tokens Number>
|
|
25
|
+
}]
|
|
26
|
+
funding_address: <Replacement Channel Funding Address String>
|
|
27
|
+
funding_tokens: <Replacement Channel Funding Tokens Number>
|
|
28
|
+
transaction_id: <Original Channel Funding Transaction Id Hex String>
|
|
29
|
+
transaction_vout: <Original Channel Funding Output Index Number>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@throws
|
|
33
|
+
<Error>
|
|
34
|
+
|
|
35
|
+
@returns
|
|
36
|
+
{
|
|
37
|
+
[add_funds_vin]: <Add Funds Input Index>
|
|
38
|
+
transaction: <Unsigned Transaction Hex String>
|
|
39
|
+
transaction_id: <Transaction Id Hex String>
|
|
40
|
+
transaction_vin: <Input Index of Original Channel Spend Number>
|
|
41
|
+
transaction_vout: <Replacement Channel Transaction Output Index Number>
|
|
42
|
+
}
|
|
43
|
+
*/
|
|
44
|
+
module.exports = args => {
|
|
45
|
+
if (!args.bitcoinjs_network) {
|
|
46
|
+
throw new Error('ExpectedBitcoinJsNetworkNameToAssembleReplacementTx');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!args.close_transaction) {
|
|
50
|
+
throw new Error('ExpectedCloseTransactionToAssembleReplacementTx');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!isArray(args.decrease)) {
|
|
54
|
+
throw new Error('ExpectedArrayOfDecreasesToAssembleReplacementTx');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!args.funding_address) {
|
|
58
|
+
throw new Error('ExpectedFundingAddressToAssembleReplacementTx');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!args.funding_tokens) {
|
|
62
|
+
throw new Error('ExpectedFundingTokensToAssembleReplacementTx');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!args.transaction_id) {
|
|
66
|
+
throw new Error('ExpectedFundingTransactionIdToAssembleReplacementTx');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (args.transaction_vout === undefined) {
|
|
70
|
+
throw new Error('ExpectedFundingTransactionVoutToAssembleReplacementTx');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const replacement = new Transaction();
|
|
74
|
+
|
|
75
|
+
// The replacement will spend the same input as the close transaction
|
|
76
|
+
const [{witness}, secondInput] = fromHex(args.close_transaction).ins;
|
|
77
|
+
|
|
78
|
+
// Confirm that the witness stack is a 2:2
|
|
79
|
+
if (!isOpenWitnessScript({script: bufferAsHex(witness.slice().pop())})) {
|
|
80
|
+
throw new Error('ExpectedMultiSigWitnessInCloseTransaction');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!!secondInput) {
|
|
84
|
+
throw new Error('ExpectedOnlySingleInputForCloseTransaction');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The replacement will spend the funding outpoint
|
|
88
|
+
const hash = hashFromTxId(args.transaction_id);
|
|
89
|
+
const index = args.transaction_vout;
|
|
90
|
+
|
|
91
|
+
// The new channel multi-sig will be the replacement output
|
|
92
|
+
const network = networks[args.bitcoinjs_network];
|
|
93
|
+
|
|
94
|
+
// The replacement spends to the channel funding output
|
|
95
|
+
const fundingOutput = {
|
|
96
|
+
script: toOutputScript(args.funding_address, network),
|
|
97
|
+
tokens: args.funding_tokens,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// There can also be other outputs attached
|
|
101
|
+
const decreaseOutputs = args.decrease.map(({output, tokens}) => ({
|
|
102
|
+
tokens,
|
|
103
|
+
script: hexAsBuffer(output),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const outputs = [].concat(decreaseOutputs).concat(fundingOutput);
|
|
107
|
+
|
|
108
|
+
// Sort outputs by BIP 69
|
|
109
|
+
outputs.sort((a, b) => {
|
|
110
|
+
// Sort by tokens ascending when no tie breaker needed
|
|
111
|
+
if (a.tokens !== b.tokens) {
|
|
112
|
+
return a.tokens - b.tokens;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Otherwise compare output scripts lexicographically ascending
|
|
116
|
+
return a.script.compare(b.script);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const respendOutputIndex = outputs.findIndex(out => {
|
|
120
|
+
return out.script.equals(fundingOutput.script);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Add the outputs to the replacement transaction
|
|
124
|
+
outputs.forEach(({script, tokens}) => replacement.addOutput(script, tokens));
|
|
125
|
+
|
|
126
|
+
// Exit early when not adding more inputs to the transaction
|
|
127
|
+
if (!args.add_funds_transaction_id) {
|
|
128
|
+
replacement.addInput(hash, index, sequence);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
transaction: replacement.toHex(),
|
|
132
|
+
transaction_id: replacement.getId(),
|
|
133
|
+
transaction_vin: replacement.ins.findIndex(input => {
|
|
134
|
+
return input.hash.equals(hash) && input.index === index;
|
|
135
|
+
}),
|
|
136
|
+
transaction_vout: respendOutputIndex,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// An additional input needs to be added when adding funds
|
|
141
|
+
const add = {
|
|
142
|
+
hash: hashFromTxId(args.add_funds_transaction_id),
|
|
143
|
+
index: args.add_funds_transaction_vout,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Adding funds means spending the channel output plus the transit one
|
|
147
|
+
const spends = [{hash, index}, add];
|
|
148
|
+
|
|
149
|
+
// Sort the spending inputs for BIP 69
|
|
150
|
+
spends.sort((a, b) => {
|
|
151
|
+
const aHash = bufferAsHex(a.hash);
|
|
152
|
+
const bHash = bufferAsHex(b.hash);
|
|
153
|
+
|
|
154
|
+
return aHash.localeCompare(bHash) || a.index - b.index;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Add the spends as inputs to the replacement tx
|
|
158
|
+
spends.forEach(({hash, index}) => replacement.addInput(hash, index));
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
add_funds_vin: replacement.ins.findIndex(input => {
|
|
162
|
+
return input.hash.equals(add.hash) && input.index === add.index;
|
|
163
|
+
}),
|
|
164
|
+
transaction: replacement.toHex(),
|
|
165
|
+
transaction_id: replacement.getId(),
|
|
166
|
+
transaction_vin: replacement.ins.findIndex(input => {
|
|
167
|
+
return input.hash.equals(hash) && input.index === index;
|
|
168
|
+
}),
|
|
169
|
+
transaction_vout: respendOutputIndex,
|
|
170
|
+
};
|
|
171
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const privateAsType = isPrivate => isPrivate ? 1 : 0;
|
|
2
|
+
|
|
3
|
+
/** Map change requests to requests considering local channels
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
channels: [{
|
|
7
|
+
capacity: <Channel Capacity Number>
|
|
8
|
+
[cooperative_close_address]: <Cooperative Close Locked Address String>
|
|
9
|
+
id: <Standard Format Channel Id String>
|
|
10
|
+
partner_public_key: <Peer Public Key Hex String>
|
|
11
|
+
}]
|
|
12
|
+
requests: [{
|
|
13
|
+
channel: <Standard Format Channel Id String>
|
|
14
|
+
[decrease]: <Reduce Capacity Tokens Number>
|
|
15
|
+
from: <From Node Public Key Id Hex String>
|
|
16
|
+
id: <Change Request Id Hex String>
|
|
17
|
+
[increase]: <Add Capacity Tokens Number>
|
|
18
|
+
[type]: <Intended Replacement Channel Channel Type Flags Number>
|
|
19
|
+
}]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@returns
|
|
23
|
+
{
|
|
24
|
+
requests: [{
|
|
25
|
+
[address]: <Cooperative Close Address String>
|
|
26
|
+
capacity: <Channel Capacity Tokens Number>
|
|
27
|
+
channel: <Standard Format Channel Id String>
|
|
28
|
+
[decrease]: <Reduce Capacity Tokens Number>
|
|
29
|
+
from: <From Node Public Key Id Hex String>
|
|
30
|
+
id: <Change Request Id Hex String>
|
|
31
|
+
[increase]: <Add Capacity Tokens Number>
|
|
32
|
+
[type]: <Change Channel Channel Type Flags Number>
|
|
33
|
+
}]
|
|
34
|
+
}
|
|
35
|
+
*/
|
|
36
|
+
module.exports = ({channels, requests}) => {
|
|
37
|
+
// Change requests can only come from channels with the messaging peer
|
|
38
|
+
const capacityChangeRequests = requests.filter(request => {
|
|
39
|
+
const channel = channels.find(chan => chan.id === request.channel);
|
|
40
|
+
|
|
41
|
+
// Ignore requests from channels that aren't present
|
|
42
|
+
if (!channel) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The change request must come from the peer in the channel
|
|
47
|
+
return channel.partner_public_key === request.from;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const existing = {};
|
|
51
|
+
|
|
52
|
+
// Only consider one change request per channel
|
|
53
|
+
const changeRequests = capacityChangeRequests
|
|
54
|
+
.filter(({channel}) => {
|
|
55
|
+
// Exit early when the channel already has a change request
|
|
56
|
+
if (!!existing[channel]) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Do not allow more requests for this channel
|
|
61
|
+
existing[channel] = true;
|
|
62
|
+
|
|
63
|
+
return true;
|
|
64
|
+
})
|
|
65
|
+
.map(request => {
|
|
66
|
+
const channel = channels.find(chan => chan.id === request.channel);
|
|
67
|
+
|
|
68
|
+
const currentType = privateAsType(channel.is_private);
|
|
69
|
+
|
|
70
|
+
const type = request.type !== undefined && request.type !== currentType;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
address: channel.cooperative_close_address,
|
|
74
|
+
capacity: channel.capacity,
|
|
75
|
+
channel: channel.id,
|
|
76
|
+
decrease: request.decrease,
|
|
77
|
+
from: channel.partner_public_key,
|
|
78
|
+
id: request.id,
|
|
79
|
+
increase: request.increase,
|
|
80
|
+
type: type ? request.type : undefined,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {requests: changeRequests};
|
|
85
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncDetectSeries = require('async/detectSeries');
|
|
3
|
+
const asyncRetry = require('async/retry');
|
|
4
|
+
const {getIdentity} = require('ln-service');
|
|
5
|
+
const {getNodeAlias} = require('ln-sync');
|
|
6
|
+
const {returnResult} = require('asyncjs-util');
|
|
7
|
+
const {updateChannelFee} = require('ln-sync');
|
|
8
|
+
|
|
9
|
+
const acceptCapacityChange = require('./accept_capacity_change');
|
|
10
|
+
const getCapacityChangeRequests = require('./get_capacity_change_requests');
|
|
11
|
+
const initiateCapacityChange = require('./initiate_capacity_change');
|
|
12
|
+
|
|
13
|
+
const describeType = type => !(type & 0) ? 'private' : 'public';
|
|
14
|
+
const interval = 10 * 1000;
|
|
15
|
+
const peerName = ({alias, id}) => `${alias} ${id.substring(0, 8)}`.trim();
|
|
16
|
+
const times = 6 * 60 * 6;
|
|
17
|
+
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
|
|
18
|
+
const waitForCloseInterval = 1000 * 3;
|
|
19
|
+
|
|
20
|
+
/** Change a channel's capacity
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
ask: <Ask Function>
|
|
24
|
+
[delay]: <Wait Time For Incoming Capacity Requests Milliseconds Number>
|
|
25
|
+
lnd: <Authenticated LND API Object>
|
|
26
|
+
logger: <Winston Logger Object>
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = ({ask, delay, lnd, logger}, cbk) => {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
return asyncAuto({
|
|
32
|
+
// Check arguments
|
|
33
|
+
validate: cbk => {
|
|
34
|
+
if (!ask) {
|
|
35
|
+
return cbk([400, 'ExpectedAskFunctionToChangeCapacity']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!lnd) {
|
|
39
|
+
return cbk([400, 'ExpectedLndToChangeCapacity']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!logger) {
|
|
43
|
+
return cbk([400, 'ExpectedLoggerObjectToChangeCapacity']);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cbk();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Ask to confirm what will happen in a change capacity
|
|
50
|
+
askToConfirm: ['validate', ({}, cbk) => {
|
|
51
|
+
const details = [
|
|
52
|
+
'This is experimental. Channels may force-close instead of change.',
|
|
53
|
+
'Adding funds currently requires an extra confirmation wait.',
|
|
54
|
+
'Chain fees will be deducted from the changed capacity total.',
|
|
55
|
+
'Chain fees will be paid from the initiator local balance.',
|
|
56
|
+
'Forwards with the peer will be halted during capacity change.',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Show the warnings
|
|
60
|
+
logger.info({details});
|
|
61
|
+
|
|
62
|
+
return ask({type: 'confirm', name: 'ok', message: 'OK?'}, ({ok}) => {
|
|
63
|
+
if (!ok) {
|
|
64
|
+
return cbk([400, 'CanceledChannelCapacityChange']);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cbk();
|
|
68
|
+
});
|
|
69
|
+
}],
|
|
70
|
+
|
|
71
|
+
// Get the public key of this node
|
|
72
|
+
getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
|
|
73
|
+
|
|
74
|
+
// Wait for an incoming capacity change request
|
|
75
|
+
waitForProposal: ['validate', ({}, cbk) => {
|
|
76
|
+
return getCapacityChangeRequests({delay, lnd}, cbk);
|
|
77
|
+
}],
|
|
78
|
+
|
|
79
|
+
// Ask for acceptance of a channel
|
|
80
|
+
askToSelectChange: [
|
|
81
|
+
'askToConfirm',
|
|
82
|
+
'waitForProposal',
|
|
83
|
+
({waitForProposal}, cbk) =>
|
|
84
|
+
{
|
|
85
|
+
return asyncDetectSeries(waitForProposal.requests, (request, cbk) => {
|
|
86
|
+
return getNodeAlias({lnd, id: request.from}, (err, res) => {
|
|
87
|
+
if (!!err) {
|
|
88
|
+
return cbk(err);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const change = !!request.increase ? 'Increase' : 'Decrease';
|
|
92
|
+
const delta = request.decrease || request.increase;
|
|
93
|
+
const hasType = request.type !== undefined;
|
|
94
|
+
const id = request.channel;
|
|
95
|
+
const peer = peerName(res);
|
|
96
|
+
const size = tokensAsBigUnit(request.capacity);
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
const action = `${change} capacity ${size} channel ${id}`;
|
|
100
|
+
const by = !!delta ? ` by ${tokensAsBigUnit(delta)}` : '';
|
|
101
|
+
const type = hasType ? describeType(request.type) : '';
|
|
102
|
+
|
|
103
|
+
const changeType = hasType ? ` and make channel ${type}` : '';
|
|
104
|
+
|
|
105
|
+
return ask({
|
|
106
|
+
type: 'confirm',
|
|
107
|
+
name: 'accept',
|
|
108
|
+
message: `${action} with ${peer}${by}${changeType}?`,
|
|
109
|
+
},
|
|
110
|
+
({accept}) => {
|
|
111
|
+
if (!accept) {
|
|
112
|
+
return cbk(null, false);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fail changing when cooperative_close_address is set
|
|
116
|
+
if (!!request.address && !!request.increase) {
|
|
117
|
+
return cbk([400, 'ChannelHasLockedCoopCloseAddressSet']);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return cbk(null, true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
cbk);
|
|
125
|
+
}],
|
|
126
|
+
|
|
127
|
+
// Accept a capacity change proposal
|
|
128
|
+
acceptChange: ['askToSelectChange', ({askToSelectChange}, cbk) => {
|
|
129
|
+
// Exit early when there is no change proposal to accept
|
|
130
|
+
if (!askToSelectChange) {
|
|
131
|
+
return cbk();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return acceptCapacityChange({
|
|
135
|
+
lnd,
|
|
136
|
+
logger,
|
|
137
|
+
channel: askToSelectChange.channel,
|
|
138
|
+
from: askToSelectChange.from,
|
|
139
|
+
id: askToSelectChange.id,
|
|
140
|
+
increase: askToSelectChange.increase,
|
|
141
|
+
},
|
|
142
|
+
cbk);
|
|
143
|
+
}],
|
|
144
|
+
|
|
145
|
+
// Initiate a capacity change
|
|
146
|
+
initiateChange: ['askToSelectChange', ({askToSelectChange}, cbk) => {
|
|
147
|
+
if (!!askToSelectChange) {
|
|
148
|
+
return cbk();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return initiateCapacityChange({ask, lnd, logger}, cbk);
|
|
152
|
+
}],
|
|
153
|
+
|
|
154
|
+
// Reset the channel routing policies
|
|
155
|
+
adjustPolicy: [
|
|
156
|
+
'acceptChange',
|
|
157
|
+
'getIdentity',
|
|
158
|
+
'initiateChange',
|
|
159
|
+
({acceptChange, getIdentity, initiateChange}, cbk) =>
|
|
160
|
+
{
|
|
161
|
+
const policy = acceptChange || initiateChange;
|
|
162
|
+
|
|
163
|
+
logger.info({restoring_routing_policy: true});
|
|
164
|
+
|
|
165
|
+
return asyncRetry({interval, times}, cbk => {
|
|
166
|
+
return updateChannelFee({
|
|
167
|
+
lnd,
|
|
168
|
+
base_fee_mtokens: policy.base_fee_mtokens,
|
|
169
|
+
cltv_delta: policy.cltv_delta,
|
|
170
|
+
fee_rate: policy.fee_rate,
|
|
171
|
+
from: getIdentity.public_key,
|
|
172
|
+
transaction_id: policy.transaction_id,
|
|
173
|
+
transaction_vout: policy.transaction_vout,
|
|
174
|
+
},
|
|
175
|
+
cbk);
|
|
176
|
+
},
|
|
177
|
+
cbk);
|
|
178
|
+
}],
|
|
179
|
+
|
|
180
|
+
// Capacity change complete
|
|
181
|
+
complete: ['adjustPolicy', ({}, cbk) => {
|
|
182
|
+
logger.info({channel_capacity_successfully_changed: true});
|
|
183
|
+
|
|
184
|
+
return cbk();
|
|
185
|
+
}],
|
|
186
|
+
},
|
|
187
|
+
returnResult({reject, resolve}, cbk));
|
|
188
|
+
});
|
|
189
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const {encodeBigSize} = require('bolt01');
|
|
2
|
+
const {rawChanId} = require('bolt07');
|
|
3
|
+
|
|
4
|
+
const channelFlagsType = '5';
|
|
5
|
+
const channelIdRecordType = '2';
|
|
6
|
+
const decreaseRecordType = '3';
|
|
7
|
+
const increaseRecordType = '4';
|
|
8
|
+
const requestIdRecordType = '1';
|
|
9
|
+
const typeAsHex = type => Buffer.from([type]).toString('hex');
|
|
10
|
+
|
|
11
|
+
/** Encode a request to change a channel capacity
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
channel: <Channel to Change Standard Format Id String>
|
|
15
|
+
[decrease]: <Remove Channel Funds By Tokens Number>
|
|
16
|
+
id: <Request Id Hex String>
|
|
17
|
+
increase: <Add Channel Funds By Tokens Number>
|
|
18
|
+
type: <New Channel Type Number>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@returns
|
|
22
|
+
{
|
|
23
|
+
records: [{
|
|
24
|
+
type: <Record Type Number String>
|
|
25
|
+
value: <Record Value Hex Encoded String>
|
|
26
|
+
}]
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = ({channel, decrease, id, increase, type}) => {
|
|
30
|
+
const records = [
|
|
31
|
+
{
|
|
32
|
+
type: requestIdRecordType,
|
|
33
|
+
value: id,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: channelIdRecordType,
|
|
37
|
+
value: rawChanId({channel}).id,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: channelFlagsType,
|
|
41
|
+
value: typeAsHex(type),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
if (!!decrease) {
|
|
46
|
+
// Add a record reflecting the decrease in the capacity
|
|
47
|
+
records.push({
|
|
48
|
+
type: decreaseRecordType,
|
|
49
|
+
value: encodeBigSize({number: decrease.toString()}).encoded,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!!increase) {
|
|
54
|
+
// Add a record reflecting the increase in the capacity
|
|
55
|
+
records.push({
|
|
56
|
+
type: increaseRecordType,
|
|
57
|
+
value: encodeBigSize({number: increase.toString()}).encoded,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {records};
|
|
62
|
+
};
|