paid-services 3.3.0 → 3.6.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 (54) hide show
  1. package/.prettierignore +2 -0
  2. package/CHANGELOG.md +17 -0
  3. package/capacity/accept_capacity_change.js +3 -3
  4. package/capacity/ask_for_change_details.js +34 -1
  5. package/capacity/capacity_change_requests.js +9 -0
  6. package/capacity/change_channel_capacity.js +7 -1
  7. package/capacity/encode_change_request.js +8 -1
  8. package/capacity/fee_for_replacement.js +1 -1
  9. package/capacity/get_capacity_change_requests.js +2 -1
  10. package/capacity/get_capacity_replacement.js +1 -1
  11. package/capacity/initiate_capacity_change.js +5 -5
  12. package/capacity/parse_capacity_change_request.js +8 -1
  13. package/capacity/propose_capacity_change.js +4 -1
  14. package/p2p/service_peer_requests.js +10 -2
  15. package/package.json +10 -9
  16. package/service_types.json +16 -0
  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 +50 -14
  22. package/test/integration/test_change_channel_capacity.js +7 -1
  23. package/test/integration/test_decrease_channel_capacity.js +16 -7
  24. package/test/integration/test_increase_channel_capacity.js +4 -1
  25. package/test/trades/test_decode_basic_trade.js +40 -0
  26. package/test/trades/test_decode_open_trade.js +50 -0
  27. package/test/trades/test_encode_open_trade.js +35 -0
  28. package/test/trades/test_encode_trade.js +37 -0
  29. package/trades/accept_trade.js +61 -0
  30. package/trades/create_trade.js +149 -37
  31. package/trades/decode_anchored_trade.js +56 -0
  32. package/trades/decode_basic_trade.js +45 -0
  33. package/trades/decode_open_trade.js +69 -0
  34. package/trades/decode_trade.js +48 -66
  35. package/trades/decode_trade_secret.js +89 -0
  36. package/trades/encode_anchored_trade.js +35 -0
  37. package/trades/encode_open_trade.js +89 -0
  38. package/trades/encode_trade.js +48 -33
  39. package/trades/encode_trade_secret.js +50 -0
  40. package/trades/finalize_trade_secret.js +123 -0
  41. package/trades/find_trade.js +339 -0
  42. package/trades/manage_trade.js +61 -39
  43. package/trades/manage_trades.js +209 -2
  44. package/trades/network_from_network_record.js +3 -3
  45. package/trades/network_record_from_network.js +36 -0
  46. package/trades/network_record_from_request.js +2 -1
  47. package/trades/node_as_record.js +98 -0
  48. package/trades/record_as_node.js +113 -0
  49. package/trades/request_trade_by_id.js +95 -0
  50. package/trades/service_anchored_trades.js +181 -0
  51. package/trades/service_open_trade.js +172 -0
  52. package/trades/service_trade_requests.js +255 -0
  53. package/trades/trade_from_invoice.js +51 -0
  54. package/capacity/service_types.json +0 -6
@@ -27,7 +27,6 @@ const logger = {error: () => {}, info: () => {}};
27
27
  const maturity = 100;
28
28
  const size = 2;
29
29
  const slow = 3000;
30
- const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
31
30
  const times = 2000;
32
31
  const {toOutputScript} = address;
33
32
  const weightAsVBytes = n => Math.ceil(n / 4);
@@ -47,11 +46,13 @@ test(`Decrease capacity replacement`, async ({end, equal, strictSame}) => {
47
46
 
48
47
  try {
49
48
  // Open up a new channel
50
- const channelOpen = await openChannel({
51
- lnd,
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
+ local_tokens: capacity,
53
+ partner_public_key: target.id,
54
+ partner_socket: target.socket,
55
+ });
55
56
  });
56
57
 
57
58
  // Wait for the channel to be active
@@ -116,6 +117,10 @@ test(`Decrease capacity replacement`, async ({end, equal, strictSame}) => {
116
117
  return cbk({query: target.id});
117
118
  }
118
119
 
120
+ if (args.name === 'type') {
121
+ return cbk({type: 'Private'});
122
+ }
123
+
119
124
  throw new Error('UnexpectedQueryNameForProposingSide');
120
125
  },
121
126
  });
@@ -159,7 +164,9 @@ test(`Decrease capacity replacement`, async ({end, equal, strictSame}) => {
159
164
  interval: slow,
160
165
  },
161
166
  async () => {
162
- const [channel] = (await getChannels({lnd, is_active: true})).channels.filter(n => n.capacity < capacity);
167
+ const {channels} = await getChannels({lnd, is_active: true});
168
+
169
+ const [channel] = channels.filter(n => n.capacity < capacity);
163
170
 
164
171
  await generate({});
165
172
 
@@ -167,6 +174,8 @@ test(`Decrease capacity replacement`, async ({end, equal, strictSame}) => {
167
174
  throw new Error('ExpectedChannelActivation');
168
175
  }
169
176
 
177
+ equal(channel.is_private, true, 'Channel is changed to private');
178
+
170
179
  {
171
180
  const {policies} = await getChannel({lnd, id: channel.id});
172
181
 
@@ -26,7 +26,6 @@ const interval = 10;
26
26
  const logger = {error: () => {}, info: () => {}};
27
27
  const maturity = 100;
28
28
  const size = 2;
29
- const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
30
29
  const times = 2000;
31
30
  const {toOutputScript} = address;
32
31
  const weightAsVBytes = n => Math.ceil(n / 4);
@@ -111,6 +110,10 @@ test(`Accept capacity replacement`, async ({end, equal, strictSame}) => {
111
110
  return cbk({query: target.id});
112
111
  }
113
112
 
113
+ if (args.name === 'type') {
114
+ return cbk({type: args.default});
115
+ }
116
+
114
117
  throw new Error('UnexpectedQueryNameForProposingSide');
115
118
  },
116
119
  });
@@ -0,0 +1,40 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../trades/decode_basic_trade');
4
+
5
+ const tests = [
6
+ {
7
+ args: {},
8
+ description: 'Records are expected',
9
+ error: 'ExpectedArrayOfRecordsToDecodeBasicTrade',
10
+ },
11
+ {
12
+ args: {records: []},
13
+ description: 'A description record is expected',
14
+ error: 'ExpectedDescriptionRecordToDecodeBasicTrade',
15
+ },
16
+ {
17
+ args: {records: [{type: '2', value: '00'}]},
18
+ description: 'An id record is expected',
19
+ error: 'ExpectedIdRecordToDecodeBasicTradeDetails',
20
+ },
21
+ {
22
+ args: {records: [{type: '1', value: '00'}, {type: '2', value: '00'}]},
23
+ description: 'Basic trade is encoded',
24
+ expected: {description: '\u0000', id: '00'},
25
+ },
26
+ ];
27
+
28
+ tests.forEach(({args, description, error, expected}) => {
29
+ return test(description, async ({end, strictSame, throws}) => {
30
+ if (!!error) {
31
+ throws(() => method(args), new Error(error), 'Got error');
32
+ } else {
33
+ const res = method(args);
34
+
35
+ strictSame(res, expected, 'Got expected result');
36
+ }
37
+
38
+ return end();
39
+ });
40
+ });
@@ -0,0 +1,50 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../trades/decode_open_trade');
4
+
5
+ const tests = [
6
+ {
7
+ args: {},
8
+ description: 'A network name is expected',
9
+ error: 'ExpectedNetworkNameToDecodeOpenTrade',
10
+ },
11
+ {
12
+ args: {network: 'btc'},
13
+ description: 'Records are expected',
14
+ error: 'ExpectedArrayOfRecordsToDecodeOpenTrade',
15
+ },
16
+ {
17
+ args: {network: 'btc', records: []},
18
+ description: 'Nodes are expected',
19
+ error: 'ExpectedNodesRecordToDecodeOpenTradeDetails',
20
+ },
21
+ {
22
+ args: {
23
+ network: 'btc',
24
+ records: [
25
+ {type: '0', value: '01'},
26
+ {type: '4', value: '000a01080000010000020003'},
27
+ ],
28
+ },
29
+ description: 'Open trade is decoded',
30
+ expected: {
31
+ id: undefined,
32
+ network: 'btc',
33
+ nodes: [{high_channel: '1x2x3'}],
34
+ },
35
+ },
36
+ ];
37
+
38
+ tests.forEach(({args, description, error, expected}) => {
39
+ return test(description, async ({end, strictSame, throws}) => {
40
+ if (!!error) {
41
+ throws(() => method(args), new Error(error), 'Got error');
42
+ } else {
43
+ const res = method(args);
44
+
45
+ strictSame(res, expected, 'Got expected result');
46
+ }
47
+
48
+ return end();
49
+ });
50
+ });
@@ -0,0 +1,35 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../trades/encode_open_trade');
4
+
5
+ const tests = [
6
+ {
7
+ args: {
8
+ network: 'btc',
9
+ nodes: [{
10
+ channels: [{
11
+ id: '1x2x3',
12
+ partner_public_key: Buffer.alloc(33, 2).toString('hex'),
13
+ }],
14
+ id: Buffer.alloc(33, 3).toString('hex'),
15
+ sockets: [],
16
+ }],
17
+ },
18
+ description: 'Open trade is encoded',
19
+ expected: {trade: '626f73ff000101040c000a01080000010000020003'},
20
+ },
21
+ ];
22
+
23
+ tests.forEach(({args, description, error, expected}) => {
24
+ return test(description, async ({end, strictSame, throws}) => {
25
+ if (!!error) {
26
+ throws(() => method(args), new Error(error), 'Got error');
27
+ } else {
28
+ const res = method(args);
29
+
30
+ strictSame(res, expected, 'Got expected result');
31
+ }
32
+
33
+ return end();
34
+ });
35
+ });
@@ -0,0 +1,37 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../trades/encode_trade');
4
+
5
+ const tests = [
6
+ {
7
+ args: {
8
+ connect: {
9
+ network: 'btc',
10
+ nodes: [{
11
+ channels: [{
12
+ id: '1x2x3',
13
+ partner_public_key: Buffer.alloc(33, 2).toString('hex'),
14
+ }],
15
+ id: Buffer.alloc(33, 3).toString('hex'),
16
+ sockets: [],
17
+ }],
18
+ },
19
+ },
20
+ description: 'Trade is encoded',
21
+ expected: {trade: '626f73ff000101040c000a01080000010000020003'},
22
+ },
23
+ ];
24
+
25
+ tests.forEach(({args, description, error, expected}) => {
26
+ return test(description, async ({end, strictSame, throws}) => {
27
+ if (!!error) {
28
+ throws(() => method(args), new Error(error), 'Got error');
29
+ } else {
30
+ const res = method(args);
31
+
32
+ strictSame(res, expected, 'Got expected result');
33
+ }
34
+
35
+ return end();
36
+ });
37
+ });
@@ -0,0 +1,61 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncEach = require('async/each');
3
+ const {cancelHodlInvoice} = require('ln-service');
4
+ const {getInvoice} = require('ln-service');
5
+ const {returnResult} = require('asyncjs-util');
6
+ const {settleHodlInvoice} = require('ln-service');
7
+
8
+ const {isArray} = Array;
9
+
10
+ /** Accept an open ended trade
11
+
12
+ {
13
+ cancel: [<Alternative Invoice Id Hex String>]
14
+ id: <Trade Id Hex String>
15
+ lnd: <Authenticated LND API Object>
16
+ secret: <Invoice to Settle Preimage Hex String>
17
+ }
18
+
19
+ @returns via cbk or Promise
20
+ */
21
+ module.exports = ({cancel, id, lnd, secret}, cbk) => {
22
+ return new Promise((resolve, reject) => {
23
+ return asyncAuto({
24
+ // Check arguments
25
+ validate: cbk => {
26
+ if (!isArray(cancel)) {
27
+ return cbk([400, 'ExpectedArrayOfIdsToCancelToAcceptTrade']);
28
+ }
29
+
30
+ if (!id) {
31
+ return cbk([400, 'ExpectedAnchorTradeIdToAcceptTrade']);
32
+ }
33
+
34
+ if (!lnd) {
35
+ return cbk([400, 'ExpectedAuthenticatedLndToAcceptTrade']);
36
+ }
37
+
38
+ if (!secret) {
39
+ return cbk([400, 'ExpectedSettlementPreimageToAcceptTrade']);
40
+ }
41
+
42
+ return cbk();
43
+ },
44
+
45
+ // Fetch the anchor invoice to make sure it's still open
46
+ getAnchor: ['validate', ({}, cbk) => getInvoice({id, lnd}, cbk)],
47
+
48
+ // Cancel alternative invoices so that only one resolves as settled
49
+ cancel: ['getAnchor', ({}, cbk) => {
50
+ return asyncEach([].concat(cancel).concat(id), (id, cbk) => {
51
+ return cancelHodlInvoice({id, lnd}, cbk);
52
+ },
53
+ cbk);
54
+ }],
55
+
56
+ // Settle the held invoice with the preimage
57
+ settle: ['cancel', ({}, cbk) => settleHodlInvoice({lnd, secret}, cbk)],
58
+ },
59
+ returnResult({reject, resolve}, cbk));
60
+ });
61
+ };
@@ -1,17 +1,24 @@
1
1
  const asyncAuto = require('async/auto');
2
2
  const {createInvoice} = require('ln-service');
3
- const {getIdentity} = require('ln-service');
3
+ const {getChannels} = require('ln-service');
4
+ const {getNetwork} = require('ln-sync');
5
+ const {getWalletInfo} = require('ln-service');
4
6
  const {returnResult} = require('asyncjs-util');
5
7
 
6
- const encodeTrade = require('./encode_trade');
7
- const encryptTradeSecret = require('./encrypt_trade_secret');
8
+ const encodeAnchoredTrade = require('./encode_anchored_trade');
9
+ const finalizeTradeSecret = require('./finalize_trade_secret');
10
+ const serviceOpenTrade = require('./service_open_trade');
8
11
 
9
12
  const asNumber = n => parseFloat(n, 10);
13
+ const daysAsMs = days => Number(days) * 1000 * 60 * 60 * 24;
14
+ const defaultExpirationDays = 14;
10
15
  const {floor} = Math;
16
+ const futureDate = ms => new Date(Date.now() + ms).toISOString();
11
17
  const isNumber = n => !isNaN(n);
12
18
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
13
19
  const maxDescriptionLength = 100;
14
20
  const maxSecretLength = 100;
21
+ const uriAsSocket = n => n.substring(67);
15
22
  const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
16
23
 
17
24
  /** Create a new trade
@@ -19,6 +26,7 @@ const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
19
26
  {
20
27
  ask: <Ask Function>
21
28
  lnd: <Authenticated LND API Object>
29
+ logger: <Winston Logger Object>
22
30
  }
23
31
 
24
32
  @returns via cbk or Promise
@@ -26,7 +34,7 @@ const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
26
34
  trade: <Hex Encoded Trade String>
27
35
  }
28
36
  */
29
- module.exports = ({ask, lnd}, cbk) => {
37
+ module.exports = ({ask, lnd, logger}, cbk) => {
30
38
  return new Promise((resolve, reject) => {
31
39
  return asyncAuto({
32
40
  // Check arguments
@@ -39,28 +47,32 @@ module.exports = ({ask, lnd}, cbk) => {
39
47
  return cbk([400, 'ExpectedAuthenticatedLndToCreateTrade']);
40
48
  }
41
49
 
50
+ if (!logger) {
51
+ return cbk([400, 'ExpectedWinstonLoggerToCreateTrade']);
52
+ }
53
+
42
54
  return cbk();
43
55
  },
44
56
 
45
57
  // Get the node identity key
46
- getNodeId: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
58
+ getIdentity: ['validate', ({}, cbk) => getWalletInfo({lnd}, cbk)],
47
59
 
48
60
  // Ask for the public key of the node to trade with
49
- askForNodeId: ['getNodeId', ({getNodeId}, cbk) => {
61
+ askForNodeId: ['getIdentity', ({getIdentity}, cbk) => {
50
62
  return ask({
51
63
  name: 'id',
52
- message: 'Public key of node you are trading with?',
64
+ message: 'Public key of node you are trading with? (Optional)',
53
65
  type: 'input',
54
66
  validate: input => {
55
67
  if (!input) {
56
- return false;
68
+ return true;
57
69
  }
58
70
 
59
71
  if (!isPublicKey(input)) {
60
72
  return 'Expected public key of node to trade with';
61
73
  }
62
74
 
63
- if (input === getNodeId.public_key) {
75
+ if (input === getIdentity.public_key) {
64
76
  return 'Expected public key of other node';
65
77
  }
66
78
 
@@ -70,6 +82,26 @@ module.exports = ({ask, lnd}, cbk) => {
70
82
  cbk);
71
83
  }],
72
84
 
85
+ // Get the public channels to use for an open trade
86
+ getChannels: ['askForNodeId', ({askForNodeId}, cbk) => {
87
+ // Exit early when there is a specific node
88
+ if (!!askForNodeId.id) {
89
+ return cbk();
90
+ }
91
+
92
+ return getChannels({lnd, is_public: true}, cbk);
93
+ }],
94
+
95
+ // Get the network name to use for an open trade
96
+ getNetwork: ['askForNodeId', ({askForNodeId}, cbk) => {
97
+ // Exit early when there is a specific node
98
+ if (!!askForNodeId.id) {
99
+ return cbk();
100
+ }
101
+
102
+ return getNetwork({lnd}, cbk);
103
+ }],
104
+
73
105
  // Ask for how to describe the payload
74
106
  askForDescription: ['askForNodeId', ({}, cbk) => {
75
107
  return ask({
@@ -134,55 +166,135 @@ module.exports = ({ask, lnd}, cbk) => {
134
166
  cbk);
135
167
  }],
136
168
 
137
- // Encrypt the secret data
138
- encryptPayload: [
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',
139
190
  'askForNodeId',
191
+ 'askForPrice',
140
192
  'askForSecret',
141
- ({askForNodeId, askForSecret}, cbk) =>
193
+ ({
194
+ askForDescription,
195
+ askForExpiration,
196
+ askForNodeId,
197
+ askForPrice,
198
+ askForSecret,
199
+ },
200
+ cbk) =>
142
201
  {
143
- return encryptTradeSecret({
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({
144
213
  lnd,
145
- secret: utf8AsHex(askForSecret.secret),
146
- to: askForNodeId.id,
214
+ description: encoded,
215
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
216
+ tokens: asNumber(askForPrice.tokens),
147
217
  },
148
218
  cbk);
149
219
  }],
150
220
 
151
- // Create the invoice to purchase the unlocking secret
152
- createPurchaseInvoice: [
221
+ // Wait for a peer to connect and ask for the trade details
222
+ serviceTradeRequests: [
153
223
  'askForDescription',
224
+ 'askForExpiration',
225
+ 'askForNodeId',
154
226
  'askForPrice',
155
- 'encryptPayload',
156
- ({askForDescription, askForPrice, encryptPayload}, cbk) =>
227
+ 'askForSecret',
228
+ 'createAnchor',
229
+ 'getChannels',
230
+ 'getIdentity',
231
+ 'getNetwork',
232
+ ({
233
+ askForDescription,
234
+ askForExpiration,
235
+ askForNodeId,
236
+ askForPrice,
237
+ askForSecret,
238
+ createAnchor,
239
+ getChannels,
240
+ getIdentity,
241
+ getNetwork,
242
+ },
243
+ cbk) =>
157
244
  {
158
- return createInvoice({
245
+ // Exit early if this is a closed trade
246
+ if (!!askForNodeId.id) {
247
+ return cbk();
248
+ }
249
+
250
+ return serviceOpenTrade({
159
251
  lnd,
252
+ logger,
253
+ channels: getChannels.channels,
160
254
  description: askForDescription.description,
161
- secret: encryptPayload.payment_secret,
255
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
256
+ id: createAnchor.id,
257
+ network: getNetwork.network,
258
+ public_key: getIdentity.public_key,
259
+ secret: askForSecret.secret,
162
260
  tokens: asNumber(askForPrice.tokens),
261
+ uris: (getIdentity.uris || []),
163
262
  },
164
263
  cbk);
165
264
  }],
166
265
 
167
- // Encode all the trade data into wire format
168
- encodeTradeToWireFormat: [
169
- 'createPurchaseInvoice',
170
- 'encryptPayload',
171
- ({createPurchaseInvoice, encryptPayload}, cbk) =>
266
+ // Finalize the trade with an encrypted secret and an invoice
267
+ finalize: [
268
+ 'askForDescription',
269
+ 'askForExpiration',
270
+ 'askForNodeId',
271
+ 'askForPrice',
272
+ 'askForSecret',
273
+ ({
274
+ askForDescription,
275
+ askForExpiration,
276
+ askForNodeId,
277
+ askForPrice,
278
+ askForSecret,
279
+ },
280
+ cbk) =>
172
281
  {
173
- try {
174
- const {trade} = encodeTrade({
175
- auth: encryptPayload.trade_auth_tag,
176
- payload: encryptPayload.trade_cipher,
177
- request: createPurchaseInvoice.request,
178
- });
179
-
180
- return cbk(null, {trade});
181
- } catch (err) {
182
- return cbk([500, err.message]);
282
+ // Exit early when there is no node to finalize the trade to
283
+ if (!askForNodeId.id) {
284
+ return cbk();
183
285
  }
286
+
287
+ return finalizeTradeSecret({
288
+ lnd,
289
+ description: askForDescription.description,
290
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
291
+ secret: askForSecret.secret,
292
+ to: askForNodeId.id,
293
+ tokens: asNumber(askForPrice.tokens),
294
+ },
295
+ cbk);
184
296
  }],
185
297
  },
186
- returnResult({reject, resolve, of: 'encodeTradeToWireFormat'}, cbk));
298
+ returnResult({reject, resolve, of: 'finalize'}, cbk));
187
299
  });
188
300
  };
@@ -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,45 @@
1
+ const findDescriptionRecord = records => records.find(n => n.type === '2');
2
+ const findIdRecord = records => records.find(n => n.type === '1');
3
+ const hexAsUtf8 = hex => Buffer.from(hex, 'hex').toString('utf8');
4
+ const {isArray} = Array;
5
+
6
+ /** Decode a basic trade
7
+
8
+ 1: <Trade Id>
9
+ 2: <Trade Description>
10
+
11
+ {
12
+ records: [{
13
+ type: <Basic Trade Record Type Number String>
14
+ value: <Value Hex Encoded String>
15
+ }]
16
+ }
17
+
18
+ @returns
19
+ {
20
+ description: <Trade Description String>
21
+ id: <Trade Id Hex String>
22
+ }
23
+ */
24
+ module.exports = ({records}) => {
25
+ if (!isArray(records)) {
26
+ throw new Error('ExpectedArrayOfRecordsToDecodeBasicTrade');
27
+ }
28
+
29
+ const descriptionRecord = findDescriptionRecord(records);
30
+
31
+ if (!descriptionRecord) {
32
+ throw new Error('ExpectedDescriptionRecordToDecodeBasicTrade');
33
+ }
34
+
35
+ const idRecord = findIdRecord(records);
36
+
37
+ if (!idRecord) {
38
+ throw new Error('ExpectedIdRecordToDecodeBasicTradeDetails');
39
+ }
40
+
41
+ return {
42
+ description: hexAsUtf8(descriptionRecord.value),
43
+ id: idRecord.value,
44
+ };
45
+ };