paid-services 3.6.0 → 3.7.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,5 +1,9 @@
1
1
  # Versions
2
2
 
3
+ ## Version 3.7.0
4
+
5
+ - `changeChannelCapacity`: Add support for decreasing funds into a new channel
6
+
3
7
  ## Version 3.6.0
4
8
 
5
9
  - `manageTrades`: Add support for persistent open trades
@@ -212,6 +212,8 @@ module.exports = ({channel, from, id, increase, lnd, logger}, cbk) => {
212
212
  return failure([503, 'FailedToParseSignChangeRequest']);
213
213
  }
214
214
 
215
+ logger.info({closing_and_reopening_channel: true});
216
+
215
217
  // Trigger the replacement flow and sign the unsigned tx
216
218
  return getReplacementSignature({
217
219
  channel,
@@ -48,8 +48,9 @@ const weightBuffer = 150;
48
48
  cltv_delta: <Locktime Delta Number>
49
49
  coop_close_address: <Cooperative Close Address String>
50
50
  decrease: [{
51
- address: <Decrease Address String>
52
- output: <Output Script Hex String>
51
+ [address]: <Decrease Address String>
52
+ [node]: <Create Channel with Node With Public Key Hex String>
53
+ [output]: <Output Script Hex String>
53
54
  tokens: <Decrease Tokens Number>
54
55
  }]
55
56
  estimated_capacity: <Estimated Tokens For New Channel Capacity Number>
@@ -172,6 +173,7 @@ module.exports = ({ask, id, lnd}, cbk) => {
172
173
  const disabled = [
173
174
  !!channel.cooperative_close_address,
174
175
  !channel.is_active,
176
+ !!channel.pending_payments.length,
175
177
  ];
176
178
 
177
179
  return {
@@ -344,7 +346,7 @@ module.exports = ({ask, id, lnd}, cbk) => {
344
346
  return cbk(err);
345
347
  }
346
348
 
347
- if (decreases.find(n => n.address === res.address)) {
349
+ if (!!res.address && decreases.find(n => n.address === res.address)) {
348
350
  return cbk([400, 'ExpectedUniqueAddressForSpend']);
349
351
  }
350
352
 
@@ -356,11 +358,8 @@ module.exports = ({ask, id, lnd}, cbk) => {
356
358
  return cbk(err);
357
359
  }
358
360
 
359
- const final = decreases
360
- .filter(n => !!n.tokens)
361
- .map(({address, output, tokens}) => ({address, output, tokens}));
362
-
363
- return cbk(null, final);
361
+ // Only include decreases that spend funds
362
+ return cbk(null, decreases.filter(n => !!n.tokens));
364
363
  }
365
364
  );
366
365
  }],
@@ -413,10 +412,10 @@ module.exports = ({ask, id, lnd}, cbk) => {
413
412
  return type;
414
413
  }
415
414
 
416
- return `${type} (Keep Current Status)`;
415
+ return `${type} (keep current status)`;
417
416
  }),
418
417
  default: channel.is_private ? 'Private' : 'Public',
419
- message: 'Channel type?',
418
+ message: 'Replacement channel type?',
420
419
  name: 'type',
421
420
  type: 'list',
422
421
  },
@@ -1,14 +1,28 @@
1
1
  const {address} = require('bitcoinjs-lib');
2
2
  const asyncAuto = require('async/auto');
3
3
  const {createChainAddress} = require('ln-service');
4
+ const {getIdentity} = require('ln-service');
5
+ const {acceptsChannelOpen} = require('ln-sync');
4
6
  const {getNetwork} = require('ln-sync');
5
7
  const {networks} = require('bitcoinjs-lib');
6
8
  const {returnResult} = require('asyncjs-util');
7
9
 
10
+ const proposeChannelOpen = require('./propose_channel_open');
11
+
12
+ const askForChanSize = max => `Capacity of the new channel? (max: ${max})`;
13
+ const askForSendSize = max => `Amount you want to send out? (max: ${max})`;
8
14
  const bufferAsHex = buffer => buffer.toString('hex');
9
15
  const dust = 550;
16
+ const {isInteger} = Number;
10
17
  const isNumber = n => !isNaN(n);
18
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
19
+ const minChannelCapacity = 20000;
20
+ const minSpendTokens = 0;
21
+ const openChannelAction = 'open_channel';
22
+ const spendToExternalAddressAction = 'external_spend_funds';
23
+ const spendToInternalAddressAction = 'internal_spend_funds';
11
24
  const {toOutputScript} = address;
25
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
12
26
 
13
27
  /** Ask for details about a decrease
14
28
 
@@ -22,6 +36,7 @@ const {toOutputScript} = address;
22
36
  {
23
37
  [address]: <Send Funds to Address String>
24
38
  is_final: <Decrease is Final Decrease Bool>
39
+ [node]: <Create Channel with Node With Public Key Hex String>
25
40
  [output]: <Output Script Hex String>
26
41
  tokens: <Withdraw Tokens Number>
27
42
  }
@@ -46,66 +61,149 @@ module.exports = ({ask, lnd, max}, cbk) => {
46
61
  return cbk();
47
62
  },
48
63
 
49
- // Ask for the amount to decrease
50
- askForAmount: ['validate', ({}, cbk) => {
64
+ // Select the type of decrease
65
+ askForDecreaseType: ['validate', ({}, cbk) => {
51
66
  return ask({
52
- default: '0',
53
- name: 'amount',
54
- message: `How much do you want to spend out (max: ${max})?`,
67
+ choices: [
68
+ {
69
+ name: `Send decreased funds to the internal chain wallet`,
70
+ value: spendToInternalAddressAction,
71
+ },
72
+ {
73
+ name: `Send decreased funds to an external chain address`,
74
+ value: spendToExternalAddressAction,
75
+ },
76
+ {
77
+ disabled: max < minChannelCapacity,
78
+ name: `Spend decreased funds into new channel with another peer`,
79
+ value: openChannelAction,
80
+ },
81
+ ],
82
+ message: 'How do you want to change the channel capacity?',
83
+ name: 'decrease',
84
+ type: 'list',
85
+ },
86
+ ({decrease}) => cbk(null, decrease));
87
+ }],
88
+
89
+ // Get the node identity key to make sure a channel open isn't with self
90
+ getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
91
+
92
+ // Get network name to validate addresses against
93
+ getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
94
+
95
+ // Ask for the public key of the node to trade with
96
+ askForNodeId: [
97
+ 'askForDecreaseType',
98
+ 'getIdentity',
99
+ ({askForDecreaseType, getIdentity}, cbk) =>
100
+ {
101
+ // Exit early when not opening a new channel
102
+ if (askForDecreaseType !== openChannelAction) {
103
+ return cbk();
104
+ }
105
+
106
+ return ask({
107
+ name: 'key',
108
+ message: 'Public key of node to open channel with?',
55
109
  type: 'input',
56
110
  validate: input => {
57
- if (!isNumber(input) || !Number.isInteger(Number(input))) {
111
+ if (!input) {
58
112
  return false;
59
113
  }
60
114
 
61
- if (!!Number(input) && Number(input) < dust) {
62
- return false;
115
+ if (!isPublicKey(input)) {
116
+ return 'Expected public key of node to open channel with';
63
117
  }
64
118
 
65
- if (!!Number(input) && Number(input) > max) {
66
- return `The maximum possible to decrease is ${max}`;
119
+ if (input === getIdentity.public_key) {
120
+ return 'Expected public key of other node';
67
121
  }
68
122
 
69
123
  return true;
70
124
  },
71
125
  },
72
- ({amount}) => cbk(null, amount));
126
+ ({key}) => cbk(null, key));
73
127
  }],
74
128
 
75
- // Get network name
76
- getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
77
-
78
- // Ask if withdrawing to an external address
79
- askForExternal: ['askForAmount', ({askForAmount}, cbk) => {
80
- if (!Number(askForAmount)) {
129
+ // Create a decrease address to withdraw funds out to
130
+ createAddress: ['askForDecreaseType', ({askForDecreaseType}, cbk) => {
131
+ // Exit early when there is no need to create an internal address
132
+ if (askForDecreaseType !== spendToInternalAddressAction) {
81
133
  return cbk();
82
134
  }
83
135
 
136
+ return createChainAddress({lnd}, cbk);
137
+ }],
138
+
139
+ // Ask for the amount to decrease
140
+ askForAmount: ['askForNodeId', ({askForNodeId}, cbk) => {
84
141
  return ask({
85
- default: false,
86
- name: 'spend',
87
- message: `Spend ${askForAmount} to an external address?`,
88
- type: 'confirm',
142
+ default: !askForNodeId ? minSpendTokens : minChannelCapacity,
143
+ message: !askForNodeId ? askForSendSize(max) : askForChanSize(max),
144
+ name: 'amount',
145
+ validate: input => {
146
+ // A numeric input is required
147
+ if (!isNumber(input) || !isInteger(Number(input))) {
148
+ return false;
149
+ }
150
+
151
+ // Zero value is acceptable for a decrease when not opening channel
152
+ if (!askForNodeId && !Number(input)) {
153
+ return true;
154
+ }
155
+
156
+ // On-chain values must always be above dust
157
+ if (!!Number(input) && Number(input) < dust) {
158
+ return false;
159
+ }
160
+
161
+ // Channel opens must always be above the minimum channel size
162
+ if (!!askForNodeId && Number(input) < minChannelCapacity) {
163
+ return `The minimum channel capacity is ${minChannelCapacity}`;
164
+ }
165
+
166
+ // A decrease cannot spend more funds than are available
167
+ if (!!Number(input) && Number(input) > max) {
168
+ return `The maximum possible to decrease is ${max}`;
169
+ }
170
+
171
+ return true;
172
+ },
89
173
  },
90
- ({spend}) => cbk(null, spend));
174
+ ({amount}) => cbk(null, Number(amount)));
91
175
  }],
92
176
 
93
177
  // Ask for an external address
94
178
  askForAddress: [
95
179
  'askForAmount',
96
- 'askForExternal',
97
- ({askForAmount, askForExternal}, cbk) =>
180
+ 'askForDecreaseType',
181
+ 'getNetwork',
182
+ ({askForAmount, askForDecreaseType, getNetwork}, cbk) =>
98
183
  {
99
- // Exit early when the withdraw address is internal
100
- if (askForExternal !== true) {
184
+ // Exit early when the withdraw address is not external
185
+ if (askForDecreaseType !== spendToExternalAddressAction) {
101
186
  return cbk();
102
187
  }
103
188
 
104
189
  return ask({
105
190
  name: 'address',
106
- message: `Address to spend ${askForAmount} to?`,
191
+ message: `Address to spend ${tokensAsBigUnit(askForAmount)} to?`,
107
192
  type: 'input',
108
- validate: input => !!input,
193
+ validate: input => {
194
+ if (!input) {
195
+ return false;
196
+ }
197
+
198
+ // Make sure that the entered address can be derived to an output
199
+ try {
200
+ toOutputScript(input, networks[getNetwork.bitcoinjs]);
201
+ } catch (err) {
202
+ return 'Failed to parse address. Try a standard one?';
203
+ }
204
+
205
+ return true;
206
+ },
109
207
  },
110
208
  ({address}) => cbk(null, {address}));
111
209
  }],
@@ -113,31 +211,46 @@ module.exports = ({ask, lnd, max}, cbk) => {
113
211
  // Ask if this is the last output
114
212
  askForAddition: [
115
213
  'askForAddress',
116
- 'askForExternal',
117
- ({askForExternal}, cbk) =>
214
+ 'askForAmount',
215
+ 'askForDecreaseType',
216
+ ({askForAmount, askForDecreaseType}, cbk) =>
118
217
  {
119
- // Exit early when the spend was to an internal addess
120
- if (!askForExternal) {
218
+ // Exit early and only ask for addition when decrease is a "spend" type
219
+ if (askForDecreaseType === spendToInternalAddressAction) {
220
+ return cbk();
221
+ }
222
+
223
+ // Exit early when there are no more funds to spend
224
+ if (max - askForAmount < dust) {
121
225
  return cbk();
122
226
  }
123
227
 
124
228
  return ask({
125
229
  default: false,
126
230
  name: 'add',
127
- message: 'Add another spend to a different address?',
231
+ message: 'Spend additional funds from the channel capacity?',
128
232
  type: 'confirm',
129
233
  },
130
234
  ({add}) => cbk(null, add));
131
235
  }],
132
236
 
133
- // Create a decrease address to withdraw funds out to
134
- createAddress: ['askForExternal', ({askForExternal}, cbk) => {
135
- // Exit early when the withdraw address is external
136
- if (askForExternal !== false) {
237
+ // Propose a channel open to confirm the peer will accept the channel
238
+ checkChannelAcceptance: [
239
+ 'askForAmount',
240
+ 'askForNodeId',
241
+ ({askForAmount, askForNodeId}, cbk) =>
242
+ {
243
+ // Exit early if pubkey was not entered
244
+ if (!askForNodeId) {
137
245
  return cbk();
138
246
  }
139
247
 
140
- return createChainAddress({lnd}, cbk);
248
+ return proposeChannelOpen({
249
+ lnd,
250
+ capacity: askForAmount,
251
+ id: askForNodeId,
252
+ },
253
+ cbk);
141
254
  }],
142
255
 
143
256
  // Final decrease output details
@@ -145,38 +258,43 @@ module.exports = ({ask, lnd, max}, cbk) => {
145
258
  'askForAddition',
146
259
  'askForAddress',
147
260
  'askForAmount',
261
+ 'checkChannelAcceptance',
148
262
  'createAddress',
149
263
  'getNetwork',
150
264
  ({
151
265
  askForAddition,
152
266
  askForAddress,
153
267
  askForAmount,
268
+ askForNodeId,
269
+ checkChannelAcceptance,
154
270
  createAddress,
155
271
  getNetwork,
156
272
  },
157
273
  cbk) =>
158
274
  {
159
- const {address} = (createAddress || askForAddress || {});
160
-
161
- // Exit early when not decreasing to an address
162
- if (!address) {
163
- return cbk(null, {is_final: true, tokens: Number(askForAmount)});
275
+ // Make sure that a channel proposal didn't fail
276
+ if (!!checkChannelAcceptance && !checkChannelAcceptance.is_accepted) {
277
+ return cbk([503, 'RemoteNodeRejectedNewChannelProposal']);
164
278
  }
165
279
 
166
- // Make sure the address is valid
167
- try {
168
- toOutputScript(address, networks[getNetwork.bitcoinjs]);
169
- } catch (err) {
170
- return cbk([400, 'FailedToParseAddress', {err}]);
280
+ // Exit early when decreasing to a new channel
281
+ if (!!askForNodeId) {
282
+ return cbk(null, {
283
+ is_final: !askForAddition,
284
+ node: askForNodeId,
285
+ tokens: askForAmount,
286
+ });
171
287
  }
172
288
 
289
+ const {address} = (createAddress || askForAddress);
290
+
173
291
  const output = toOutputScript(address, networks[getNetwork.bitcoinjs]);
174
292
 
175
293
  return cbk(null, {
176
294
  address,
177
295
  is_final: !askForAddition,
178
296
  output: bufferAsHex(output),
179
- tokens: Number(askForAmount),
297
+ tokens: askForAmount,
180
298
  });
181
299
  }],
182
300
  },
@@ -13,7 +13,7 @@ const dummySignature = Buffer.alloc(74);
13
13
  const dummyTokens = 0;
14
14
  const dummyVout = 0;
15
15
  const hexAsBuffer = hex => Buffer.from(hex, 'hex');
16
- const increaseBuffer = 300;
16
+ const increaseBuffer = 250;
17
17
  const {max} = Math;
18
18
  const weightAsVBytes = weight => weight / 4;
19
19
 
@@ -24,7 +24,7 @@ const weightAsVBytes = weight => weight / 4;
24
24
  commit_transaction_fee: <Commit Transaction Fee Tokens Number>
25
25
  commit_transaction_weight: <Commit Transaction Weight Units Number>
26
26
  decrease: [{
27
- output: <Output Script Hex String>
27
+ [output]: <Output Script Hex String>
28
28
  tokens: <Spend Value to Chain Address Tokens Number>
29
29
  }]
30
30
  [increase]: <Add Funds Tokens Number>
@@ -51,16 +51,12 @@ module.exports = args => {
51
51
 
52
52
  // Add the decreases as outputs
53
53
  args.decrease.forEach(({output, tokens}) => {
54
- return tx.addOutput(hexAsBuffer(output), tokens);
54
+ return tx.addOutput(hexAsBuffer(output || dummyP2wsh), tokens);
55
55
  });
56
56
 
57
57
  // Add an output to represent the new channel output
58
58
  tx.addOutput(dummyP2wsh, args.capacity);
59
59
 
60
- args.decrease.forEach(({output}) => {
61
- return tx.addOutput(hexAsBuffer(output), dummyTokens);
62
- });
63
-
64
60
  // Add an input to represent the old channel input
65
61
  tx.addInput(dummyHash, dummyVout);
66
62
 
@@ -1,10 +1,14 @@
1
+ const {address} = require('bitcoinjs-lib');
1
2
  const asyncAuto = require('async/auto');
3
+ const asyncEach = require('async/each');
2
4
  const {closeChannel} = require('ln-service');
5
+ const {connectPeer} = require('ln-sync');
3
6
  const {decodeChanId} = require('bolt07');
4
7
  const {getChainFeeRate} = require('ln-service');
5
8
  const {getChainTransactions} = require('ln-service');
6
9
  const {getHeight} = require('ln-service');
7
10
  const {getPendingChannels} = require('ln-service');
11
+ const {networks} = require('bitcoinjs-lib');
8
12
  const {openChannels} = require('ln-service');
9
13
  const {returnResult} = require('asyncjs-util');
10
14
  const {Transaction} = require('bitcoinjs-lib');
@@ -26,6 +30,7 @@ const notFoundIndex = -1;
26
30
  const positive = n => Math.max(1, n);
27
31
  const slowTarget = 1000;
28
32
  const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
33
+ const {toOutputScript} = address;
29
34
  const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
30
35
 
31
36
  /** Close a channel and get a capacity replacement transaction
@@ -36,7 +41,8 @@ const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
36
41
  bitcoinjs_network: <BitcoinJs Network Name String>
37
42
  decrease: [{
38
43
  address: <Spend to Chain Address String>
39
- output: <Spend to Output Script Hex String>
44
+ [node]: <Node to Open Channel to Public Key Hex String>
45
+ [output]: <Spend to Output Script Hex String>
40
46
  tokens: <Spend Value Number>
41
47
  }]
42
48
  id: <Original Channel Standard Format Channel Id String>
@@ -51,10 +57,12 @@ const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
51
57
  {
52
58
  [add_funds_vin]: <Funding Spend Input Index Number>
53
59
  capacity: <Replacement Channel Capacity Tokens Number>
60
+ force_close_tx: <Raw Force Close Transaction Hex String>
61
+ open_pending_ids: [<Proposed Additional Channel Id String>]
62
+ pending_channel_id: <Proposed Replacement Channel Pending Id Hex String>
54
63
  remote_balance: <Initial Balance for Peer Tokens Number>
55
64
  signature: <Signature for Replacement Transaction Hex Encoded String>
56
65
  signing_key: <Signing Public Key Hex String>
57
- pending_channel_id: <Proposed Channel Pending Id Hex String>
58
66
  transaction_id: <Replacement Channel Transaction Id Hex String>
59
67
  transaction_vin: <Original Channel Input Index Number>
60
68
  transaction_vout: <Replacement Channel Transaction Output Index Number>
@@ -102,8 +110,34 @@ module.exports = (args, cbk) => {
102
110
  // Get the current chain height
103
111
  getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
104
112
 
113
+ // Connect to nodes to open channels to
114
+ connect: ['validate', ({}, cbk) => {
115
+ return asyncEach(args.decrease.filter(n => !!n.node), (spend, cbk) => {
116
+ return connectPeer({id: spend.node, lnd: args.lnd}, cbk);
117
+ },
118
+ cbk);
119
+ }],
120
+
121
+ // Propose any additional channels to nodes
122
+ openChannels: ['connect', ({}, cbk) => {
123
+ // Exit early when there are no nodes to propose channels to
124
+ if (!args.decrease.filter(n => !!n.node).length) {
125
+ return cbk(null, {pending: []});
126
+ }
127
+
128
+ return openChannels({
129
+ channels: args.decrease.filter(n => !!n.node).map(decrease => ({
130
+ capacity: decrease.tokens,
131
+ partner_public_key: decrease.node,
132
+ })),
133
+ is_avoiding_broadcast: true,
134
+ lnd: args.lnd,
135
+ },
136
+ cbk);
137
+ }],
138
+
105
139
  // Force close the channel in order to get a non-final sequence number
106
- channelClose: ['getFeeRate', ({}, cbk) => {
140
+ channelClose: ['getFeeRate','openChannels', ({}, cbk) => {
107
141
  return closeChannel({
108
142
  lnd: args.lnd,
109
143
  is_force_close: true,
@@ -277,19 +311,31 @@ module.exports = (args, cbk) => {
277
311
  replacementTx: [
278
312
  'closeTx',
279
313
  'newCapacity',
314
+ 'openChannels',
280
315
  'proposeChannel',
281
- ({closeTx, newCapacity, proposeChannel}, cbk) =>
316
+ ({closeTx, newCapacity, openChannels, proposeChannel}, cbk) =>
282
317
  {
283
318
  // The replacement tx pays to the proposed channel multisig address
284
319
  const [{address}] = proposeChannel.pending;
285
320
 
321
+ const network = networks[args.bitcoinjs_network];
322
+
323
+ // Map pending channel opens to match the spends
324
+ const openAsSpends = openChannels.pending.map(({address, tokens}) => ({
325
+ tokens,
326
+ output: bufferAsHex(toOutputScript(address, network)),
327
+ }));
328
+
329
+ // Decreases are spends to regular addresses plus channel outputs
330
+ const decreases = [].concat(args.decrease).concat(openAsSpends);
331
+
286
332
  try {
287
333
  const replacement = assembleReplacementTx({
288
334
  add_funds_transaction_id: args.add_funds_transaction_id,
289
335
  add_funds_transaction_vout: args.add_funds_transaction_vout,
290
336
  bitcoinjs_network: args.bitcoinjs_network,
291
337
  close_transaction: closeTx.transaction,
292
- decrease: args.decrease.map(decrease => ({
338
+ decrease: decreases.filter(n => !!n.output).map(decrease => ({
293
339
  output: decrease.output,
294
340
  tokens: decrease.tokens,
295
341
  })),
@@ -334,6 +380,7 @@ module.exports = (args, cbk) => {
334
380
  proposedReplacement: [
335
381
  'closeTx',
336
382
  'newCapacity',
383
+ 'openChannels',
337
384
  'pendingChannel',
338
385
  'proposeChannel',
339
386
  'replacementTx',
@@ -341,6 +388,7 @@ module.exports = (args, cbk) => {
341
388
  ({
342
389
  closeTx,
343
390
  newCapacity,
391
+ openChannels,
344
392
  pendingChannel,
345
393
  proposeChannel,
346
394
  replacementTx,
@@ -353,11 +401,12 @@ module.exports = (args, cbk) => {
353
401
  return cbk(null, {
354
402
  add_funds_vin: replacementTx.add_funds_vin,
355
403
  capacity: newCapacity,
356
- force_close_tx_id: closeTx.id,
404
+ force_close_tx: closeTx.transaction,
405
+ open_pending_ids: openChannels.pending.map(n => n.id),
406
+ pending_channel_id: pending.id,
357
407
  remote_balance: pendingChannel.remote_balance,
358
408
  signature: signReplacement.signature,
359
409
  signing_key: signReplacement.key,
360
- pending_channel_id: pending.id,
361
410
  transaction_id: replacementTx.transaction_id,
362
411
  transaction_vin: replacementTx.transaction_vin,
363
412
  transaction_vout: replacementTx.transaction_vout,