paid-services 3.4.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.
Files changed (38) hide show
  1. package/.prettierignore +2 -0
  2. package/CHANGELOG.md +16 -0
  3. package/capacity/accept_capacity_change.js +2 -0
  4. package/capacity/ask_for_change_details.js +41 -9
  5. package/capacity/ask_for_decrease.js +167 -49
  6. package/capacity/capacity_change_requests.js +9 -0
  7. package/capacity/change_channel_capacity.js +7 -1
  8. package/capacity/encode_change_request.js +8 -1
  9. package/capacity/fee_for_replacement.js +3 -7
  10. package/capacity/get_capacity_change_requests.js +1 -0
  11. package/capacity/get_capacity_replacement.js +56 -7
  12. package/capacity/initiate_capacity_change.js +1 -2
  13. package/capacity/parse_capacity_change_request.js +8 -1
  14. package/capacity/propose_capacity_change.js +208 -92
  15. package/capacity/propose_channel_open.js +54 -0
  16. package/package.json +9 -9
  17. package/services/response_for_relay.js +0 -1
  18. package/test/capacity/test_capacity_change_requests.js +1 -0
  19. package/test/capacity/test_encode_change_request.js +5 -0
  20. package/test/capacity/test_parse_capacity_change_request.js +1 -0
  21. package/test/integration/test_accept_capacity_change.js +3 -4
  22. package/test/integration/test_change_channel_capacity.js +8 -1
  23. package/test/integration/test_decrease_channel_capacity.js +23 -8
  24. package/test/integration/test_increase_channel_capacity.js +4 -1
  25. package/test/integration/test_split_channel_capacity.js +241 -0
  26. package/trades/accept_trade.js +12 -3
  27. package/trades/create_trade.js +82 -50
  28. package/trades/decode_anchored_trade.js +56 -0
  29. package/trades/encode_anchored_trade.js +35 -0
  30. package/trades/finalize_trade_secret.js +7 -0
  31. package/trades/find_trade.js +28 -3
  32. package/trades/manage_trade.js +15 -4
  33. package/trades/manage_trades.js +209 -2
  34. package/trades/request_trade_by_id.js +7 -1
  35. package/trades/service_anchored_trades.js +181 -0
  36. package/trades/service_open_trade.js +172 -0
  37. package/trades/service_trade_requests.js +106 -35
  38. package/trades/trade_from_invoice.js +51 -0
@@ -1,11 +1,29 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncUntil = require('async/until');
3
+ const {cancelHodlInvoice} = require('ln-service');
4
+ const {diffieHellmanComputeSecret} = require('ln-service');
5
+ const {getIdentity} = require('ln-service');
6
+ const {getInvoices} = require('ln-service');
7
+ const {getNetwork} = require('ln-sync');
8
+ const {getNodeAlias} = require('ln-sync');
2
9
  const {returnResult} = require('asyncjs-util');
3
10
 
4
11
  const createTrade = require('./create_trade');
12
+ const encodeTrade = require('./encode_trade');
5
13
  const manageTrade = require('./manage_trade');
14
+ const serviceAnchoredTrades = require('./service_anchored_trades');
15
+ const serviceOpenTrade = require('./service_open_trade');
16
+ const tradeFromInvoice = require('./trade_from_invoice');
6
17
 
18
+ const cancelAction = 'cancel-';
7
19
  const createAction = 'create';
8
20
  const decodeAction = 'decode';
21
+ const defaultInvoicesLimit = 100;
22
+ const listAction = 'list';
23
+ const serveAction = 'serve';
24
+ const serveTradeAction = 'serve-trade-';
25
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
26
+ const viewAction = 'view';
9
27
 
10
28
  /** Create, view, and accept trades
11
29
 
@@ -13,11 +31,12 @@ const decodeAction = 'decode';
13
31
  ask: <Ask Function>
14
32
  lnd: <Authenticated LND API Object>
15
33
  logger: <Winston Logger Object>
34
+ separator: <Create Separator Function>
16
35
  }
17
36
 
18
37
  @returns via cbk or Promise
19
38
  */
20
- module.exports = ({ask, lnd, logger}, cbk) => {
39
+ module.exports = ({ask, lnd, logger, separator}, cbk) => {
21
40
  return new Promise((resolve, reject) => {
22
41
  return asyncAuto({
23
42
  // Check arguments
@@ -37,12 +56,21 @@ module.exports = ({ask, lnd, logger}, cbk) => {
37
56
  return cbk();
38
57
  },
39
58
 
59
+ // Get node identity
60
+ getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
61
+
62
+ // Get the network name
63
+ getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
64
+
40
65
  // Select a trade option
41
66
  select: ['validate', ({}, cbk) => {
42
67
  return ask({
43
68
  choices: [
44
69
  {name: 'Create Trade', value: createAction},
45
70
  {name: 'Decode Trade', value: decodeAction},
71
+ separator(),
72
+ {name: 'Open Trades', value: listAction},
73
+ {name: 'Serve Trades', value: serveAction},
46
74
  ],
47
75
  message: 'Trade?',
48
76
  name: 'action',
@@ -51,6 +79,15 @@ module.exports = ({ask, lnd, logger}, cbk) => {
51
79
  cbk);
52
80
  }],
53
81
 
82
+ // Confirm that signer RPC is enabled
83
+ checkSigner: ['getIdentity', ({getIdentity}, cbk) => {
84
+ return diffieHellmanComputeSecret({
85
+ lnd,
86
+ partner_public_key: getIdentity.public_key,
87
+ },
88
+ cbk);
89
+ }],
90
+
54
91
  // Create a new trade
55
92
  create: ['select', ({select}, cbk) => {
56
93
  // Exit early when not creating a new trade
@@ -74,7 +111,7 @@ module.exports = ({ask, lnd, logger}, cbk) => {
74
111
  }],
75
112
 
76
113
  // View an existing trade
77
- view: ['select', ({select}, cbk) => {
114
+ view: ['checkSigner', 'select', ({checkSigner, select}, cbk) => {
78
115
  // Exit early when not decoding a trade
79
116
  if (select.action !== decodeAction) {
80
117
  return cbk();
@@ -82,6 +119,176 @@ module.exports = ({ask, lnd, logger}, cbk) => {
82
119
 
83
120
  return manageTrade({ask, lnd, logger}, cbk)
84
121
  }],
122
+
123
+ // Get open trades
124
+ getTrades: ['validate', ({}, cbk) => {
125
+ const paging = {trades: []};
126
+
127
+ // Find open anchored trades
128
+ asyncUntil(
129
+ cbk => cbk(null, paging.token === false),
130
+ cbk => {
131
+ return getInvoices({
132
+ lnd,
133
+ is_unconfirmed: true,
134
+ limit: !paging.token ? defaultInvoicesLimit : undefined,
135
+ token: paging.token,
136
+ },
137
+ (err, res) => {
138
+ if (!!err) {
139
+ return cbk(err);
140
+ }
141
+
142
+ paging.token = res.next || false;
143
+
144
+ res.invoices
145
+ .map(tradeFromInvoice)
146
+ .map(n => n.trade)
147
+ .filter(n => !!n)
148
+ .forEach(n => paging.trades.push(n));
149
+
150
+ return cbk();
151
+ });
152
+ },
153
+ err => {
154
+ if (!!err) {
155
+ return cbk(err);
156
+ }
157
+
158
+ return cbk(null, paging.trades);
159
+ }
160
+ );
161
+ }],
162
+
163
+ // List open trades
164
+ list: ['getTrades', 'select', ({getTrades, select}, cbk) => {
165
+ // Exit early when not viewing open trades
166
+ if (select.action !== listAction) {
167
+ return cbk();
168
+ }
169
+
170
+ if (!getTrades.length) {
171
+ return cbk([404, 'NoTradesCurrentlyOpen']);
172
+ }
173
+
174
+ return ask({
175
+ choices: getTrades.map(trade => ({
176
+ name: `${tokensAsBigUnit(trade.tokens)} ${trade.description}`,
177
+ value: trade.id,
178
+ })),
179
+ message: 'Trade?',
180
+ name: 'manage',
181
+ type: 'list',
182
+ },
183
+ cbk);
184
+ }],
185
+
186
+ // Manage an open trades
187
+ manage: ['getTrades', 'list', ({getTrades, list}, cbk) => {
188
+ // Exit early when not viewing open trades
189
+ if (!list) {
190
+ return cbk();
191
+ }
192
+
193
+ return ask({
194
+ choices: [
195
+ {name: 'Serve Trade', value: `${serveTradeAction}${list.manage}`},
196
+ separator(),
197
+ {name: 'Cancel Trade', value: `${cancelAction}${list.manage}`},
198
+ ],
199
+ message: 'Trade?',
200
+ name: 'trade',
201
+ type: 'list',
202
+ },
203
+ cbk);
204
+ }],
205
+
206
+ // Service an open trade
207
+ serviceTrade: [
208
+ 'getIdentity',
209
+ 'getNetwork',
210
+ 'getTrades',
211
+ 'manage',
212
+ ({getIdentity, getNetwork, getTrades, manage}, cbk) =>
213
+ {
214
+ // Exit early when this is not cancel of an open trade
215
+ if (!manage || !manage.trade.startsWith(serveTradeAction)) {
216
+ return cbk();
217
+ }
218
+
219
+ const id = manage.trade.slice(serveTradeAction.length);
220
+
221
+ const trade = getTrades.find(n => n.id === id);
222
+
223
+ return serviceOpenTrade({
224
+ lnd,
225
+ logger,
226
+ channels: [],
227
+ description: trade.description,
228
+ expires_at: trade.expires_at,
229
+ id: trade.id,
230
+ network: getNetwork.network,
231
+ public_key: getIdentity.public_key,
232
+ secret: trade.secret,
233
+ tokens: trade.tokens,
234
+ uris: [],
235
+ },
236
+ cbk);
237
+ }],
238
+
239
+ // Cancel an open trade
240
+ cancelTrade: ['manage', ({manage}, cbk) => {
241
+ // Exit early when this is not cancel of an open trade
242
+ if (!manage || !manage.trade.startsWith(cancelAction)) {
243
+ return cbk();
244
+ }
245
+
246
+ return cancelHodlInvoice({
247
+ lnd,
248
+ id: manage.trade.slice(cancelAction.length),
249
+ },
250
+ cbk);
251
+ }],
252
+
253
+ // Serve open trades
254
+ service: [
255
+ 'getIdentity',
256
+ 'getNetwork',
257
+ 'select',
258
+ ({getIdentity, getNetwork, select}, cbk) =>
259
+ {
260
+ // Exit early when not serving trades
261
+ if (select.action !== serveAction) {
262
+ return cbk();
263
+ }
264
+
265
+ const sub = serviceAnchoredTrades({lnd});
266
+
267
+ const {trade} = encodeTrade({
268
+ connect: {
269
+ network: getNetwork.network,
270
+ nodes: [{channels: [], id: getIdentity.public_key, sockets: []}],
271
+ },
272
+ });
273
+
274
+ logger.info({servicing_all_open_trades: trade});
275
+
276
+ sub.on('settled', async ({description, to, tokens}) => {
277
+ const {alias, id} = await getNodeAlias({lnd, id: to});
278
+
279
+ return logger.info(`sold: ${tokensAsBigUnit(tokens)} ${description} to ${alias} ${to}`);
280
+ });
281
+
282
+ sub.on('start', ({description, tokens}) => {
283
+ return logger.info(`open: ${tokensAsBigUnit(tokens)} ${description}`);
284
+ });
285
+
286
+ sub.on('error', err => {
287
+ sub.removeAllListeners();
288
+
289
+ return cbk(err);
290
+ });
291
+ }],
85
292
  },
86
293
  returnResult({reject, resolve}, cbk));
87
294
  });
@@ -22,6 +22,7 @@ const tradeIdRecordType = '0';
22
22
  auth: <Encrypted Payload Auth Hex String>
23
23
  payload: <Preimage Encrypted Payload Hex String>
24
24
  request: <BOLT 11 Payment Request String>
25
+ trade: <Encoded Trade Record String>
25
26
  }
26
27
  */
27
28
  module.exports = ({id, lnd, to}, cbk) => {
@@ -77,7 +78,12 @@ module.exports = ({id, lnd, to}, cbk) => {
77
78
  }
78
79
 
79
80
  try {
80
- return cbk(null, secret);
81
+ return cbk(null, {
82
+ auth: secret.auth,
83
+ payload: secret.payload,
84
+ request: secret.request,
85
+ trade: tradeRecord.value,
86
+ });
81
87
  } catch (err) {
82
88
  return cbk([503, 'ExpectedValidTradeRecordFromPeer', {err}]);
83
89
  }
@@ -0,0 +1,181 @@
1
+ const EventEmitter = require('events');
2
+
3
+ const asyncUntil = require('async/until');
4
+ const {getInvoices} = require('ln-service');
5
+ const {subscribeToInvoice} = require('ln-service');
6
+ const {subscribeToInvoices} = require('ln-service');
7
+
8
+ const decodeAnchoredTrade = require('./decode_anchored_trade');
9
+ const serviceTradeRequests = require('./service_trade_requests');
10
+ const tradeFromInvoice = require('./trade_from_invoice');
11
+
12
+ const defaultInvoicesLimit = 100;
13
+ const events = ['details', 'error', 'failure', 'settled', 'trade'];
14
+ const fromNow = date => new Date(date) - new Date();
15
+ const {keys} = Object;
16
+
17
+ /** Service trades represented by anchored invoices
18
+
19
+ {
20
+ lnd: <Authenticated LND API Object>
21
+ }
22
+
23
+ // Returned details
24
+ @event 'details'
25
+ {
26
+ to: <Public Key Id Hex String>
27
+ }
28
+
29
+ // Terminal failure
30
+ @event 'error'
31
+ <Error Object>
32
+
33
+ // Encountered a failure
34
+ @event 'failure'
35
+ [<Failure Code Number>, <Failure Code String>]
36
+
37
+ // Received payment
38
+ @event 'settled'
39
+ {
40
+ description: <Trade Description String>
41
+ id: <Trade Id Hex String>
42
+ secret: <Secret Payload String>
43
+ to: <Public Key Id Hex String>
44
+ tokens: <Tokens Number>
45
+ }
46
+
47
+ // Starting service for trade
48
+ @event 'start'
49
+ {
50
+ description: <Trade Description String>
51
+ expires_at: <Trade Expires at ISO 8601 Date String>
52
+ id: <Trade Id Hex String>
53
+ secret: <Secret Payload String>
54
+ tokens: <Tokens Number>
55
+ }
56
+
57
+ // Return finalized trade
58
+ @event 'trade'
59
+ {
60
+ to: <Public Key Id Hex String>
61
+ }
62
+
63
+ @returns
64
+ <EventEmitter Object>
65
+ */
66
+ module.exports = ({lnd}) => {
67
+ const emitter = new EventEmitter();
68
+ const listening = {};
69
+ const paging = {};
70
+
71
+ // Service a trade until it's paid or canceled
72
+ const serviceTrade = trade => {
73
+ // Exit early when already listening to this trade
74
+ if (!!listening[trade.id]) {
75
+ return;
76
+ }
77
+
78
+ const sub = serviceTradeRequests({
79
+ lnd,
80
+ description: trade.description,
81
+ expires_at: trade.expires_at,
82
+ id: trade.id,
83
+ secret: trade.secret,
84
+ tokens: trade.tokens,
85
+ });
86
+
87
+ // Pick up the trade to service
88
+ emitter.emit('start', {
89
+ description: trade.description,
90
+ expires_at: trade.expires_at,
91
+ id: trade.id,
92
+ secret: trade.secret,
93
+ tokens: trade.tokens,
94
+ });
95
+
96
+ // Settled a trade
97
+ sub.on('settled', ({to}) => {
98
+ return emitter.emit('settled', {
99
+ to,
100
+ id: trade.id,
101
+ description: trade.description,
102
+ secret: trade.secret,
103
+ tokens: trade.tokens,
104
+ });
105
+ });
106
+
107
+ // Remember this listener to avoid double-listening to the same trade
108
+ listening[trade.id] = sub;
109
+
110
+ sub.on('details', ({to}) => emitter.emit('details', {to, id: trade.id}));
111
+ sub.on('failure', err => emitter.emit('failure', {err, id: trade.id}));
112
+ sub.on('trade', ({to}) => emitter.emit('trade', {to, id: trade.id}));
113
+
114
+ sub.once('end', () => sub.removeAllListeners());
115
+
116
+ const invoiceUpdates = subscribeToInvoice({lnd, id: trade.id});
117
+
118
+ invoiceUpdates.on('invoice_updated', updated => {
119
+ if (!updated.is_canceled) {
120
+ return;
121
+ }
122
+
123
+ return invoiceUpdates.removeAllListeners();
124
+ });
125
+
126
+ return;
127
+ };
128
+
129
+ // Listen for any new invoices that represent anchored trades
130
+ const sub = subscribeToInvoices({lnd});
131
+
132
+ // Listen for new trades to appear
133
+ sub.on('invoice_updated', invoice => {
134
+ const {trade} = tradeFromInvoice(invoice);
135
+
136
+ // Exit early when there is no anchored trade found in the description
137
+ if (!trade) {
138
+ return;
139
+ }
140
+
141
+ return serviceTrade(trade);
142
+ });
143
+
144
+ // Find open anchored trades
145
+ asyncUntil(
146
+ cbk => cbk(null, paging.token === false),
147
+ cbk => {
148
+ return getInvoices({
149
+ lnd,
150
+ is_unconfirmed: true,
151
+ limit: !paging.token ? defaultInvoicesLimit : undefined,
152
+ token: paging.token,
153
+ },
154
+ (err, res) => {
155
+ if (!!err) {
156
+ emitter.emit(err);
157
+
158
+ // Stop listening to all invoices
159
+ sub.removeAllListeners();
160
+
161
+ // Stop listening to all anchor invoices
162
+ keys(listening).forEach(id => listening[id].removeAllListeners());
163
+
164
+ return;
165
+ }
166
+
167
+ paging.token = res.next || false;
168
+
169
+ res.invoices
170
+ .map(tradeFromInvoice)
171
+ .map(n => n.trade)
172
+ .filter(n => !!n)
173
+ .forEach(serviceTrade);
174
+
175
+ return cbk();
176
+ });
177
+ }
178
+ );
179
+
180
+ return emitter;
181
+ };
@@ -0,0 +1,172 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {getNodeAlias} = require('ln-sync');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const encodeOpenTrade = require('./encode_open_trade');
6
+ const serviceTradeRequests = require('./service_trade_requests');
7
+
8
+ const asNumber = n => parseFloat(n, 10);
9
+ const {isArray} = Array;
10
+ const nodeName = (alias, id) => `${alias} ${id}`;
11
+ const slowTradeDate = () => new Date(Date.now() + (86400000*15)).toISOString();
12
+ const uriAsSocket = n => n.substring(67);
13
+
14
+ /** Service an individual trade
15
+
16
+ {
17
+ channels: [{
18
+ id: <Standard Format Channel Id String>
19
+ partner_public_key: <Node Public Key Hex String>
20
+ }]
21
+ description: <Trade Description String>
22
+ expires_at: <Expires At ISO 8601 Date String>
23
+ id: <Trade Anchor Id Hex String>
24
+ lnd: <Authenticated LND API Object>
25
+ logger: <Winston Logger Object>
26
+ network: <Network Name String>
27
+ public_key: <Identity Public Key Hex String>
28
+ secret: <Secret to Sell String>
29
+ tokens: <Trade Price Tokens Number>
30
+ uris: [<Node URI String>]
31
+ }
32
+
33
+ @returns via cbk or Promise
34
+ */
35
+ module.exports = (args, cbk) => {
36
+ return new Promise((resolve, reject) => {
37
+ return asyncAuto({
38
+ // Check arguments
39
+ validate: cbk => {
40
+ if (!isArray(args.channels)) {
41
+ return cbk([400, 'ExpectedArrayOfChannelsToServiceTrade']);
42
+ }
43
+
44
+ if (!args.description) {
45
+ return cbk([400, 'ExpectedDescriptionToServiceTrade']);
46
+ }
47
+
48
+ if (!args.expires_at) {
49
+ return cbk([400, 'ExpectedExpiresAtDateToServiceTrade']);
50
+ }
51
+
52
+ if (!args.id) {
53
+ return cbk([400, 'ExpectedTradeAnchorIdToServiceTrade']);
54
+ }
55
+
56
+ if (!args.lnd) {
57
+ return cbk([400, 'ExpectedAuthenticatedLndToServiceTrade']);
58
+ }
59
+
60
+ if (!args.logger) {
61
+ return cbk([400, 'ExpectedWinstonLoggerToServiceTrade']);
62
+ }
63
+
64
+ if (!args.network) {
65
+ return cbk([400, 'ExpectedNetworkNameToServiceTrade']);
66
+ }
67
+
68
+ if (!args.public_key) {
69
+ return cbk([400, 'ExpectedIdentityPublicKeyToServiceTrade']);
70
+ }
71
+
72
+ if (!args.secret) {
73
+ return cbk([400, 'ExpectedSecretToSellToServiceTrade']);
74
+ }
75
+
76
+ if (!args.tokens) {
77
+ return cbk([400, 'ExpectedTokensPriceToServiceTrade']);
78
+ }
79
+
80
+ if (!isArray(args.uris)) {
81
+ return cbk([400, 'ExpectedArrayOfNodeSocketUrisToServiceTrade']);
82
+ }
83
+
84
+ return cbk();
85
+ },
86
+
87
+ // Serve the trade
88
+ serve: ['validate', ({}, cbk) => {
89
+ const isSlowTrade = args.expires_at > slowTradeDate();
90
+ const settled = [];
91
+
92
+ // Encode the open trade details to give out
93
+ const openTrade = encodeOpenTrade({
94
+ network: args.network,
95
+ nodes: [{
96
+ channels: isSlowTrade ? [] : args.channels,
97
+ id: args.public_key,
98
+ sockets: !args.channels.length ? args.uris.map(uriAsSocket) : [],
99
+ }],
100
+ });
101
+
102
+ args.logger.info({trade_expiry: new Date(args.expires_at)});
103
+
104
+ const sub = serviceTradeRequests({
105
+ description: args.description,
106
+ expires_at: args.expires_at,
107
+ id: args.id,
108
+ lnd: args.lnd,
109
+ secret: args.secret,
110
+ tokens: asNumber(args.tokens),
111
+ });
112
+
113
+ // Someone asked for trade details
114
+ sub.on('details', async ({to}) => {
115
+ try {
116
+ const {alias, id} = await getNodeAlias({id: to, lnd: args.lnd});
117
+
118
+ const toName = nodeName(alias, id);
119
+
120
+ return args.logger.info({return_trade_info_to: toName});
121
+ } catch (err) {
122
+ return args.logger.error({err});
123
+ }
124
+ });
125
+
126
+ sub.on('failure', failure => args.logger.error({failure}));
127
+
128
+ sub.on('settled', ({to}) => settled.push(to));
129
+
130
+ sub.on('trade', async ({to}) => {
131
+ try {
132
+ const {alias, id} = await getNodeAlias({id: to, lnd: args.lnd});
133
+
134
+ const toName = nodeName(alias, id);
135
+
136
+ return args.logger.info({make_trade_invoice_for: toName});
137
+ } catch (err) {
138
+ return args.logger.error({err});
139
+ }
140
+ });
141
+
142
+ // Show the trade details blob
143
+ return args.logger.info({
144
+ waiting_for_trade_request_to: openTrade.trade,
145
+ });
146
+
147
+ // Wait for the trade to end
148
+ sub.once('end', async () => {
149
+ try {
150
+ const [to] = settled;
151
+
152
+ // Exit early when there was no settlement
153
+ if (!to) {
154
+ args.logger.info({trade_ended: true});
155
+
156
+ return cbk();
157
+ }
158
+
159
+ const {alias, id} = await getNodeAlias({id: to, lnd: args.lnd});
160
+
161
+ args.logger.info({finished_trade_with: nodeName(alias, id)});
162
+ } catch (err) {
163
+ args.logger.error({err});
164
+ } finally {
165
+ return cbk();
166
+ }
167
+ });
168
+ }],
169
+ },
170
+ returnResult({reject, resolve}, cbk));
171
+ });
172
+ };