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,21 +1,23 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const {createInvoice} = require('ln-service');
2
3
  const {getChannels} = require('ln-service');
3
4
  const {getNetwork} = require('ln-sync');
4
- const {getNodeAlias} = require('ln-sync');
5
5
  const {getWalletInfo} = require('ln-service');
6
6
  const {returnResult} = require('asyncjs-util');
7
7
 
8
- const encodeOpenTrade = require('./encode_open_trade');
8
+ const encodeAnchoredTrade = require('./encode_anchored_trade');
9
9
  const finalizeTradeSecret = require('./finalize_trade_secret');
10
- const serviceTradeRequests = require('./service_trade_requests');
10
+ const serviceOpenTrade = require('./service_open_trade');
11
11
 
12
12
  const asNumber = n => parseFloat(n, 10);
13
+ const daysAsMs = days => Number(days) * 1000 * 60 * 60 * 24;
14
+ const defaultExpirationDays = 14;
13
15
  const {floor} = Math;
16
+ const futureDate = ms => new Date(Date.now() + ms).toISOString();
14
17
  const isNumber = n => !isNaN(n);
15
18
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
16
19
  const maxDescriptionLength = 100;
17
20
  const maxSecretLength = 100;
18
- const nodeName = (alias, id) => `${alias} ${id}`;
19
21
  const uriAsSocket = n => n.substring(67);
20
22
  const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
21
23
 
@@ -164,20 +166,76 @@ module.exports = ({ask, lnd, logger}, cbk) => {
164
166
  cbk);
165
167
  }],
166
168
 
169
+ // Ask for the expiration of the trade
170
+ askForExpiration: ['askForPrice', ({}, cbk) => {
171
+ return ask({
172
+ default: defaultExpirationDays,
173
+ name: 'days',
174
+ message: 'Days to offer this for?',
175
+ validate: input => {
176
+ if (!isNumber(input) || !Number(input)) {
177
+ return false;
178
+ }
179
+
180
+ return true;
181
+ },
182
+ },
183
+ cbk);
184
+ }],
185
+
186
+ // Create an anchor invoice for the trade
187
+ createAnchor: [
188
+ 'askForDescription',
189
+ 'askForExpiration',
190
+ 'askForNodeId',
191
+ 'askForPrice',
192
+ 'askForSecret',
193
+ ({
194
+ askForDescription,
195
+ askForExpiration,
196
+ askForNodeId,
197
+ askForPrice,
198
+ askForSecret,
199
+ },
200
+ cbk) =>
201
+ {
202
+ // Exit early if this is a closed trade
203
+ if (!!askForNodeId.id) {
204
+ return cbk();
205
+ }
206
+
207
+ const {encoded} = encodeAnchoredTrade({
208
+ description: askForDescription.description,
209
+ secret: askForSecret.secret,
210
+ });
211
+
212
+ return createInvoice({
213
+ lnd,
214
+ description: encoded,
215
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
216
+ tokens: asNumber(askForPrice.tokens),
217
+ },
218
+ cbk);
219
+ }],
220
+
167
221
  // Wait for a peer to connect and ask for the trade details
168
222
  serviceTradeRequests: [
169
223
  'askForDescription',
224
+ 'askForExpiration',
170
225
  'askForNodeId',
171
226
  'askForPrice',
172
227
  'askForSecret',
228
+ 'createAnchor',
173
229
  'getChannels',
174
230
  'getIdentity',
175
231
  'getNetwork',
176
232
  ({
177
233
  askForDescription,
234
+ askForExpiration,
178
235
  askForNodeId,
179
236
  askForPrice,
180
237
  askForSecret,
238
+ createAnchor,
181
239
  getChannels,
182
240
  getIdentity,
183
241
  getNetwork,
@@ -189,64 +247,37 @@ module.exports = ({ask, lnd, logger}, cbk) => {
189
247
  return cbk();
190
248
  }
191
249
 
192
- // Encode the open trade details to give out
193
- const openTrade = encodeOpenTrade({
194
- network: getNetwork.network,
195
- nodes: [{
196
- channels: getChannels.channels,
197
- id: getIdentity.public_key,
198
- sockets: (getIdentity.uris || []).map(uriAsSocket),
199
- }],
200
- });
201
-
202
- const settled = [];
203
-
204
- const sub = serviceTradeRequests({
250
+ return serviceOpenTrade({
205
251
  lnd,
252
+ logger,
253
+ channels: getChannels.channels,
206
254
  description: askForDescription.description,
255
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
256
+ id: createAnchor.id,
257
+ network: getNetwork.network,
258
+ public_key: getIdentity.public_key,
207
259
  secret: askForSecret.secret,
208
260
  tokens: asNumber(askForPrice.tokens),
209
- });
210
-
211
- sub.on('details', async ({to}) => {
212
- const {alias, id} = await getNodeAlias({lnd, id: to});
213
-
214
- return logger.info({return_trade_info_to: nodeName(alias, id)});
215
- });
216
-
217
- sub.once('end', async () => {
218
- const [to] = settled;
219
-
220
- if (!!to) {
221
- const {alias, id} = await getNodeAlias({lnd, id: to});
222
-
223
- return logger.info({finished_trade_with: nodeName(alias, id)});
224
- }
225
-
226
- return cbk();
227
- });
228
-
229
- sub.on('failure', failure => logger.error({failure}));
230
-
231
- sub.on('settled', ({to}) => settled.push(to));
232
-
233
- sub.on('trade', async ({to}) => {
234
- const {alias, id} = await getNodeAlias({lnd, id: to});
235
-
236
- return logger.info({make_trade_invoice_for: nodeName(alias, id)});
237
- });
238
-
239
- // Show the trade details blob
240
- return logger.info({waiting_for_trade_request_to: openTrade.trade});
261
+ uris: (getIdentity.uris || []),
262
+ },
263
+ cbk);
241
264
  }],
242
265
 
243
266
  // Finalize the trade with an encrypted secret and an invoice
244
267
  finalize: [
245
268
  'askForDescription',
269
+ 'askForExpiration',
246
270
  'askForNodeId',
247
271
  'askForPrice',
248
272
  'askForSecret',
249
- ({askForDescription, askForNodeId, askForPrice, askForSecret}, cbk) =>
273
+ ({
274
+ askForDescription,
275
+ askForExpiration,
276
+ askForNodeId,
277
+ askForPrice,
278
+ askForSecret,
279
+ },
280
+ cbk) =>
250
281
  {
251
282
  // Exit early when there is no node to finalize the trade to
252
283
  if (!askForNodeId.id) {
@@ -256,6 +287,7 @@ module.exports = ({ask, lnd, logger}, cbk) => {
256
287
  return finalizeTradeSecret({
257
288
  lnd,
258
289
  description: askForDescription.description,
290
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
259
291
  secret: askForSecret.secret,
260
292
  to: askForNodeId.id,
261
293
  tokens: asNumber(askForPrice.tokens),
@@ -0,0 +1,56 @@
1
+ const {decodeTlvStream} = require('bolt01');
2
+
3
+ const anchorPrefix = 'anchor-trade-secret:';
4
+ const base64AsHex = base64 => Buffer.from(base64, 'base64').toString('hex');
5
+ const findDescription = records => records.find(n => n.type === '1');
6
+ const findSecret = records => records.find(n => n.type === '0');
7
+ const hexAsUtf = n => !n ? '' : Buffer.from(n.value, 'hex').toString('utf8');
8
+
9
+ /** Decode an anchored trade data blob
10
+
11
+ Data blobs look like anchored-trade-secret:<base64-encoded-data>
12
+
13
+ Inside the base64 is encoded TLV data:
14
+ 0: <Trade Secret>
15
+ [1]: <Trade Description>
16
+
17
+ {
18
+ encoded: <Encoded Trade Data String>
19
+ }
20
+
21
+ @returns
22
+ {
23
+ [trade]: {
24
+ description: <Description of Trade String>
25
+ secret: <Trade Secret String>
26
+ }
27
+ }
28
+ */
29
+ module.exports = ({encoded}) => {
30
+ if (!encoded.startsWith(anchorPrefix)) {
31
+ return {};
32
+ }
33
+
34
+ const data = encoded.slice(anchorPrefix.length);
35
+
36
+ try {
37
+ decodeTlvStream({encoded: base64AsHex(data)});
38
+ } catch (err) {
39
+ return {};
40
+ }
41
+
42
+ const {records} = decodeTlvStream({encoded: base64AsHex(data)});
43
+
44
+ const secret = findSecret(records);
45
+
46
+ if (!secret.value) {
47
+ return {};
48
+ }
49
+
50
+ return {
51
+ trade: {
52
+ description: hexAsUtf(findDescription(records)),
53
+ secret: hexAsUtf(secret),
54
+ },
55
+ };
56
+ };
@@ -0,0 +1,35 @@
1
+ const {encodeTlvStream} = require('bolt01');
2
+
3
+ const anchorPrefix = 'anchor-trade-secret:';
4
+ const descriptionRecord = value => !!value ? ({value, type: '1'}) : undefined;
5
+ const secretRecord = value => ({value, type: '0'});
6
+ const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
7
+ const withPrefix = (pre, v) => pre + Buffer.from(v, 'hex').toString('base64');
8
+
9
+ /** Encode a trade into an anchor blob
10
+
11
+ This will look like anchored-trade-secret:<base64-encoded-data>
12
+
13
+ The data encodes a TLV:
14
+ 0: <Trade Secret>
15
+ [1]: <Trade Description>
16
+
17
+ {
18
+ [description]: <Open Trade Description String>
19
+ secret: <Open Trade Secret String>
20
+ }
21
+
22
+ @returns
23
+ {
24
+ encoded: <Encoded Anchor Trade Data Blob String>
25
+ }
26
+ */
27
+ module.exports = ({description, secret}) => {
28
+ const records = [secretRecord(secret), descriptionRecord(description)]
29
+ .filter(n => !!n)
30
+ .map(({type, value}) => ({type, value: utf8AsHex(value)}));
31
+
32
+ const {encoded} = encodeTlvStream({records});
33
+
34
+ return {encoded: withPrefix(anchorPrefix, encoded)};
35
+ };
@@ -17,6 +17,7 @@ const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
17
17
 
18
18
  {
19
19
  description: <Trade Description String>
20
+ expires_at: <Trade Expires At ISO 8601 Date String>
20
21
  is_hold: <Create Hold Invoice Bool>
21
22
  lnd: <Authenticated LND API Object>
22
23
  secret: <Clear Secret String>
@@ -40,6 +41,10 @@ module.exports = (args, cbk) => {
40
41
  return cbk([400, 'ExpectedDescriptionToFinalizeTradeSecret']);
41
42
  }
42
43
 
44
+ if (!args.expires_at) {
45
+ return cbk([400, 'ExpectedExpiresAtDateToFinalizeTradeSecret']);
46
+ }
47
+
43
48
  if (!args.lnd) {
44
49
  return cbk([400, 'ExpectedAuthenticatedLndToFinalizeTradeSecret']);
45
50
  }
@@ -75,6 +80,7 @@ module.exports = (args, cbk) => {
75
80
  if (!!args.is_hold) {
76
81
  return createHodlInvoice({
77
82
  description: args.description,
83
+ expires_at: args.expires_at,
78
84
  id: bufferAsHex(sha256(hexAsBuffer(encrypt.payment_secret))),
79
85
  lnd: args.lnd,
80
86
  secret: encrypt.payment_secret,
@@ -85,6 +91,7 @@ module.exports = (args, cbk) => {
85
91
 
86
92
  return createInvoice({
87
93
  description: args.description,
94
+ expires_at: args.expires_at,
88
95
  lnd: args.lnd,
89
96
  secret: encrypt.payment_secret,
90
97
  tokens: args.tokens,
@@ -24,6 +24,8 @@ const makeRequestId = () => randomBytes(32).toString('hex');
24
24
  const requestTradeType = '805005';
25
25
  const requestTradesTimeoutMs = 1000 * 30;
26
26
  const tradesRequestIdType = '1';
27
+ const uniq = arr => Array.from(new Set(arr));
28
+ const uniqBy = (a,b) => a.filter((e,i) => a.findIndex(n => n[b] == e[b]) == i);
27
29
  const waitForTradesMs = 1000 * 5;
28
30
 
29
31
  /** Find a trade to purchase
@@ -49,6 +51,7 @@ const waitForTradesMs = 1000 * 5;
49
51
  auth: <Encrypted Payload Auth Hex String>
50
52
  payload: <Preimage Encrypted Payload Hex String>
51
53
  request: <BOLT 11 Payment Request String>
54
+ trade: <Encoded Trade String>
52
55
  }
53
56
  */
54
57
  module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
@@ -89,7 +92,23 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
89
92
 
90
93
  // Exit early when the node id and sockets is specified directly
91
94
  if (!!connect.node) {
92
- return cbk(null, connect.node);
95
+ return getNode({
96
+ lnd,
97
+ is_omitting_channels: true,
98
+ public_key: connect.node.id,
99
+ },
100
+ (err, res) => {
101
+ if (!!err) {
102
+ return cbk(null, connect.node);
103
+ }
104
+
105
+ const sockets = connect.node.sockets.map(n => n.socket);
106
+
107
+ return cbk(null, {
108
+ id: connect.node.id,
109
+ sockets: uniq([].concat(sockets).concat(connect.node.sockets)),
110
+ });
111
+ });
93
112
  }
94
113
 
95
114
  // Exit early when there is no landmark channel
@@ -156,6 +175,8 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
156
175
 
157
176
  // Try and connect to a referenced node
158
177
  return asyncDetect(getNodes.filter(n => !!n), (node, cbk) => {
178
+ logger.info({connecting_to: node.id});
179
+
159
180
  // Attempt referenced sockets to establish p2p connection
160
181
  return asyncDetectSeries(node.sockets, (socket, cbk) => {
161
182
  return addPeer({lnd, socket, public_key: node.id}, err => {
@@ -238,6 +259,8 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
238
259
  return res.success({});
239
260
  });
240
261
 
262
+ logger.info({requesting_trade_details: true});
263
+
241
264
  // Once connected, ask for the trade details
242
265
  return makePeerRequest({
243
266
  lnd,
@@ -270,7 +293,9 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
270
293
  return cbk([404, 'NoTradesFound']);
271
294
  }
272
295
 
273
- const [trade, other] = requestTrades.trades;
296
+ const uniqueTrades = uniqBy(requestTrades.trades, 'description');
297
+
298
+ const [trade, other] = uniqueTrades;
274
299
 
275
300
  // Exit early when there is only a single trade
276
301
  if (!other) {
@@ -279,7 +304,7 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
279
304
 
280
305
  // Present the possible choices
281
306
  return ask({
282
- choices: requestTrades.trades.map(trade => ({
307
+ choices: uniqueTrades.map(trade => ({
283
308
  name: trade.description,
284
309
  value: trade.id,
285
310
  })),
@@ -16,6 +16,7 @@ const findTrade = require('./find_trade');
16
16
  const hexAsUtf8 = hex => Buffer.from(hex, 'hex').toString();
17
17
  const {isArray} = Array;
18
18
  const isHexPreimage = n => !!n && /^[0-9A-F]{64}$/i.test(n);
19
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
19
20
 
20
21
  /** Manage an individual trade
21
22
 
@@ -141,7 +142,7 @@ module.exports = ({ask, lnd, logger}, cbk) => {
141
142
  logger.info({
142
143
  id: requestDetails.id,
143
144
  offering: requestDetails.description,
144
- price: requestDetails.tokens,
145
+ price: tokensAsBigUnit(requestDetails.tokens),
145
146
  is_canceled: invoice.is_canceled || undefined,
146
147
  is_purchased: invoice.is_confirmed || undefined,
147
148
  });
@@ -169,7 +170,7 @@ module.exports = ({ask, lnd, logger}, cbk) => {
169
170
  logger.info({
170
171
  id: requestDetails.id,
171
172
  offering: requestDetails.description,
172
- price: requestDetails.tokens,
173
+ price: tokensAsBigUnit(requestDetails.tokens),
173
174
  });
174
175
 
175
176
  return ask({
@@ -195,7 +196,15 @@ module.exports = ({ask, lnd, logger}, cbk) => {
195
196
  return cbk();
196
197
  }
197
198
 
198
- return cancelHodlInvoice({lnd, id: requestDetails.id}, cbk);
199
+ return cancelHodlInvoice({lnd, id: requestDetails.id}, err => {
200
+ if (!!err) {
201
+ return cbk(err);
202
+ }
203
+
204
+ logger.info({trade_canceled: true});
205
+
206
+ return cbk();
207
+ });
199
208
  }],
200
209
 
201
210
  // Select a purchase option for the trade
@@ -216,10 +225,12 @@ module.exports = ({ask, lnd, logger}, cbk) => {
216
225
  return cbk();
217
226
  }
218
227
 
228
+ logger.info({trade: findTrade.trade});
229
+
219
230
  logger.info({
220
231
  request: findTrade.request,
221
232
  purchase: requestDetails.description,
222
- price: requestDetails.tokens,
233
+ price: tokensAsBigUnit(requestDetails.tokens),
223
234
  to: requestDetails.destination,
224
235
  });
225
236