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.
Files changed (52) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/capacity/accept_capacity_change.js +284 -0
  3. package/capacity/ask_for_change_details.js +505 -0
  4. package/capacity/ask_for_decrease.js +185 -0
  5. package/capacity/assemble_replacement_tx.js +171 -0
  6. package/capacity/capacity_change_requests.js +76 -0
  7. package/capacity/change_channel_capacity.js +183 -0
  8. package/capacity/encode_change_request.js +55 -0
  9. package/capacity/encode_sign_capacity_response.js +32 -0
  10. package/capacity/fee_for_replacement.js +108 -0
  11. package/capacity/finalize_capacity_replacement.js +67 -0
  12. package/capacity/get_capacity_change_requests.js +98 -0
  13. package/capacity/get_capacity_replacement.js +371 -0
  14. package/capacity/get_replacement_signature.js +232 -0
  15. package/capacity/index.js +4 -0
  16. package/capacity/initiate_capacity_change.js +506 -0
  17. package/capacity/interim_replacement_psbt.js +57 -0
  18. package/capacity/is_open_witness_script.js +40 -0
  19. package/capacity/parse_accept_request.js +58 -0
  20. package/capacity/parse_accept_response.js +52 -0
  21. package/capacity/parse_capacity_change_request.js +101 -0
  22. package/capacity/parse_sign_capacity_request.js +59 -0
  23. package/capacity/propose_capacity_change.js +361 -0
  24. package/capacity/service_types.json +6 -0
  25. package/capacity/sign_capacity_replacement.js +119 -0
  26. package/capacity/witness_script_from_close_txs.js +50 -0
  27. package/index.js +2 -0
  28. package/p2p/encode_peer_response.js +0 -1
  29. package/p2p/make_peer_request.js +3 -3
  30. package/package.json +6 -4
  31. package/test/capacity/test_assemble_replacement_tx.js +46 -0
  32. package/test/capacity/test_capacity_change_requests.js +54 -0
  33. package/test/capacity/test_encode_change_request.js +49 -0
  34. package/test/capacity/test_encode_sign_capacity_response.js +47 -0
  35. package/test/capacity/test_fee_for_replacement.js +40 -0
  36. package/test/capacity/test_finalize_capacity_replacement.js +45 -0
  37. package/test/capacity/test_interim_replacement_psbt.js +44 -0
  38. package/test/capacity/test_is_open_witness_script.js +40 -0
  39. package/test/capacity/test_parse_accept_request.js +42 -0
  40. package/test/capacity/test_parse_capacity_change_request.js +51 -0
  41. package/test/capacity/test_parse_sign_capacity_request.js +49 -0
  42. package/test/capacity/test_witness_script_from_close_txs.js +53 -0
  43. package/test/integration/test_accept_capacity_change.js +152 -0
  44. package/test/integration/test_change_channel_capacity.js +196 -0
  45. package/test/integration/test_decrease_channel_capacity.js +206 -0
  46. package/test/integration/test_increase_channel_capacity.js +197 -0
  47. package/test/integration/test_sign_capacity_replacement.js +142 -0
  48. package/test/p2p/test_encode_peer_request.js +53 -0
  49. package/test/p2p/test_encode_peer_response.js +38 -0
  50. package/test/p2p/test_encode_response_code.js +48 -0
  51. package/trades/network_from_network_record.js +6 -0
  52. 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
+ };