paid-services 3.3.0 → 3.4.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 (35) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/capacity/accept_capacity_change.js +3 -3
  3. package/capacity/get_capacity_change_requests.js +1 -1
  4. package/capacity/get_capacity_replacement.js +1 -1
  5. package/capacity/initiate_capacity_change.js +5 -4
  6. package/capacity/propose_capacity_change.js +3 -1
  7. package/p2p/service_peer_requests.js +10 -2
  8. package/package.json +3 -2
  9. package/service_types.json +16 -0
  10. package/test/integration/test_accept_capacity_change.js +50 -13
  11. package/test/integration/test_change_channel_capacity.js +3 -0
  12. package/test/trades/test_decode_basic_trade.js +40 -0
  13. package/test/trades/test_decode_open_trade.js +50 -0
  14. package/test/trades/test_encode_open_trade.js +35 -0
  15. package/test/trades/test_encode_trade.js +37 -0
  16. package/trades/accept_trade.js +52 -0
  17. package/trades/create_trade.js +124 -44
  18. package/trades/decode_basic_trade.js +45 -0
  19. package/trades/decode_open_trade.js +69 -0
  20. package/trades/decode_trade.js +48 -66
  21. package/trades/decode_trade_secret.js +89 -0
  22. package/trades/encode_open_trade.js +89 -0
  23. package/trades/encode_trade.js +48 -33
  24. package/trades/encode_trade_secret.js +50 -0
  25. package/trades/finalize_trade_secret.js +116 -0
  26. package/trades/find_trade.js +314 -0
  27. package/trades/manage_trade.js +46 -35
  28. package/trades/network_from_network_record.js +3 -3
  29. package/trades/network_record_from_network.js +36 -0
  30. package/trades/network_record_from_request.js +2 -1
  31. package/trades/node_as_record.js +98 -0
  32. package/trades/record_as_node.js +113 -0
  33. package/trades/request_trade_by_id.js +89 -0
  34. package/trades/service_trade_requests.js +184 -0
  35. package/capacity/service_types.json +0 -6
@@ -1,50 +1,65 @@
1
- const {encodeTlvStream} = require('bolt01');
1
+ const encodeOpenTrade = require('./encode_open_trade');
2
+ const encodeTradeSecret = require('./encode_trade_secret');
2
3
 
3
- const networkRecordFromRequest = require('./network_record_from_request');
4
- const {requestAsRequestRecords} = require('./../records');
4
+ /** Encode v1 trade records
5
5
 
6
- const asTrade = hex => `626f73ff${hex}`;
6
+ A v1 trade is either a qualified trade with a request, or an open ended trade
7
+ with connection details.
7
8
 
8
- /** Encode a trade
9
+ 0: version (v1)
10
+ [1]: network
11
+ [2]: payment request
12
+ [3]: trade details
13
+ [4]: nodes records
14
+ [5]: trade identifier
9
15
 
10
16
  {
11
- auth: <Encrypted Payload Auth Hex String>
12
- payload: <Preimage Encrypted Payload Hex String>
13
- request: <BOLT 11 Payment Request String>
17
+ [connect]: {
18
+ [id]: <Reference Trade Id Hex String>
19
+ network: <Network Name String>
20
+ nodes: [{
21
+ channels: [<Standard Format Channel Id String>]
22
+ id: <Node Public Key Id Hex String>
23
+ [sockets]: [<Peer Socket String>]
24
+ }]
25
+ }
26
+ [secret]: {
27
+ auth: <Encrypted Payload Auth Hex String>
28
+ payload: <Preimage Encrypted Payload Hex String>
29
+ request: <BOLT 11 Payment Request String>
30
+ }
14
31
  }
15
32
 
33
+ @throws
34
+ <Error>
35
+
16
36
  @returns
17
37
  {
18
38
  trade: <Hex Encoded Trade String>
19
39
  }
20
40
  */
21
- module.exports = ({auth, payload, request}, cbk) => {
22
- // Encode the trade record
23
- const tradeRecords = [{
24
- type: '2',
25
- value: requestAsRequestRecords({request}).encoded,
26
- }];
27
-
28
- // Add a network name record if required
29
- if (!!networkRecordFromRequest({request}).value) {
30
- tradeRecords.push({
31
- type: '1',
32
- value: networkRecordFromRequest({request}).value,
33
- });
41
+ module.exports = ({connect, secret}) => {
42
+ if (!connect && !secret) {
43
+ throw new Error('ExpectedEitherConnectDetailsOrTradeSecret');
34
44
  }
35
45
 
36
- // Encode the encrypted data
37
- const encryptionRecords = encodeTlvStream({
38
- records: [{type: '0', value: payload}, {type: '1', value: auth}],
39
- });
40
-
41
- // Encode the details of the trade
42
- const detailsRecords = encodeTlvStream({
43
- records: [{type: '0', value: encryptionRecords.encoded}],
44
- });
46
+ if (!!connect && !!secret) {
47
+ throw new Error('ExpectedEitherConnectDetailsOrTradeSecretNotBoth');
48
+ }
45
49
 
46
- // Add the trade details to the trade
47
- tradeRecords.push({type: '3', value: detailsRecords.encoded});
50
+ // Exit early when this is an open ended trade
51
+ if (!!connect) {
52
+ return encodeOpenTrade({
53
+ id: connect.id,
54
+ network: connect.network,
55
+ nodes: connect.nodes,
56
+ });
57
+ }
48
58
 
49
- return {trade: asTrade(encodeTlvStream({records: tradeRecords}).encoded)};
59
+ // Encode the trade secret
60
+ return encodeTradeSecret({
61
+ auth: secret.auth,
62
+ payload: secret.payload,
63
+ request: secret.request,
64
+ });
50
65
  };
@@ -0,0 +1,50 @@
1
+ const {encodeTlvStream} = require('bolt01');
2
+
3
+ const networkRecordFromRequest = require('./network_record_from_request');
4
+ const {requestAsRequestRecords} = require('./../records');
5
+
6
+ const asTrade = hex => `626f73ff${hex}`;
7
+
8
+ /** Encode a trade
9
+
10
+ {
11
+ auth: <Encrypted Payload Auth Hex String>
12
+ payload: <Preimage Encrypted Payload Hex String>
13
+ request: <BOLT 11 Payment Request String>
14
+ }
15
+
16
+ @returns
17
+ {
18
+ trade: <Hex Encoded Trade String>
19
+ }
20
+ */
21
+ module.exports = ({auth, payload, request}, cbk) => {
22
+ // Encode the trade record
23
+ const tradeRecords = [{
24
+ type: '2',
25
+ value: requestAsRequestRecords({request}).encoded,
26
+ }];
27
+
28
+ // Add a network name record if required
29
+ if (!!networkRecordFromRequest({request}).value) {
30
+ tradeRecords.push({
31
+ type: '1',
32
+ value: networkRecordFromRequest({request}).value,
33
+ });
34
+ }
35
+
36
+ // Encode the encrypted data
37
+ const encryptionRecords = encodeTlvStream({
38
+ records: [{type: '0', value: payload}, {type: '1', value: auth}],
39
+ });
40
+
41
+ // Encode the details of the trade
42
+ const detailsRecords = encodeTlvStream({
43
+ records: [{type: '0', value: encryptionRecords.encoded}],
44
+ });
45
+
46
+ // Add the trade details to the trade
47
+ tradeRecords.push({type: '3', value: detailsRecords.encoded});
48
+
49
+ return {trade: asTrade(encodeTlvStream({records: tradeRecords}).encoded)};
50
+ };
@@ -0,0 +1,116 @@
1
+ const {createHash} = require('crypto');
2
+
3
+ const asyncAuto = require('async/auto');
4
+ const {createHodlInvoice} = require('ln-service');
5
+ const {createInvoice} = require('ln-service');
6
+ const {returnResult} = require('asyncjs-util');
7
+
8
+ const encodeTradeSecret = require('./encode_trade_secret');
9
+ const encryptTradeSecret = require('./encrypt_trade_secret');
10
+
11
+ const bufferAsHex = buffer => buffer.toString('hex');
12
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
13
+ const sha256 = preimage => createHash('sha256').update(preimage).digest();
14
+ const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
15
+
16
+ /** Create a trade secret for a node
17
+
18
+ {
19
+ description: <Trade Description String>
20
+ is_hold: <Create Hold Invoice Bool>
21
+ lnd: <Authenticated LND API Object>
22
+ secret: <Clear Secret String>
23
+ to: <To Public Key Hex Encoded String>
24
+ tokens: <Price Tokens Number>
25
+ }
26
+
27
+ @returns via cbk or Promise
28
+ {
29
+ id: <Payment Hash Hex String>
30
+ secret: <Hex Encoded Payment Secret String>
31
+ trade: <Hex Encoded Trade String>
32
+ }
33
+ */
34
+ module.exports = (args, cbk) => {
35
+ return new Promise((resolve, reject) => {
36
+ return asyncAuto({
37
+ // Check arguments
38
+ validate: cbk => {
39
+ if (!args.description) {
40
+ return cbk([400, 'ExpectedDescriptionToFinalizeTradeSecret']);
41
+ }
42
+
43
+ if (!args.lnd) {
44
+ return cbk([400, 'ExpectedAuthenticatedLndToFinalizeTradeSecret']);
45
+ }
46
+
47
+ if (!args.secret) {
48
+ return cbk([400, 'ExpectedSecretValueToFinalizeTradeSecret']);
49
+ }
50
+
51
+ if (!args.to) {
52
+ return cbk([400, 'ExpectedToPublicKeyToFinalizeTradeSecret']);
53
+ }
54
+
55
+ if (!args.tokens) {
56
+ return cbk([400, 'ExpectedTokensPriceToFinalizeTradeSecret']);
57
+ }
58
+
59
+ return cbk();
60
+ },
61
+
62
+ // Encrypt the secret data
63
+ encrypt: ['validate', ({}, cbk) => {
64
+ return encryptTradeSecret({
65
+ lnd: args.lnd,
66
+ secret: utf8AsHex(args.secret),
67
+ to: args.to,
68
+ },
69
+ cbk);
70
+ }],
71
+
72
+ // Create the invoice to purchase the unlocking secret
73
+ createInvoice: ['encrypt', ({encrypt}, cbk) => {
74
+ // Exit early when this is a hold invoice
75
+ if (!!args.is_hold) {
76
+ return createHodlInvoice({
77
+ description: args.description,
78
+ id: bufferAsHex(sha256(hexAsBuffer(encrypt.payment_secret))),
79
+ lnd: args.lnd,
80
+ secret: encrypt.payment_secret,
81
+ tokens: args.tokens,
82
+ },
83
+ cbk);
84
+ }
85
+
86
+ return createInvoice({
87
+ description: args.description,
88
+ lnd: args.lnd,
89
+ secret: encrypt.payment_secret,
90
+ tokens: args.tokens,
91
+ },
92
+ cbk);
93
+ }],
94
+
95
+ // Encode all the trade data into wire format
96
+ encode: ['createInvoice', 'encrypt', ({createInvoice, encrypt}, cbk) => {
97
+ try {
98
+ const {trade} = encodeTradeSecret({
99
+ auth: encrypt.trade_auth_tag,
100
+ payload: encrypt.trade_cipher,
101
+ request: createInvoice.request,
102
+ });
103
+
104
+ return cbk(null, {
105
+ trade,
106
+ id: createInvoice.id,
107
+ secret: encrypt.payment_secret,
108
+ });
109
+ } catch (err) {
110
+ return cbk([500, err.message]);
111
+ }
112
+ }],
113
+ },
114
+ returnResult({reject, resolve, of: 'encode'}, cbk));
115
+ });
116
+ };
@@ -0,0 +1,314 @@
1
+ const {randomBytes} = require('crypto');
2
+
3
+ const {addPeer} = require('ln-service');
4
+ const asyncAuto = require('async/auto');
5
+ const asyncDetect = require('async/detect');
6
+ const asyncDetectSeries = require('async/detectSeries');
7
+ const asyncMap = require('async/map');
8
+ const {getChannel} = require('ln-service');
9
+ const {getNode} = require('ln-service');
10
+ const {getPeers} = require('ln-service');
11
+ const {returnResult} = require('asyncjs-util');
12
+
13
+ const decodeBasicTrade = require('./decode_basic_trade');
14
+ const {makePeerRequest} = require('./../p2p');
15
+ const requestTradeById = require('./request_trade_by_id');
16
+ const {servicePeerRequests} = require('./../p2p');
17
+ const {serviceTypeReceiveTrades} = require('./../service_types');
18
+ const {serviceTypeRequestTrades} = require('./../service_types');
19
+
20
+ const findBasicRecord = records => records.find(n => n.type === '1');
21
+ const findIdRecord = records => records.find(n => n.type === '0');
22
+ const {isArray} = Array;
23
+ const makeRequestId = () => randomBytes(32).toString('hex');
24
+ const requestTradeType = '805005';
25
+ const requestTradesTimeoutMs = 1000 * 30;
26
+ const tradesRequestIdType = '1';
27
+ const waitForTradesMs = 1000 * 5;
28
+
29
+ /** Find a trade to purchase
30
+
31
+ {
32
+ ask: <Inquirer Ask Function>
33
+ [id]: <Trade Id Encoded Hex String>
34
+ identity: <Own Node Identity Public Key Hex String>
35
+ lnd: <Authenticated LND API Object>
36
+ logger: <Winston Logger Object>
37
+ nodes: [{
38
+ [high_channel]: <High Key Channel Id String>
39
+ [low_channel]: <Low Key Channel Id String>
40
+ [node]: {
41
+ id: <Node Public Key Id Hex String>
42
+ sockets: [<Peer Socket String>]
43
+ }
44
+ }]
45
+ }
46
+
47
+ @returns via cbk or Promise
48
+ {
49
+ auth: <Encrypted Payload Auth Hex String>
50
+ payload: <Preimage Encrypted Payload Hex String>
51
+ request: <BOLT 11 Payment Request String>
52
+ }
53
+ */
54
+ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
55
+ return new Promise((resolve, reject) => {
56
+ return asyncAuto({
57
+ // Check arguments
58
+ validate: cbk => {
59
+ if (!ask) {
60
+ return cbk([400, 'ExpectedAskMethodToFindTrade']);
61
+ }
62
+
63
+ if (!identity) {
64
+ return cbk([400, 'ExpectedIdentityToFindTrade']);
65
+ }
66
+
67
+ if (!lnd) {
68
+ return cbk([400, 'ExpectedLndApiToFindTrade']);
69
+ }
70
+
71
+ if (!logger) {
72
+ return cbk([400, 'ExpectedLoggerObjectToFindTrade']);
73
+ }
74
+
75
+ if (!isArray(nodes)) {
76
+ return cbk([400, 'ExpectedNodesArrayToFindTrade']);
77
+ }
78
+
79
+ return cbk();
80
+ },
81
+
82
+ // Find node connect info to connect to
83
+ getNodes: ['validate', ({}, cbk) => {
84
+ return asyncMap(nodes, (connect, cbk) => {
85
+ // Exit early when the node is self
86
+ if (!!connect.node && connect.node.id === identity) {
87
+ return cbk();
88
+ }
89
+
90
+ // Exit early when the node id and sockets is specified directly
91
+ if (!!connect.node) {
92
+ return cbk(null, connect.node);
93
+ }
94
+
95
+ // Exit early when there is no landmark channel
96
+ if (!connect.high_channel && !connect.low_channel) {
97
+ return cbk();
98
+ }
99
+
100
+ const channelId = connect.high_channel || connect.low_channel;
101
+
102
+ // Get the public keys of the landmark channel
103
+ return getChannel({lnd, id: channelId}, (err, res) => {
104
+ // Exit early when the channel cannot be fetched
105
+ if (!!err) {
106
+ return cbk();
107
+ }
108
+
109
+ const [low, high] = res.policies.map(n => n.public_key);
110
+
111
+ // The node key is either the lower or the higher public key
112
+ const nodeId = !!connect.low_channel ? low : high;
113
+
114
+ // Exit early when the node is self
115
+ if (nodeId === identity) {
116
+ return cbk();
117
+ }
118
+
119
+ // Find the sockets for the node
120
+ return getNode({
121
+ lnd,
122
+ is_omitting_channels: true,
123
+ public_key: nodeId,
124
+ },
125
+ (err, res) => {
126
+ if (!!err) {
127
+ return cbk();
128
+ }
129
+
130
+ return cbk(null, {
131
+ id: nodeId,
132
+ sockets: res.sockets.map(n => n.socket),
133
+ });
134
+ });
135
+ });
136
+ },
137
+ cbk);
138
+ }],
139
+
140
+ // Get the list of connected peers
141
+ getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
142
+
143
+ // Try and connect to a node in order to do p2p messaging
144
+ connect: ['getNodes', 'getPeers', ({getNodes, getPeers}, cbk) => {
145
+ const connected = getPeers.peers.map(n => n.public_key);
146
+
147
+ // Look for a node that is already a connected peer
148
+ const [alreadyConnected] = getNodes.filter(node => {
149
+ return !!node && connected.includes(node.id);
150
+ });
151
+
152
+ // Exit early when already connected to a node
153
+ if (!!alreadyConnected) {
154
+ return cbk(null, alreadyConnected);
155
+ }
156
+
157
+ // Try and connect to a referenced node
158
+ return asyncDetect(getNodes.filter(n => !!n), (node, cbk) => {
159
+ // Attempt referenced sockets to establish p2p connection
160
+ return asyncDetectSeries(node.sockets, (socket, cbk) => {
161
+ return addPeer({lnd, socket, public_key: node.id}, err => {
162
+ // Stop trying sockets when there is no error
163
+ return cbk(null, !err);
164
+ });
165
+ },
166
+ (err, socket) => {
167
+ if (!!err) {
168
+ return cbk(err);
169
+ }
170
+
171
+ // The node is connected if one of the sockets worked
172
+ return cbk(null, !!socket);
173
+ });
174
+ },
175
+ cbk);
176
+ }],
177
+
178
+ // Node to request trades from
179
+ to: ['connect', ({connect}, cbk) => {
180
+ if (!connect) {
181
+ return cbk([503, 'FailedToConnectToNode']);
182
+ }
183
+
184
+ return cbk(null, connect.id);
185
+ }],
186
+
187
+ // Request a specific trade
188
+ requestTrade: ['to', ({to}, cbk) => {
189
+ // Exit early when this is an open ended trade request with no id
190
+ if (!id) {
191
+ return cbk();
192
+ }
193
+
194
+ return requestTradeById({id, lnd, to}, cbk);
195
+ }],
196
+
197
+ // Request an inventory of trades
198
+ requestTrades: ['to', ({to}, cbk) => {
199
+ // Exit early when this is a trade request for a specific id
200
+ if (!!id) {
201
+ return cbk();
202
+ }
203
+
204
+ const requestId = makeRequestId();
205
+ const service = servicePeerRequests({lnd});
206
+ const trades = [];
207
+
208
+ // When the requesting period is over, return the received trades
209
+ const finished = err => {
210
+ service.stop({});
211
+
212
+ return !!err ? cbk(err) : cbk(null, {trades});
213
+ };
214
+
215
+ // Listen for trades
216
+ service.request({type: serviceTypeReceiveTrades}, (req, res) => {
217
+ const requestIdRecord = findIdRecord(req.records);
218
+
219
+ // Exit early when this is a request for something else
220
+ if (!requestIdRecord || requestIdRecord.value !== requestId) {
221
+ return;
222
+ }
223
+
224
+ // Make sure there is a basic trade record in the records
225
+ if (!findBasicRecord(req.records)) {
226
+ return res.failure([400, 'ExpectedBasicTradeRecord']);
227
+ }
228
+
229
+ // Make sure the basic trade record is valid
230
+ try {
231
+ decodeBasicTrade({records: req.records});
232
+ } catch (err) {
233
+ return res.failure([400, 'ExpectedValidBasicTradeRecord']);
234
+ }
235
+
236
+ trades.push(decodeBasicTrade({records: req.records}));
237
+
238
+ return res.success({});
239
+ });
240
+
241
+ // Once connected, ask for the trade details
242
+ return makePeerRequest({
243
+ lnd,
244
+ to,
245
+ records: [{type: tradesRequestIdType, value: requestId}],
246
+ timeout: requestTradesTimeoutMs,
247
+ type: serviceTypeRequestTrades,
248
+ },
249
+ (err, res) => {
250
+ if (!!err) {
251
+ return finished(err);
252
+ }
253
+
254
+ return setTimeout(finished, waitForTradesMs);
255
+ });
256
+ }],
257
+
258
+ // Select the desired basic trade
259
+ selectTrade: [
260
+ 'requestTrade',
261
+ 'requestTrades',
262
+ ({requestTrade, requestTrades}, cbk) =>
263
+ {
264
+ // Exit early when the trade was already selected
265
+ if (!!requestTrade) {
266
+ return cbk();
267
+ }
268
+
269
+ if (!requestTrades.trades.length) {
270
+ return cbk([404, 'NoTradesFound']);
271
+ }
272
+
273
+ const [trade, other] = requestTrades.trades;
274
+
275
+ // Exit early when there is only a single trade
276
+ if (!other) {
277
+ return cbk(null, trade);
278
+ }
279
+
280
+ // Present the possible choices
281
+ return ask({
282
+ choices: requestTrades.trades.map(trade => ({
283
+ name: trade.description,
284
+ value: trade.id,
285
+ })),
286
+ message: 'What would you like to buy?',
287
+ name: 'id',
288
+ type: 'list',
289
+ },
290
+ cbk);
291
+ }],
292
+
293
+ // Fetch the full trade-secret that was selected
294
+ fetchSelectedTrade: ['selectTrade', 'to', ({selectTrade, to}, cbk) => {
295
+ // Exit early when trade is already known
296
+ if (!selectTrade) {
297
+ return cbk();
298
+ }
299
+
300
+ return requestTradeById({lnd, to, id: selectTrade.id}, cbk);
301
+ }],
302
+
303
+ // Final trade details
304
+ trade: [
305
+ 'fetchSelectedTrade',
306
+ 'requestTrade',
307
+ ({fetchSelectedTrade, requestTrade}, cbk) =>
308
+ {
309
+ return cbk(null, fetchSelectedTrade || requestTrade);
310
+ }],
311
+ },
312
+ returnResult({reject, resolve, of: 'trade'}, cbk));
313
+ });
314
+ };