paid-services 3.11.1 → 3.12.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 CHANGED
@@ -1,6 +1,10 @@
1
1
  # Versions
2
2
 
3
- ## Version 3.11.1
3
+ ## Version 3.12.0
4
+
5
+ - `balancedOpenRequest`: Add method to derive balanced open proposal details
6
+
7
+ ## Version 3.11.4
4
8
 
5
9
  - `changeChannelCapacity`: Add `nodes` to allow moving channel to another node
6
10
 
@@ -0,0 +1,155 @@
1
+ const {parsePaymentRequest} = require('invoices');
2
+
3
+ const {balancedChannelKeyTypes} = require('./service_key_types');
4
+
5
+ const expectedRequestMtokens = '10000';
6
+ const expectedResponseMtokens = '1000';
7
+ const hexAsUtf8 = hex => Buffer.from(hex, 'hex').toString();
8
+ const isHexHashSized = hex => hex.length === 64;
9
+ const isHexNumberSized = hex => hex.length < 14;
10
+ const isOdd = n => n % 2;
11
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
12
+ const parseHexNumber = hex => parseInt(hex, 16);
13
+
14
+ /** Derive a balanced open request from a received payment
15
+
16
+ {
17
+ confirmed_at: <Received Request At ISO 8601 Date String>
18
+ is_push: <Received Funds as Push Payment Bool>
19
+ payments: [{
20
+ messages: [{
21
+ type: <Message Record TLV Type String>
22
+ value: <Message Record Value Hex String>
23
+ }]
24
+ }]
25
+ received_mtokens: <Received Millitokens String>
26
+ }
27
+
28
+ @returns
29
+ {
30
+ [proposal]: {
31
+ accept_request: request,
32
+ capacity: parseHexNumber(channelCapacity.value),
33
+ fee_rate: parseHexNumber(fundingFeeRate.value),
34
+ partner_public_key: destination,
35
+ proposed_at: invoice.confirmed_at,
36
+ remote_multisig_key: remoteMultiSigKey.value,
37
+ remote_tx_id: remoteTxId.value,
38
+ remote_tx_vout: parseHexNumber(remoteTxVout.value),
39
+ }
40
+ }
41
+ */
42
+ module.exports = args => {
43
+ // Exit early when not receiving a push of the proposal amount
44
+ if (!args.is_push || args.received_mtokens !== expectedRequestMtokens) {
45
+ return {}
46
+ }
47
+
48
+ const payment = args.payments.find(payment => {
49
+ return !!payment.messages.find(({type}) => {
50
+ return type === balancedChannelKeyTypes.accept_request;
51
+ });
52
+ });
53
+
54
+ // Exit early when there is no payment with an accept request
55
+ if (!payment) {
56
+ return {};
57
+ }
58
+
59
+ // The accept request is the reply request for the proposal
60
+ const acceptRequest = payment.messages.find(({type}) => {
61
+ return type === balancedChannelKeyTypes.accept_request;
62
+ });
63
+
64
+ const request = hexAsUtf8(acceptRequest.value);
65
+
66
+ // Make sure the accept payment request is a regular one
67
+ try {
68
+ parsePaymentRequest({request});
69
+ } catch (err) {
70
+ return {};
71
+ }
72
+
73
+ const {destination, mtokens} = parsePaymentRequest({request});
74
+
75
+ // The accept payment request should request the expected amount
76
+ if (mtokens !== expectedResponseMtokens) {
77
+ return {};
78
+ }
79
+
80
+ // Find the requested channel capacity
81
+ const channelCapacity = payment.messages.find(({type}) => {
82
+ return type === balancedChannelKeyTypes.channel_capacity;
83
+ });
84
+
85
+ // Exit early when there is no channel capacity
86
+ if (!channelCapacity || !isHexNumberSized(channelCapacity.value)) {
87
+ return {};
88
+ }
89
+
90
+ // Exit early when the capacity doesn't make sense for splitting equally
91
+ if (isOdd(parseHexNumber(channelCapacity.value))) {
92
+ return {};
93
+ }
94
+
95
+ // Find the requested chain fee rate
96
+ const fundingFeeRate = payment.messages.find(({type}) => {
97
+ return type === balancedChannelKeyTypes.funding_tx_fee_rate;
98
+ });
99
+
100
+ // Exit early when there is no fee rate
101
+ if (!fundingFeeRate || !isHexNumberSized(fundingFeeRate.value)) {
102
+ return {};
103
+ }
104
+
105
+ // There must be a non-zero fee rate
106
+ if (!parseHexNumber(fundingFeeRate.value)) {
107
+ return {};
108
+ }
109
+
110
+ // Find the remote multisig key for the open
111
+ const remoteMultiSigKey = payment.messages.find(({type}) => {
112
+ return type === balancedChannelKeyTypes.multisig_public_key;
113
+ });
114
+
115
+ // Exit early when there is no remote multisig key
116
+ if (!remoteMultiSigKey) {
117
+ return {};
118
+ }
119
+
120
+ // The remote multisig key must be a public key
121
+ if (!isPublicKey(remoteMultiSigKey.value)) {
122
+ return {};
123
+ }
124
+
125
+ // Find the remote transaction UTXO transaction id
126
+ const remoteTxId = payment.messages.find(({type}) => {
127
+ return type === balancedChannelKeyTypes.transit_tx_id;
128
+ });
129
+
130
+ // Exit early when there is no tx id
131
+ if (!remoteTxId || !isHexHashSized(remoteTxId.value)) {
132
+ return {};
133
+ }
134
+
135
+ // Find the remote transaction UTXO transaction output index
136
+ const remoteTxVout = payment.messages.find(({type}) => {
137
+ return type === balancedChannelKeyTypes.transit_tx_vout;
138
+ });
139
+
140
+ // Exit early when there is no tx vout
141
+ if (!remoteTxVout || !isHexNumberSized(remoteTxVout.value)) {
142
+ return {};
143
+ }
144
+
145
+ return {
146
+ accept_request: request,
147
+ capacity: parseHexNumber(channelCapacity.value),
148
+ fee_rate: parseHexNumber(fundingFeeRate.value),
149
+ partner_public_key: destination,
150
+ proposed_at: args.confirmed_at,
151
+ remote_multisig_key: remoteMultiSigKey.value,
152
+ remote_tx_id: remoteTxId.value,
153
+ remote_tx_vout: parseHexNumber(remoteTxVout.value),
154
+ };
155
+ };
@@ -0,0 +1,3 @@
1
+ const balancedOpenRequest = require('./balanced_open_request');
2
+
3
+ module.exports = {balancedOpenRequest};
@@ -0,0 +1,12 @@
1
+ {
2
+ "balancedChannelKeyTypes": {
3
+ "accept_request": "80501",
4
+ "channel_capacity": "80502",
5
+ "funding_signature": "80503",
6
+ "funding_tx_fee_rate": "80504",
7
+ "multisig_public_key": "80505",
8
+ "transit_public_key": "80506",
9
+ "transit_tx_id": "80507",
10
+ "transit_tx_vout": "80508"
11
+ }
12
+ }
@@ -293,8 +293,22 @@ module.exports = (args, cbk) => {
293
293
  return cbk(null, newCapacity);
294
294
  }],
295
295
 
296
+ // Make sure that we are still connected to the original peer
297
+ confirmConnection: [
298
+ 'newCapacity',
299
+ 'pendingChannel',
300
+ ({pendingChannel}, cbk) =>
301
+ {
302
+ return connectPeer({
303
+ id: pendingChannel.partner_public_key,
304
+ lnd: args.open_lnd,
305
+ },
306
+ cbk);
307
+ }],
308
+
296
309
  // Propose the new channel to replace the existing one
297
310
  proposeChannel: [
311
+ 'confirmConnection',
298
312
  'newCapacity',
299
313
  'pendingChannel',
300
314
  ({newCapacity, pendingChannel}, cbk) =>
@@ -7,6 +7,7 @@ const {fromHex} = Transaction;
7
7
  /** Generate a dummy PSBT to allow for setting up the channel funding
8
8
 
9
9
  {
10
+ ecp: <ECPair Object>
10
11
  [increase_transaction]: <Increase Funds Transaction Hex String>
11
12
  open_transaction: <Original Channel Funding Transaction Hex String>
12
13
  signature: <Hex Encoded Signature String>
@@ -49,6 +50,7 @@ module.exports = args => {
49
50
  }
50
51
 
51
52
  const {psbt} = transactionAsPsbt({
53
+ ecp: args.ecp,
52
54
  spending: spending.filter(n => !!n),
53
55
  transaction: replacement.toHex(),
54
56
  });
@@ -12,8 +12,8 @@ const {getPendingChannels} = require('ln-service');
12
12
  const {returnResult} = require('asyncjs-util');
13
13
  const {signTransaction} = require('ln-service');
14
14
  const {subscribeToBlocks} = require('ln-service');
15
+ const tinysecp = require('tiny-secp256k1');
15
16
  const {Transaction} = require('bitcoinjs-lib');
16
- const {transactionAsPsbt} = require('psbt');
17
17
 
18
18
  const finalizeCapacityReplacement = require('./finalize_capacity_replacement');
19
19
  const getCapacityReplacement = require('./get_capacity_replacement');
@@ -69,6 +69,9 @@ const unsignedTransactionType = '1';
69
69
  module.exports = (args, cbk) => {
70
70
  return new Promise((resolve, reject) => {
71
71
  return asyncAuto({
72
+ // Import ECPair library
73
+ ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp),
74
+
72
75
  // Check arguments
73
76
  validate: cbk => {
74
77
  if (!args.bitcoinjs_network) {
@@ -173,13 +176,15 @@ module.exports = (args, cbk) => {
173
176
 
174
177
  // Derive the funding PSBT to use for funding
175
178
  funding: [
179
+ 'ecp',
176
180
  'getReplacement',
177
181
  'signAddFunds',
178
- ({getReplacement, signAddFunds}, cbk) =>
182
+ ({ecp, getReplacement, signAddFunds}, cbk) =>
179
183
  {
180
184
  const [addFundsSignature] = signAddFunds.signatures || [];
181
185
 
182
186
  const {psbt} = interimReplacementPsbt({
187
+ ecp,
183
188
  increase_public_key: args.increase_key,
184
189
  increase_signature: addFundsSignature,
185
190
  increase_transaction: args.increase_transaction,
@@ -346,12 +351,24 @@ module.exports = (args, cbk) => {
346
351
 
347
352
  // Listen to new blocks to wait for the channel change confirmation
348
353
  const sub = subscribeToBlocks({lnd: args.lnd});
354
+ let isFinished = false;
349
355
 
350
- // Fail with error when the blocks subscription is lost
351
- sub.on('error', err => {
356
+ // Avoid multiple callbacks
357
+ const done = (err, res) => {
352
358
  sub.removeAllListeners();
353
359
 
354
- return cbk([503, 'LostBlockchainSubscription', {err}]);
360
+ if (isFinished) {
361
+ return;
362
+ }
363
+
364
+ isFinished = true;
365
+
366
+ return cbk(err, res);
367
+ };
368
+
369
+ // Fail with error when the blocks subscription is lost
370
+ sub.on('error', err => {
371
+ return done([503, 'LostBlockchainSubscription', {err}]);
355
372
  });
356
373
 
357
374
  // Publish the new channel transaction when a block is received
@@ -378,9 +395,7 @@ module.exports = (args, cbk) => {
378
395
 
379
396
  // The new channel confirmed successfully and so the change worked
380
397
  if (!!channel && !!channel.is_active) {
381
- sub.removeAllListeners();
382
-
383
- return cbk(null, {is_success: true});
398
+ return done(null, {is_success: true});
384
399
  }
385
400
  } catch (err) {
386
401
  args.logger.error({err});
@@ -414,9 +429,7 @@ module.exports = (args, cbk) => {
414
429
 
415
430
  // The old channel force closed and so the change fully failed
416
431
  if (!!channel && channel.close_transaction_id !== txId) {
417
- sub.removeAllListeners();
418
-
419
- return cbk(null, {is_success: false});
432
+ return done(null, {is_success: false});
420
433
  }
421
434
  } catch (err) {
422
435
  args.logger.error({err});
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ const {balancedOpenRequest} = require('./balanced');
1
2
  const {changeChannelCapacity} = require('./capacity');
2
3
  const {confirmServiceUse} = require('./client');
3
4
  const {createAnchoredTrade} = require('./trades');
@@ -17,6 +18,7 @@ const {servicePeerRequests} = require('./p2p');
17
18
  const serviceIds = schema.types;
18
19
 
19
20
  module.exports = {
21
+ balancedOpenRequest,
20
22
  changeChannelCapacity,
21
23
  confirmServiceUse,
22
24
  createAnchoredTrade,
package/package.json CHANGED
@@ -11,9 +11,11 @@
11
11
  "asyncjs-util": "1.2.8",
12
12
  "bolt01": "1.2.3",
13
13
  "bolt07": "1.8.0",
14
+ "ecpair": "2.0.1",
14
15
  "invoices": "2.0.4",
15
- "ln-service": "53.9.0",
16
- "ln-sync": "3.10.1"
16
+ "ln-service": "53.9.2",
17
+ "ln-sync": "3.10.1",
18
+ "tiny-secp256k1": "2.2.1"
17
19
  },
18
20
  "description": "Lightning Paid Services library",
19
21
  "devDependencies": {
@@ -38,7 +40,7 @@
38
40
  },
39
41
  "scripts": {
40
42
  "integration-tests": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 180 test/integration/*.js",
41
- "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/capacity/*.js test/client/*.js test/config/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/server/*.js test/services/*.js test/trades/*.js"
43
+ "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/balanced/*.js test/capacity/*.js test/client/*.js test/config/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/server/*.js test/services/*.js test/trades/*.js"
42
44
  },
43
- "version": "3.11.1"
45
+ "version": "3.12.0"
44
46
  }
@@ -0,0 +1,176 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../balanced/balanced_open_request');
4
+
5
+ const makeMessages = overrides => {
6
+ const args = {
7
+ '80501': Buffer.from('lntb10n1p3p6msgpp5s3xkaa2gg8q2zgmva8k9ruh3r7falxqdmkadac06wxc8fkqu4x0qdqqcqzpgxqr23ssp5ukm2dcl8wzfztx63758gmdjkna8jycypey880lenh082060fem4s9qyyssqul9nlwtpxqs2qegvpl9amltr4d9d8k9e008gr5ymv4aqkerel7dknusv60gtedgfvl3pq5lzg6c4sk4xf7lmlqtwr97cx047hpj9zqcp3mqur9').toString('hex'),
8
+ '80502': (2e6).toString(16),
9
+ '80504': (255).toString(16),
10
+ '80505': Buffer.alloc(33, 2).toString('hex'),
11
+ '80507': Buffer.alloc(32).toString('hex'),
12
+ '80508': (255).toString(16),
13
+ };
14
+
15
+ Object.keys(overrides).forEach(k => args[k] = overrides[k]);
16
+
17
+ const messages = Object.keys(args)
18
+ .filter(type => args[type] !== undefined)
19
+ .map(type => ({type, value: args[type]}));
20
+
21
+ return messages;
22
+ };
23
+
24
+ const makeArgs = overrides => {
25
+ const args = {
26
+ confirmed_at: new Date(0).toISOString(),
27
+ is_push: true,
28
+ payments: [{messages: makeMessages({})}],
29
+ received_mtokens: '10000',
30
+ };
31
+
32
+ Object.keys(overrides).forEach(k => args[k] = overrides[k]);
33
+
34
+ return args;
35
+ };
36
+
37
+ const tests = [
38
+ {
39
+ args: makeArgs({is_push: false}),
40
+ description: 'A balanced open is a push',
41
+ expected: {},
42
+ },
43
+ {
44
+ args: makeArgs({received_mtokens: '10'}),
45
+ description: 'A balanced open receives 10 sats',
46
+ expected: {},
47
+ },
48
+ {
49
+ args: makeArgs({
50
+ payments: [{messages: makeMessages({'80501': undefined})}],
51
+ }),
52
+ description: 'A balanced open has a reply request',
53
+ expected: {},
54
+ },
55
+ {
56
+ args: makeArgs({
57
+ payments: [{messages: makeMessages({'80501': 'invalid request'})}],
58
+ }),
59
+ description: 'A balanced open has a valid reply request',
60
+ expected: {},
61
+ },
62
+ {
63
+ args: makeArgs({
64
+ payments: [{
65
+ messages: makeMessages({
66
+ '80501': Buffer.from('lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql').toString('hex'),
67
+ }),
68
+ }],
69
+ }),
70
+ description: 'A balanced open has a reply request with the correct amount',
71
+ expected: {},
72
+ },
73
+ {
74
+ args: makeArgs({
75
+ payments: [{messages: makeMessages({'80502': undefined})}],
76
+ }),
77
+ description: 'A balanced open has capacity',
78
+ expected: {},
79
+ },
80
+ {
81
+ args: makeArgs({
82
+ payments: [{
83
+ messages: makeMessages({'80502': Buffer.alloc(32).toString('hex')}),
84
+ }],
85
+ }),
86
+ description: 'A balanced open has numeric capacity',
87
+ expected: {},
88
+ },
89
+ {
90
+ args: makeArgs({payments: [{messages: makeMessages({'80502': '01'})}]}),
91
+ description: 'A balanced open has even capacity',
92
+ expected: {},
93
+ },
94
+ {
95
+ args: makeArgs({
96
+ payments: [{messages: makeMessages({'80504': undefined})}],
97
+ }),
98
+ description: 'A balanced open has a fee rate',
99
+ expected: {},
100
+ },
101
+ {
102
+ args: makeArgs({
103
+ payments: [{
104
+ messages: makeMessages({'80504': Buffer.alloc(32).toString('hex')}),
105
+ }],
106
+ }),
107
+ description: 'A balanced open has a numeric fee rate',
108
+ expected: {},
109
+ },
110
+ {
111
+ args: makeArgs({payments: [{messages: makeMessages({'80504': '00'})}]}),
112
+ description: 'A balanced open has non zero fee rate',
113
+ expected: {},
114
+ },
115
+ {
116
+ args: makeArgs({
117
+ payments: [{messages: makeMessages({'80505': undefined})}],
118
+ }),
119
+ description: 'A balanced open has a remote multisig key',
120
+ expected: {},
121
+ },
122
+ {
123
+ args: makeArgs({
124
+ payments: [{messages: makeMessages({'80505': Buffer.alloc(32)})}],
125
+ }),
126
+ description: 'A balanced open has a remote multisig public key',
127
+ expected: {},
128
+ },
129
+ {
130
+ args: makeArgs({
131
+ payments: [{messages: makeMessages({'80507': undefined})}],
132
+ }),
133
+ description: 'A balanced open has a tx id',
134
+ expected: {},
135
+ },
136
+ {
137
+ args: makeArgs({payments: [{messages: makeMessages({'80507': '00'})}]}),
138
+ description: 'A balanced open has a regular sized tx id',
139
+ expected: {},
140
+ },
141
+ {
142
+ args: makeArgs({
143
+ payments: [{messages: makeMessages({'80508': undefined})}],
144
+ }),
145
+ description: 'A balanced open has a tx vout',
146
+ expected: {},
147
+ },
148
+ {
149
+ args: makeArgs({}),
150
+ description: 'Derive a balanced open request',
151
+ expected: {
152
+ accept_request: 'lntb10n1p3p6msgpp5s3xkaa2gg8q2zgmva8k9ruh3r7falxqdmkadac06wxc8fkqu4x0qdqqcqzpgxqr23ssp5ukm2dcl8wzfztx63758gmdjkna8jycypey880lenh082060fem4s9qyyssqul9nlwtpxqs2qegvpl9amltr4d9d8k9e008gr5ymv4aqkerel7dknusv60gtedgfvl3pq5lzg6c4sk4xf7lmlqtwr97cx047hpj9zqcp3mqur9',
153
+ capacity: 2000000,
154
+ fee_rate: 255,
155
+ partner_public_key: '020ec0c6a0c4fe5d8a79928ead294c36234a76f6e0dca896c35413612a3fd8dbf8',
156
+ proposed_at: '1970-01-01T00:00:00.000Z',
157
+ remote_multisig_key: '020202020202020202020202020202020202020202020202020202020202020202',
158
+ remote_tx_id: '0000000000000000000000000000000000000000000000000000000000000000',
159
+ remote_tx_vout: 255,
160
+ },
161
+ },
162
+ ];
163
+
164
+ tests.forEach(({args, description, error, expected}) => {
165
+ return test(description, async ({end, strictSame, throws}) => {
166
+ if (!!error) {
167
+ throws(() => method(args), new Error(error), 'Got error');
168
+ } else {
169
+ const res = method(args);
170
+
171
+ strictSame(res, expected, 'Got expected result');
172
+ }
173
+
174
+ return end();
175
+ });
176
+ });
@@ -6,6 +6,7 @@ const {broadcastChainTransaction} = require('ln-service');
6
6
  const {closeChannel} = require('ln-service');
7
7
  const {createChainAddress} = require('ln-service');
8
8
  const {getChainTransactions} = require('ln-service');
9
+ const {getChannel} = require('ln-service');
9
10
  const {getChannels} = require('ln-service');
10
11
  const {getNetwork} = require('ln-sync');
11
12
  const {networks} = require('bitcoinjs-lib');
@@ -51,18 +52,30 @@ test(`Accept capacity replacement`, async ({end, equal, strictSame}) => {
51
52
 
52
53
  try {
53
54
  // Open up a new channel
54
- const channelOpen = await openChannel({
55
- lnd,
56
- local_tokens: capacity,
57
- partner_public_key: target.id,
58
- partner_socket: target.socket,
55
+ const channelOpen = await asyncRetry({interval, times}, async () => {
56
+ return await openChannel({
57
+ lnd,
58
+ local_tokens: capacity,
59
+ partner_public_key: target.id,
60
+ partner_socket: target.socket,
61
+ });
59
62
  });
60
63
 
61
64
  // Wait for the channel to be active
62
65
  const channel = await asyncRetry({interval, times}, async () => {
63
66
  const [channel] = (await getChannels({lnd})).channels;
64
67
 
65
- if (!!channel && !!channel.is_active) {
68
+ if (!channel) {
69
+ await generate({});
70
+
71
+ throw new Error('ExpectedInitialChannelActivation');
72
+ }
73
+
74
+ const {policies} = await getChannel({lnd, id: channel.id});
75
+
76
+ const [missingCltv] = policies.filter(n => !n.cltv_delta);
77
+
78
+ if (!!channel && !!channel.is_active && !missingCltv) {
66
79
  return channel;
67
80
  }
68
81
 
@@ -46,12 +46,14 @@ test(`Accept capacity replacement`, async ({end, equal, strictSame}) => {
46
46
 
47
47
  try {
48
48
  // Open up a new channel
49
- const channelOpen = await openChannel({
50
- lnd,
51
- is_private: true,
52
- local_tokens: capacity,
53
- partner_public_key: target.id,
54
- partner_socket: target.socket,
49
+ const channelOpen = await asyncRetry({interval, times}, async () => {
50
+ return await openChannel({
51
+ lnd,
52
+ is_private: true,
53
+ local_tokens: capacity,
54
+ partner_public_key: target.id,
55
+ partner_socket: target.socket,
56
+ });
55
57
  });
56
58
 
57
59
  // Wait for the channel to be active