paid-services 3.2.0 → 3.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 +4 -0
- package/capacity/accept_capacity_change.js +284 -0
- package/capacity/ask_for_change_details.js +505 -0
- package/capacity/ask_for_decrease.js +185 -0
- package/capacity/assemble_replacement_tx.js +171 -0
- package/capacity/capacity_change_requests.js +76 -0
- package/capacity/change_channel_capacity.js +183 -0
- package/capacity/encode_change_request.js +55 -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 +98 -0
- package/capacity/get_capacity_replacement.js +371 -0
- package/capacity/get_replacement_signature.js +232 -0
- package/capacity/index.js +4 -0
- package/capacity/initiate_capacity_change.js +506 -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 +101 -0
- package/capacity/parse_sign_capacity_request.js +59 -0
- package/capacity/propose_capacity_change.js +361 -0
- package/capacity/service_types.json +6 -0
- package/capacity/sign_capacity_replacement.js +119 -0
- package/capacity/witness_script_from_close_txs.js +50 -0
- package/index.js +2 -0
- package/p2p/encode_peer_response.js +0 -1
- package/p2p/make_peer_request.js +3 -3
- package/package.json +6 -4
- package/test/capacity/test_assemble_replacement_tx.js +46 -0
- package/test/capacity/test_capacity_change_requests.js +54 -0
- package/test/capacity/test_encode_change_request.js +49 -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 +51 -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 +152 -0
- package/test/integration/test_change_channel_capacity.js +196 -0
- package/test/integration/test_decrease_channel_capacity.js +206 -0
- package/test/integration/test_increase_channel_capacity.js +197 -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/trades/network_from_network_record.js +6 -0
- package/trades/network_record_from_request.js +5 -0
|
@@ -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,76 @@
|
|
|
1
|
+
/** Map change requests to requests considering local channels
|
|
2
|
+
|
|
3
|
+
{
|
|
4
|
+
channels: [{
|
|
5
|
+
capacity: <Channel Capacity Number>
|
|
6
|
+
[cooperative_close_address]: <Cooperative Close Locked Address String>
|
|
7
|
+
id: <Standard Format Channel Id String>
|
|
8
|
+
partner_public_key: <Peer Public Key Hex String>
|
|
9
|
+
}]
|
|
10
|
+
requests: [{
|
|
11
|
+
channel: <Standard Format Channel Id String>
|
|
12
|
+
[decrease]: <Reduce Capacity Tokens Number>
|
|
13
|
+
from: <From Node Public Key Id Hex String>
|
|
14
|
+
id: <Change Request Id Hex String>
|
|
15
|
+
[increase]: <Add Capacity Tokens Number>
|
|
16
|
+
}]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@returns
|
|
20
|
+
{
|
|
21
|
+
requests: [{
|
|
22
|
+
[address]: <Cooperative Close Address String>
|
|
23
|
+
capacity: <Channel Capacity Tokens Number>
|
|
24
|
+
channel: <Standard Format Channel Id String>
|
|
25
|
+
[decrease]: <Reduce Capacity Tokens Number>
|
|
26
|
+
from: <From Node Public Key Id Hex String>
|
|
27
|
+
id: <Change Request Id Hex String>
|
|
28
|
+
[increase]: <Add Capacity Tokens Number>
|
|
29
|
+
}]
|
|
30
|
+
}
|
|
31
|
+
*/
|
|
32
|
+
module.exports = ({channels, requests}) => {
|
|
33
|
+
// Change requests can only come from channels with the messaging peer
|
|
34
|
+
const capacityChangeRequests = requests.filter(request => {
|
|
35
|
+
const channel = channels.find(chan => chan.id === request.channel);
|
|
36
|
+
|
|
37
|
+
// Ignore requests from channels that aren't present
|
|
38
|
+
if (!channel) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The change request must come from the peer in the channel
|
|
43
|
+
return channel.partner_public_key === request.from;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const existing = {};
|
|
47
|
+
|
|
48
|
+
// Only consider one change request per channel
|
|
49
|
+
const changeRequests = capacityChangeRequests
|
|
50
|
+
.filter(({channel}) => {
|
|
51
|
+
// Exit early when the channel already has a change request
|
|
52
|
+
if (!!existing[channel]) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Do not allow more requests for this channel
|
|
57
|
+
existing[channel] = true;
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
})
|
|
61
|
+
.map(request => {
|
|
62
|
+
const channel = channels.find(chan => chan.id === request.channel);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
address: channel.cooperative_close_address,
|
|
66
|
+
capacity: channel.capacity,
|
|
67
|
+
channel: channel.id,
|
|
68
|
+
decrease: request.decrease,
|
|
69
|
+
from: channel.partner_public_key,
|
|
70
|
+
id: request.id,
|
|
71
|
+
increase: request.increase,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {requests: changeRequests};
|
|
76
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
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 interval = 10 * 1000;
|
|
14
|
+
const peerName = ({alias, id}) => `${alias} ${id.substring(0, 8)}`.trim();
|
|
15
|
+
const times = 6 * 60 * 6;
|
|
16
|
+
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
|
|
17
|
+
const waitForCloseInterval = 1000 * 3;
|
|
18
|
+
|
|
19
|
+
/** Change a channel's capacity
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
ask: <Ask Function>
|
|
23
|
+
[delay]: <Wait Time For Incoming Capacity Requests Milliseconds Number>
|
|
24
|
+
lnd: <Authenticated LND API Object>
|
|
25
|
+
logger: <Winston Logger Object>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = ({ask, delay, lnd, logger}, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!ask) {
|
|
34
|
+
return cbk([400, 'ExpectedAskFunctionToChangeCapacity']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!lnd) {
|
|
38
|
+
return cbk([400, 'ExpectedLndToChangeCapacity']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!logger) {
|
|
42
|
+
return cbk([400, 'ExpectedLoggerObjectToChangeCapacity']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return cbk();
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Ask to confirm what will happen in a change capacity
|
|
49
|
+
askToConfirm: ['validate', ({}, cbk) => {
|
|
50
|
+
const details = [
|
|
51
|
+
'This is experimental. Channels may force-close instead of change.',
|
|
52
|
+
'Adding funds currently requires an extra confirmation wait.',
|
|
53
|
+
'Chain fees will be deducted from the changed capacity total.',
|
|
54
|
+
'Chain fees will be paid from the initiator local balance.',
|
|
55
|
+
'Forwards with the peer will be halted during capacity change.',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Show the warnings
|
|
59
|
+
logger.info({details});
|
|
60
|
+
|
|
61
|
+
return ask({type: 'confirm', name: 'ok', message: 'OK?'}, ({ok}) => {
|
|
62
|
+
if (!ok) {
|
|
63
|
+
return cbk([400, 'CanceledChannelCapacityChange']);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return cbk();
|
|
67
|
+
});
|
|
68
|
+
}],
|
|
69
|
+
|
|
70
|
+
// Get the public key of this node
|
|
71
|
+
getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
|
|
72
|
+
|
|
73
|
+
// Wait for an incoming capacity change request
|
|
74
|
+
waitForProposal: ['validate', ({}, cbk) => {
|
|
75
|
+
return getCapacityChangeRequests({delay, lnd}, cbk);
|
|
76
|
+
}],
|
|
77
|
+
|
|
78
|
+
// Ask for acceptance of a channel
|
|
79
|
+
askToSelectChange: [
|
|
80
|
+
'askToConfirm',
|
|
81
|
+
'waitForProposal',
|
|
82
|
+
({waitForProposal}, cbk) =>
|
|
83
|
+
{
|
|
84
|
+
return asyncDetectSeries(waitForProposal.requests, (request, cbk) => {
|
|
85
|
+
return getNodeAlias({lnd, id: request.from}, (err, res) => {
|
|
86
|
+
if (!!err) {
|
|
87
|
+
return cbk(err);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const change = !!request.increase ? 'Increase' : 'Decrease';
|
|
91
|
+
const delta = request.decrease || request.increase;
|
|
92
|
+
const id = request.channel;
|
|
93
|
+
const peer = peerName(res);
|
|
94
|
+
const size = tokensAsBigUnit(request.capacity);
|
|
95
|
+
|
|
96
|
+
const action = `${change} capacity ${size} channel ${id}`;
|
|
97
|
+
const by = !!delta ? ` by ${tokensAsBigUnit(delta)}` : '';
|
|
98
|
+
|
|
99
|
+
return ask({
|
|
100
|
+
type: 'confirm',
|
|
101
|
+
name: 'accept',
|
|
102
|
+
message: `${action} with ${peer}${by}?`,
|
|
103
|
+
},
|
|
104
|
+
({accept}) => {
|
|
105
|
+
if (!accept) {
|
|
106
|
+
return cbk(null, false);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fail changing when cooperative_close_address is set
|
|
110
|
+
if (!!request.address && !!request.increase) {
|
|
111
|
+
return cbk([400, 'ChannelHasLockedCoopCloseAddressSet']);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return cbk(null, true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
cbk);
|
|
119
|
+
}],
|
|
120
|
+
|
|
121
|
+
// Accept a capacity change proposal
|
|
122
|
+
acceptChange: ['askToSelectChange', ({askToSelectChange}, cbk) => {
|
|
123
|
+
// Exit early when there is no change proposal to accept
|
|
124
|
+
if (!askToSelectChange) {
|
|
125
|
+
return cbk();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return acceptCapacityChange({
|
|
129
|
+
lnd,
|
|
130
|
+
logger,
|
|
131
|
+
channel: askToSelectChange.channel,
|
|
132
|
+
from: askToSelectChange.from,
|
|
133
|
+
id: askToSelectChange.id,
|
|
134
|
+
increase: askToSelectChange.increase,
|
|
135
|
+
},
|
|
136
|
+
cbk);
|
|
137
|
+
}],
|
|
138
|
+
|
|
139
|
+
// Initiate a capacity change
|
|
140
|
+
initiateChange: ['askToSelectChange', ({askToSelectChange}, cbk) => {
|
|
141
|
+
if (!!askToSelectChange) {
|
|
142
|
+
return cbk();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return initiateCapacityChange({ask, lnd, logger}, cbk);
|
|
146
|
+
}],
|
|
147
|
+
|
|
148
|
+
// Reset the channel routing policies
|
|
149
|
+
adjustPolicy: [
|
|
150
|
+
'acceptChange',
|
|
151
|
+
'getIdentity',
|
|
152
|
+
'initiateChange',
|
|
153
|
+
({acceptChange, getIdentity, initiateChange}, cbk) =>
|
|
154
|
+
{
|
|
155
|
+
const policy = acceptChange || initiateChange;
|
|
156
|
+
|
|
157
|
+
logger.info({restoring_routing_policy: true});
|
|
158
|
+
|
|
159
|
+
return asyncRetry({interval, times}, cbk => {
|
|
160
|
+
return updateChannelFee({
|
|
161
|
+
lnd,
|
|
162
|
+
base_fee_mtokens: policy.base_fee_mtokens,
|
|
163
|
+
cltv_delta: policy.cltv_delta,
|
|
164
|
+
fee_rate: policy.fee_rate,
|
|
165
|
+
from: getIdentity.public_key,
|
|
166
|
+
transaction_id: policy.transaction_id,
|
|
167
|
+
transaction_vout: policy.transaction_vout,
|
|
168
|
+
},
|
|
169
|
+
cbk);
|
|
170
|
+
},
|
|
171
|
+
cbk);
|
|
172
|
+
}],
|
|
173
|
+
|
|
174
|
+
// Capacity change complete
|
|
175
|
+
complete: ['adjustPolicy', ({}, cbk) => {
|
|
176
|
+
logger.info({channel_capacity_successfully_changed: true});
|
|
177
|
+
|
|
178
|
+
return cbk();
|
|
179
|
+
}],
|
|
180
|
+
},
|
|
181
|
+
returnResult({reject, resolve}, cbk));
|
|
182
|
+
});
|
|
183
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const {encodeBigSize} = require('bolt01');
|
|
2
|
+
const {rawChanId} = require('bolt07');
|
|
3
|
+
|
|
4
|
+
const channelIdRecordType = '2';
|
|
5
|
+
const decreaseRecordType = '3';
|
|
6
|
+
const increaseRecordType = '4';
|
|
7
|
+
const requestIdRecordType = '1';
|
|
8
|
+
|
|
9
|
+
/** Encode a request to change a channel capacity
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
channel: <Channel to Change Standard Format Id String>
|
|
13
|
+
[decrease]: <Remove Channel Funds By Tokens Number>
|
|
14
|
+
id: <Request Id Hex String>
|
|
15
|
+
increase: <Add Channel Funds By Tokens Number>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@returns
|
|
19
|
+
{
|
|
20
|
+
records: [{
|
|
21
|
+
type: <Record Type Number String>
|
|
22
|
+
value: <Record Value Hex Encoded String>
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
*/
|
|
26
|
+
module.exports = ({channel, decrease, id, increase}) => {
|
|
27
|
+
const records = [
|
|
28
|
+
{
|
|
29
|
+
type: requestIdRecordType,
|
|
30
|
+
value: id,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: channelIdRecordType,
|
|
34
|
+
value: rawChanId({channel}).id,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (!!decrease) {
|
|
39
|
+
// Add a record reflecting the decrease in the capacity
|
|
40
|
+
records.push({
|
|
41
|
+
type: decreaseRecordType,
|
|
42
|
+
value: encodeBigSize({number: decrease.toString()}).encoded,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!!increase) {
|
|
47
|
+
// Add a record reflecting the increase in the capacity
|
|
48
|
+
records.push({
|
|
49
|
+
type: increaseRecordType,
|
|
50
|
+
value: encodeBigSize({number: increase.toString()}).encoded,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {records};
|
|
55
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const idType = '0';
|
|
2
|
+
const capacityChangeSignatureType = '1';
|
|
3
|
+
|
|
4
|
+
/** Encode the response to a sign capacity request
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
id: <Capacity Change Id Hex String>
|
|
8
|
+
signature: <Replacement Transaction Signature Hex String>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@returns
|
|
12
|
+
{
|
|
13
|
+
records: [{
|
|
14
|
+
type: <Record Type Number String>
|
|
15
|
+
value: <Record Value Hex Encoded String>
|
|
16
|
+
}]
|
|
17
|
+
}
|
|
18
|
+
*/
|
|
19
|
+
module.exports = ({id, signature}) => {
|
|
20
|
+
return {
|
|
21
|
+
records: [
|
|
22
|
+
{
|
|
23
|
+
type: idType,
|
|
24
|
+
value: id,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: capacityChangeSignatureType,
|
|
28
|
+
value: signature,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
2
|
+
|
|
3
|
+
const {ceil} = Math;
|
|
4
|
+
const decreaseBuffer = 44;
|
|
5
|
+
const dummyHash = Buffer.alloc(32);
|
|
6
|
+
const dummyIncreaseIndex = 1;
|
|
7
|
+
const dummyIndex = 0;
|
|
8
|
+
const dummyMultiSigScript = Buffer.alloc(71);
|
|
9
|
+
const dummyNullDummy = Buffer.alloc(1);
|
|
10
|
+
const dummyP2wsh = Buffer.alloc(68);
|
|
11
|
+
const dummyPublicKey = Buffer.alloc(33);
|
|
12
|
+
const dummySignature = Buffer.alloc(74);
|
|
13
|
+
const dummyTokens = 0;
|
|
14
|
+
const dummyVout = 0;
|
|
15
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
16
|
+
const increaseBuffer = 150;
|
|
17
|
+
const {max} = Math;
|
|
18
|
+
const weightAsVBytes = weight => weight / 4;
|
|
19
|
+
|
|
20
|
+
/** Calculate the chain fee required for the capacity replacement tx
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
capacity: <Original Channel Capacity Tokens Number>
|
|
24
|
+
commit_transaction_fee: <Commit Transaction Fee Tokens Number>
|
|
25
|
+
commit_transaction_weight: <Commit Transaction Weight Units Number>
|
|
26
|
+
decrease: [{
|
|
27
|
+
output: <Output Script Hex String>
|
|
28
|
+
tokens: <Spend Value to Chain Address Tokens Number>
|
|
29
|
+
}]
|
|
30
|
+
[increase]: <Add Funds Tokens Number>
|
|
31
|
+
tokens_per_vbyte: <Minimal Fee Rate Tokens Per VByte Number>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@returns
|
|
35
|
+
{
|
|
36
|
+
fee: <Estimated Replacement Chain Fee Tokens Number>
|
|
37
|
+
}
|
|
38
|
+
*/
|
|
39
|
+
module.exports = args => {
|
|
40
|
+
// The cost to close is also known as the commit fee
|
|
41
|
+
const closeTransactionFee = args.commit_transaction_fee;
|
|
42
|
+
|
|
43
|
+
// The commitment transaction size will have to be paid for
|
|
44
|
+
const replaceSize = weightAsVBytes(args.commit_transaction_weight);
|
|
45
|
+
|
|
46
|
+
// Calculate the rate that the commitment tx is paying
|
|
47
|
+
const commitRate = closeTransactionFee / replaceSize;
|
|
48
|
+
|
|
49
|
+
// Create a dummy transaction which models the replacement
|
|
50
|
+
const tx = new Transaction();
|
|
51
|
+
|
|
52
|
+
// Add the decreases as outputs
|
|
53
|
+
args.decrease.forEach(({output, tokens}) => {
|
|
54
|
+
return tx.addOutput(hexAsBuffer(output), tokens);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Add an output to represent the new channel output
|
|
58
|
+
tx.addOutput(dummyP2wsh, args.capacity);
|
|
59
|
+
|
|
60
|
+
args.decrease.forEach(({output}) => {
|
|
61
|
+
return tx.addOutput(hexAsBuffer(output), dummyTokens);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Add an input to represent the old channel input
|
|
65
|
+
tx.addInput(dummyHash, dummyVout);
|
|
66
|
+
|
|
67
|
+
// Spending a channel close requires both signatures
|
|
68
|
+
const dummyWitnessStack = [
|
|
69
|
+
dummyNullDummy,
|
|
70
|
+
dummySignature,
|
|
71
|
+
dummySignature,
|
|
72
|
+
dummyMultiSigScript,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Add a witness to the input
|
|
76
|
+
tx.setWitness(dummyIndex, dummyWitnessStack);
|
|
77
|
+
|
|
78
|
+
// An increase means that there is an additional input
|
|
79
|
+
if (!!args.increase) {
|
|
80
|
+
tx.addInput(dummyHash, dummyVout);
|
|
81
|
+
|
|
82
|
+
// Add funds input spends from transit, which means signature + public key
|
|
83
|
+
tx.setWitness(dummyIncreaseIndex, [dummySignature, dummyPublicKey]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Add additional size for an additional input
|
|
87
|
+
const inputBuffer = !!args.increase ? increaseBuffer : dummyTokens;
|
|
88
|
+
|
|
89
|
+
// Add additional size for additional outputs
|
|
90
|
+
const outputBuffer = args.decrease.length * decreaseBuffer;
|
|
91
|
+
|
|
92
|
+
// Now that we have all inputs and outputs we can measure the virtual bytes
|
|
93
|
+
const size = tx.virtualSize() + inputBuffer + outputBuffer;
|
|
94
|
+
|
|
95
|
+
// The fee for the replacement tx must be above a minimum
|
|
96
|
+
const minFee = ceil(size * commitRate);
|
|
97
|
+
|
|
98
|
+
// The base fee for replacement is the fee of the replacement
|
|
99
|
+
const baseFee = args.commit_transaction_fee;
|
|
100
|
+
|
|
101
|
+
// There is an additional fee which is a relay fee to pay for the replaced tx
|
|
102
|
+
const deltaFee = ceil(replaceSize * args.tokens_per_vbyte);
|
|
103
|
+
|
|
104
|
+
// The new tx pays either min replacement fee or the min overall relay fee
|
|
105
|
+
const fee = max(minFee, closeTransactionFee + deltaFee);
|
|
106
|
+
|
|
107
|
+
return {fee};
|
|
108
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const {script} = require('bitcoinjs-lib');
|
|
2
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
3
|
+
|
|
4
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
5
|
+
const {decompile} = script;
|
|
6
|
+
const encodeSig = (n, flag) => Buffer.concat([Buffer.from(n, 'hex'), flag]);
|
|
7
|
+
const {fromHex} = Transaction;
|
|
8
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
9
|
+
const {isBuffer} = Buffer;
|
|
10
|
+
const nullDummyStackItem = Buffer.alloc(0);
|
|
11
|
+
const signatureFlag = Buffer.from([Transaction.SIGHASH_ALL]);
|
|
12
|
+
|
|
13
|
+
/** Add signaturess to a capacity replacement transaction
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
[add_funds_vin]: <Add Funds Input Index>
|
|
17
|
+
[add_funds_public_key]: <Add Funds Transit Public Key Hex String>
|
|
18
|
+
[add_funds_signature]: <Add Funds Signature Hex String>
|
|
19
|
+
local_public_key: <Local MultiSig Public Key Hex String>
|
|
20
|
+
local_signature: <Local Replacement Signature Hex String>
|
|
21
|
+
funding_spend_vin: <Funding Transaction Output Spend Input Index Number>
|
|
22
|
+
remote_signature: <Peer Signature Hex String>
|
|
23
|
+
transaction: <Unsigned Replacement Transaction Hex String>
|
|
24
|
+
witness_script: <Funding Witness Script Hex Encoded String>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@returns
|
|
28
|
+
{
|
|
29
|
+
transaction: <Fully Signed Transaction Hex String>
|
|
30
|
+
}
|
|
31
|
+
*/
|
|
32
|
+
module.exports = args => {
|
|
33
|
+
const funding = hexAsBuffer(args.witness_script);
|
|
34
|
+
const tx = fromHex(args.transaction);
|
|
35
|
+
|
|
36
|
+
const keys = decompile(funding).filter(isBuffer).map(bufferAsHex);
|
|
37
|
+
|
|
38
|
+
const signatures = [,];
|
|
39
|
+
|
|
40
|
+
// Find the indices to place the signatures
|
|
41
|
+
const localKeyIndex = keys.findIndex(n => n === args.local_public_key);
|
|
42
|
+
const remoteKeyIndex = keys.findIndex(n => n !== args.local_public_key);
|
|
43
|
+
|
|
44
|
+
// Add the local signature to the funding witness stack
|
|
45
|
+
signatures[localKeyIndex] = encodeSig(args.local_signature, signatureFlag);
|
|
46
|
+
|
|
47
|
+
// Add the remote signature to the funding witness stack
|
|
48
|
+
signatures[remoteKeyIndex] = encodeSig(args.remote_signature, signatureFlag);
|
|
49
|
+
|
|
50
|
+
const witness = []
|
|
51
|
+
.concat([nullDummyStackItem])
|
|
52
|
+
.concat(signatures)
|
|
53
|
+
.concat([funding]);
|
|
54
|
+
|
|
55
|
+
// Fill in the witness stack including the close spend
|
|
56
|
+
tx.setWitness(args.funding_spend_vin, witness);
|
|
57
|
+
|
|
58
|
+
// The increase in funds also needs a signature added
|
|
59
|
+
if (!!args.add_funds_signature) {
|
|
60
|
+
tx.setWitness(args.add_funds_vin, [
|
|
61
|
+
encodeSig(args.add_funds_signature, signatureFlag),
|
|
62
|
+
hexAsBuffer(args.add_funds_public_key),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {transaction: tx.toHex()};
|
|
67
|
+
};
|