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,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
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {getChannels} = require('ln-service');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
const {subscribeToPeerMessages} = require('ln-service');
|
|
5
|
+
|
|
6
|
+
const capacityChangeRequests = require('./capacity_change_requests');
|
|
7
|
+
const {serviceTypeChangeCapacity} = require('./../service_types');
|
|
8
|
+
const parseCapacityChangeRequest = require('./parse_capacity_change_request');
|
|
9
|
+
const {servicePeerRequests} = require('./../p2p');
|
|
10
|
+
|
|
11
|
+
const defaultDelayMs = 1000 * 10;
|
|
12
|
+
const waitForRequestsTimeoutMs = 5000;
|
|
13
|
+
|
|
14
|
+
/** Get capacity change requests
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
[delay]: <Milliseconds To Wait For Change Requests Number>
|
|
18
|
+
lnd: <Authenticated LND API Object>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@returns via cbk or Promise
|
|
22
|
+
{
|
|
23
|
+
requests: [{
|
|
24
|
+
[address]: <Cooperative Close Address String>
|
|
25
|
+
capacity: <Channel Capacity Tokens Number>
|
|
26
|
+
channel: <Standard Format Channel Id String>
|
|
27
|
+
[decrease]: <Reduce Capacity Tokens Number>
|
|
28
|
+
from: <From Node Public Key Id Hex String>
|
|
29
|
+
id: <Change Request Id Hex String>
|
|
30
|
+
[increase]: <Add Capacity Tokens Number>
|
|
31
|
+
[type]: <Intended Replacement Channel Channel Type Flags Number>
|
|
32
|
+
}]
|
|
33
|
+
}
|
|
34
|
+
*/
|
|
35
|
+
module.exports = ({delay, lnd}, cbk) => {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
return asyncAuto({
|
|
38
|
+
// Check arguments
|
|
39
|
+
validate: cbk => {
|
|
40
|
+
if (!lnd) {
|
|
41
|
+
return cbk([400, 'ExpectedLndToGetCapacityChangeRequests']);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return cbk();
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Wait for incoming capacity change requests
|
|
48
|
+
getRequests: ['validate', ({}, cbk) => {
|
|
49
|
+
const requests = [];
|
|
50
|
+
const service = servicePeerRequests({lnd});
|
|
51
|
+
|
|
52
|
+
// Listen for capacity change requests
|
|
53
|
+
service.request({type: serviceTypeChangeCapacity}, (req, res) => {
|
|
54
|
+
const {from, records} = req;
|
|
55
|
+
|
|
56
|
+
const {request} = parseCapacityChangeRequest({from, records});
|
|
57
|
+
|
|
58
|
+
// Exit early when the change request message was not understood
|
|
59
|
+
if (!request) {
|
|
60
|
+
return res.failure([400, 'FailedToParseChangeRequest']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Only add new requests
|
|
64
|
+
if (!requests.find(n => n.id === request.id)) {
|
|
65
|
+
requests.push(request);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return res.success({});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return setTimeout(() => {
|
|
72
|
+
// Stop listening for requests after a little while
|
|
73
|
+
service.stop({});
|
|
74
|
+
|
|
75
|
+
return cbk(null, requests);
|
|
76
|
+
},
|
|
77
|
+
delay || waitForRequestsTimeoutMs);
|
|
78
|
+
}],
|
|
79
|
+
|
|
80
|
+
// Get the set of channels
|
|
81
|
+
getChannels: ['getRequests', ({}, cbk) => getChannels({lnd}, cbk)],
|
|
82
|
+
|
|
83
|
+
// Final set of capacity change requests
|
|
84
|
+
requests: [
|
|
85
|
+
'getChannels',
|
|
86
|
+
'getRequests',
|
|
87
|
+
({getChannels, getRequests}, cbk) =>
|
|
88
|
+
{
|
|
89
|
+
const {requests} = capacityChangeRequests({
|
|
90
|
+
channels: getChannels.channels,
|
|
91
|
+
requests: getRequests,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return cbk(null, {requests});
|
|
95
|
+
}],
|
|
96
|
+
},
|
|
97
|
+
returnResult({reject, resolve, of: 'requests'}, cbk));
|
|
98
|
+
});
|
|
99
|
+
};
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {closeChannel} = require('ln-service');
|
|
3
|
+
const {decodeChanId} = require('bolt07');
|
|
4
|
+
const {getChainFeeRate} = require('ln-service');
|
|
5
|
+
const {getChainTransactions} = require('ln-service');
|
|
6
|
+
const {getHeight} = require('ln-service');
|
|
7
|
+
const {getPendingChannels} = require('ln-service');
|
|
8
|
+
const {openChannels} = require('ln-service');
|
|
9
|
+
const {returnResult} = require('asyncjs-util');
|
|
10
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
11
|
+
|
|
12
|
+
const assembleReplacementTx = require('./assemble_replacement_tx');
|
|
13
|
+
const feeForReplacement = require('./fee_for_replacement');
|
|
14
|
+
const signCapacityReplacement = require('./sign_capacity_replacement');
|
|
15
|
+
|
|
16
|
+
const blocksBuffer = 2e3;
|
|
17
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
18
|
+
const defaultIncrease = 0;
|
|
19
|
+
const {fromHex} = Transaction;
|
|
20
|
+
const fuzzBlocks = 10;
|
|
21
|
+
const highValue = 3000;
|
|
22
|
+
const {isArray} = Array;
|
|
23
|
+
const maxCommitmentOutputs = 4;
|
|
24
|
+
const maxHighValueOutputs = 2;
|
|
25
|
+
const notFoundIndex = -1;
|
|
26
|
+
const positive = n => Math.max(1, n);
|
|
27
|
+
const slowTarget = 1000;
|
|
28
|
+
const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
|
|
29
|
+
const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
|
|
30
|
+
|
|
31
|
+
/** Close a channel and get a capacity replacement transaction
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
[add_funds_transaction_id]: <Adding Funds Input Tx Id Hex Encoded String>
|
|
35
|
+
[add_funds_transaction_vout]: <Adding Funds Output Index Number>
|
|
36
|
+
bitcoinjs_network: <BitcoinJs Network Name String>
|
|
37
|
+
decrease: [{
|
|
38
|
+
address: <Spend to Chain Address String>
|
|
39
|
+
output: <Spend to Output Script Hex String>
|
|
40
|
+
tokens: <Spend Value Number>
|
|
41
|
+
}]
|
|
42
|
+
id: <Original Channel Standard Format Channel Id String>
|
|
43
|
+
[increase]: <Increase Channel Size Tokens Number>
|
|
44
|
+
[is_private]: <Original Channel Is Private Bool>
|
|
45
|
+
lnd: <Authenticated LND API Object>
|
|
46
|
+
transaction_id: <Original Funding Transaction Id Hex String>
|
|
47
|
+
transaction_vout: <Original Funding Output Index Number>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@returns via cbk or Promise
|
|
51
|
+
{
|
|
52
|
+
[add_funds_vin]: <Funding Spend Input Index Number>
|
|
53
|
+
capacity: <Replacement Channel Capacity Tokens Number>
|
|
54
|
+
remote_balance: <Initial Balance for Peer Tokens Number>
|
|
55
|
+
signature: <Signature for Replacement Transaction Hex Encoded String>
|
|
56
|
+
signing_key: <Signing Public Key Hex String>
|
|
57
|
+
pending_channel_id: <Proposed Channel Pending Id Hex String>
|
|
58
|
+
transaction_id: <Replacement Channel Transaction Id Hex String>
|
|
59
|
+
transaction_vin: <Original Channel Input Index Number>
|
|
60
|
+
transaction_vout: <Replacement Channel Transaction Output Index Number>
|
|
61
|
+
unsigned_transaction: <Unsigned Transaction Hex Encoded String>
|
|
62
|
+
witness_script: <Original Channel Witness Script Hex String>
|
|
63
|
+
}
|
|
64
|
+
*/
|
|
65
|
+
module.exports = (args, cbk) => {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
return asyncAuto({
|
|
68
|
+
// Check arguments
|
|
69
|
+
validate: cbk => {
|
|
70
|
+
if (!args.bitcoinjs_network) {
|
|
71
|
+
return cbk([400, 'ExpectedNetworkNameToGetCapacityReplacement']);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isArray(args.decrease)) {
|
|
75
|
+
return cbk([400, 'ExpectedDecreaseArrayToGetCapacityReplacement']);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!args.lnd) {
|
|
79
|
+
return cbk([400, 'ExpectedLndApiToGetCapacityReplacement']);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!args.transaction_id) {
|
|
83
|
+
return cbk([400, 'ExpectedFundingTxIdToGetCapacityReplacement']);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (args.transaction_vout === undefined) {
|
|
87
|
+
return cbk([400, 'ExpectedFundingOutIndexToGetCapacityReplacement']);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return cbk();
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Get the chain fee rate for a slow confirmation to calculate fee delta
|
|
94
|
+
getFeeRate: ['validate', ({}, cbk) => {
|
|
95
|
+
return getChainFeeRate({
|
|
96
|
+
confirmation_target: slowTarget,
|
|
97
|
+
lnd: args.lnd,
|
|
98
|
+
},
|
|
99
|
+
cbk);
|
|
100
|
+
}],
|
|
101
|
+
|
|
102
|
+
// Get the current chain height
|
|
103
|
+
getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
|
|
104
|
+
|
|
105
|
+
// Force close the channel in order to get a non-final sequence number
|
|
106
|
+
channelClose: ['getFeeRate', ({}, cbk) => {
|
|
107
|
+
return closeChannel({
|
|
108
|
+
lnd: args.lnd,
|
|
109
|
+
is_force_close: true,
|
|
110
|
+
transaction_id: args.transaction_id,
|
|
111
|
+
transaction_vout: args.transaction_vout,
|
|
112
|
+
},
|
|
113
|
+
cbk);
|
|
114
|
+
}],
|
|
115
|
+
|
|
116
|
+
// Get the pending channels
|
|
117
|
+
getPending: ['channelClose', ({}, cbk) => {
|
|
118
|
+
return getPendingChannels({lnd: args.lnd}, cbk);
|
|
119
|
+
}],
|
|
120
|
+
|
|
121
|
+
// Find the pending closing channel
|
|
122
|
+
pendingChannel: ['getPending', ({getPending}, cbk) => {
|
|
123
|
+
const channel = getPending.pending_channels.find(chan => {
|
|
124
|
+
if (chan.transaction_id !== args.transaction_id) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return chan.transaction_vout === args.transaction_vout;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!channel) {
|
|
132
|
+
return cbk([503, 'FailedToFindPendingCloseChannelForReplacement']);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!!channel.pending_payments && !!channel.pending_payments.length) {
|
|
136
|
+
return cbk([503, 'ExpectedNoActiveHtlcsOnCapacityChangeChannel']);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return cbk(null, {
|
|
140
|
+
capacity: channel.capacity,
|
|
141
|
+
partner_public_key: channel.partner_public_key,
|
|
142
|
+
remote_balance: channel.remote_balance,
|
|
143
|
+
});
|
|
144
|
+
}],
|
|
145
|
+
|
|
146
|
+
// Get the chain transactions to pull out the raw close tx
|
|
147
|
+
getUpdatedTx: ['channelClose', 'getHeight', ({getHeight}, cbk) => {
|
|
148
|
+
return getChainTransactions({
|
|
149
|
+
after: getHeight.current_block_height - fuzzBlocks,
|
|
150
|
+
lnd: args.lnd,
|
|
151
|
+
},
|
|
152
|
+
cbk);
|
|
153
|
+
}],
|
|
154
|
+
|
|
155
|
+
// Find the force close transaction
|
|
156
|
+
closeTx: [
|
|
157
|
+
'channelClose',
|
|
158
|
+
'getUpdatedTx',
|
|
159
|
+
'pendingChannel',
|
|
160
|
+
({channelClose, getUpdatedTx, pendingChannel}, cbk) =>
|
|
161
|
+
{
|
|
162
|
+
const closeId = channelClose.transaction_id;
|
|
163
|
+
const id = args.transaction_id;
|
|
164
|
+
const {capacity} = pendingChannel;
|
|
165
|
+
|
|
166
|
+
const tx = getUpdatedTx.transactions.find(n => n.id === closeId);
|
|
167
|
+
|
|
168
|
+
if (!tx || !tx.transaction) {
|
|
169
|
+
return cbk([503, 'ExpectedCloseTransactionInTransactionsList']);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const commitment = fromHex(tx.transaction);
|
|
173
|
+
|
|
174
|
+
// Make sure there aren't unexpectedly too-many outputs
|
|
175
|
+
if (commitment.outs.length > maxCommitmentOutputs) {
|
|
176
|
+
return cbk([503, 'ExpectedFewerOutputsOnCommitmentTransaction']);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const highValueOuts = commitment.outs.filter(n => n.value > highValue);
|
|
180
|
+
|
|
181
|
+
// There shouldn't be many high value outputs
|
|
182
|
+
if (highValueOuts.length > maxHighValueOutputs) {
|
|
183
|
+
return cbk([500, 'ExpectedFewerHighValueOutputsOnCommitTx']);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Input details for the commitment transaction
|
|
187
|
+
const [{hash, index, witness}, other] = commitment.ins;
|
|
188
|
+
|
|
189
|
+
// The commitment transaction should be spending the funding outpoint
|
|
190
|
+
if (!hash.equals(txIdAsHash(id)) || index !== args.transaction_vout) {
|
|
191
|
+
return cbk([503, 'ExpectedSpendingOpenTransactionOutputIndex']);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// There should be no other inputs on the closing tx
|
|
195
|
+
if (!!other) {
|
|
196
|
+
return cbk([503, 'ExpectedSingleInputForCloseTransaction']);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The spending script is the final element in the witness
|
|
200
|
+
const [fundingScript] = witness.slice().reverse();
|
|
201
|
+
|
|
202
|
+
// The difference between commitment outputs and its input is the fee
|
|
203
|
+
const fee = capacity - sumOf(commitment.outs.map(n => n.value));
|
|
204
|
+
|
|
205
|
+
return cbk(null, {
|
|
206
|
+
fee,
|
|
207
|
+
id: commitment.getId(),
|
|
208
|
+
script: bufferAsHex(fundingScript),
|
|
209
|
+
transaction: tx.transaction,
|
|
210
|
+
weight: commitment.weight(),
|
|
211
|
+
});
|
|
212
|
+
}],
|
|
213
|
+
|
|
214
|
+
// Find the open transaction
|
|
215
|
+
openTx: ['getUpdatedTx', ({getUpdatedTx}, cbk) => {
|
|
216
|
+
const funding = fromHex(args.open_transaction);
|
|
217
|
+
const id = args.transaction_id;
|
|
218
|
+
|
|
219
|
+
if (!funding.outs[args.transaction_vout]) {
|
|
220
|
+
return cbk([503, 'ExpectedTransactionOutputOnFundingTransaction']);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const capacity = funding.outs[args.transaction_vout].value;
|
|
224
|
+
const output = bufferAsHex(funding.outs[args.transaction_vout].script);
|
|
225
|
+
|
|
226
|
+
return cbk(null, {capacity, output});
|
|
227
|
+
}],
|
|
228
|
+
|
|
229
|
+
// Calculate the new channel's capacity
|
|
230
|
+
newCapacity: [
|
|
231
|
+
'closeTx',
|
|
232
|
+
'getFeeRate',
|
|
233
|
+
'pendingChannel',
|
|
234
|
+
({closeTx, getFeeRate, pendingChannel}, cbk) =>
|
|
235
|
+
{
|
|
236
|
+
// The fee for the replacement tx must meet the relay rate minimum
|
|
237
|
+
const {fee} = feeForReplacement({
|
|
238
|
+
capacity: pendingChannel.capacity,
|
|
239
|
+
commit_transaction_fee: closeTx.fee,
|
|
240
|
+
commit_transaction_weight: closeTx.weight,
|
|
241
|
+
decrease: args.decrease,
|
|
242
|
+
increase: args.increase,
|
|
243
|
+
tokens_per_vbyte: getFeeRate.tokens_per_vbyte,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// The new capacity is capacity + increase - decreases - tx fee
|
|
247
|
+
const newCapacity = sumOf([
|
|
248
|
+
pendingChannel.capacity,
|
|
249
|
+
args.increase || defaultIncrease,
|
|
250
|
+
-sumOf(args.decrease.map(n => n.tokens)),
|
|
251
|
+
-fee,
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
return cbk(null, newCapacity);
|
|
255
|
+
}],
|
|
256
|
+
|
|
257
|
+
// Propose the new channel to replace the existing one
|
|
258
|
+
proposeChannel: [
|
|
259
|
+
'newCapacity',
|
|
260
|
+
'pendingChannel',
|
|
261
|
+
({newCapacity, pendingChannel}, cbk) =>
|
|
262
|
+
{
|
|
263
|
+
return openChannels({
|
|
264
|
+
channels: [{
|
|
265
|
+
capacity: newCapacity,
|
|
266
|
+
give_tokens: pendingChannel.remote_balance,
|
|
267
|
+
is_private: args.is_private,
|
|
268
|
+
partner_public_key: pendingChannel.partner_public_key,
|
|
269
|
+
is_private: args.is_private,
|
|
270
|
+
}],
|
|
271
|
+
is_avoiding_broadcast: true,
|
|
272
|
+
lnd: args.lnd,
|
|
273
|
+
},
|
|
274
|
+
cbk);
|
|
275
|
+
}],
|
|
276
|
+
|
|
277
|
+
// Put together the unsigned replacement transaction
|
|
278
|
+
replacementTx: [
|
|
279
|
+
'closeTx',
|
|
280
|
+
'newCapacity',
|
|
281
|
+
'proposeChannel',
|
|
282
|
+
({closeTx, newCapacity, proposeChannel}, cbk) =>
|
|
283
|
+
{
|
|
284
|
+
// The replacement tx pays to the proposed channel multisig address
|
|
285
|
+
const [{address}] = proposeChannel.pending;
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const replacement = assembleReplacementTx({
|
|
289
|
+
add_funds_transaction_id: args.add_funds_transaction_id,
|
|
290
|
+
add_funds_transaction_vout: args.add_funds_transaction_vout,
|
|
291
|
+
bitcoinjs_network: args.bitcoinjs_network,
|
|
292
|
+
close_transaction: closeTx.transaction,
|
|
293
|
+
decrease: args.decrease.map(decrease => ({
|
|
294
|
+
output: decrease.output,
|
|
295
|
+
tokens: decrease.tokens,
|
|
296
|
+
})),
|
|
297
|
+
funding_address: address,
|
|
298
|
+
funding_tokens: newCapacity,
|
|
299
|
+
transaction_id: args.transaction_id,
|
|
300
|
+
transaction_vout: args.transaction_vout,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return cbk(null, {
|
|
304
|
+
add_funds_vin: replacement.add_funds_vin,
|
|
305
|
+
transaction: replacement.transaction,
|
|
306
|
+
transaction_vin: replacement.transaction_vin,
|
|
307
|
+
transaction_id: replacement.transaction_id,
|
|
308
|
+
transaction_vout: replacement.transaction_vout,
|
|
309
|
+
});
|
|
310
|
+
} catch (err) {
|
|
311
|
+
return cbk([503, err.message]);
|
|
312
|
+
}
|
|
313
|
+
}],
|
|
314
|
+
|
|
315
|
+
// Partially sign the replacement transaction using the 2:2 key
|
|
316
|
+
signReplacement: [
|
|
317
|
+
'closeTx',
|
|
318
|
+
'openTx',
|
|
319
|
+
'pendingChannel',
|
|
320
|
+
'replacementTx',
|
|
321
|
+
({closeTx, openTx, pendingChannel, replacementTx}, cbk) =>
|
|
322
|
+
{
|
|
323
|
+
return signCapacityReplacement({
|
|
324
|
+
capacity: openTx.capacity,
|
|
325
|
+
lnd: args.lnd,
|
|
326
|
+
output: openTx.output,
|
|
327
|
+
script: closeTx.script,
|
|
328
|
+
transaction: replacementTx.transaction,
|
|
329
|
+
vin: replacementTx.transaction_vin,
|
|
330
|
+
},
|
|
331
|
+
cbk);
|
|
332
|
+
}],
|
|
333
|
+
|
|
334
|
+
// Details of proposed replacement
|
|
335
|
+
proposedReplacement: [
|
|
336
|
+
'closeTx',
|
|
337
|
+
'newCapacity',
|
|
338
|
+
'pendingChannel',
|
|
339
|
+
'proposeChannel',
|
|
340
|
+
'replacementTx',
|
|
341
|
+
'signReplacement',
|
|
342
|
+
({
|
|
343
|
+
closeTx,
|
|
344
|
+
newCapacity,
|
|
345
|
+
pendingChannel,
|
|
346
|
+
proposeChannel,
|
|
347
|
+
replacementTx,
|
|
348
|
+
signReplacement,
|
|
349
|
+
},
|
|
350
|
+
cbk) =>
|
|
351
|
+
{
|
|
352
|
+
const [pending] = proposeChannel.pending;
|
|
353
|
+
|
|
354
|
+
return cbk(null, {
|
|
355
|
+
add_funds_vin: replacementTx.add_funds_vin,
|
|
356
|
+
capacity: newCapacity,
|
|
357
|
+
force_close_tx_id: closeTx.id,
|
|
358
|
+
remote_balance: pendingChannel.remote_balance,
|
|
359
|
+
signature: signReplacement.signature,
|
|
360
|
+
signing_key: signReplacement.key,
|
|
361
|
+
pending_channel_id: pending.id,
|
|
362
|
+
transaction_id: replacementTx.transaction_id,
|
|
363
|
+
transaction_vin: replacementTx.transaction_vin,
|
|
364
|
+
transaction_vout: replacementTx.transaction_vout,
|
|
365
|
+
unsigned_transaction: replacementTx.transaction,
|
|
366
|
+
witness_script: closeTx.script,
|
|
367
|
+
});
|
|
368
|
+
}],
|
|
369
|
+
},
|
|
370
|
+
returnResult({reject, resolve, of: 'proposedReplacement'}, cbk));
|
|
371
|
+
});
|
|
372
|
+
};
|