paid-services 3.0.0 → 3.2.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.
@@ -0,0 +1,100 @@
1
+ const {createDecipheriv} = require('crypto');
2
+
3
+ const asyncAuto = require('async/auto');
4
+ const {diffieHellmanComputeSecret} = require('ln-service');
5
+ const {getIdentity} = require('ln-service');
6
+ const {returnResult} = require('asyncjs-util');
7
+
8
+ const algorithm = 'aes-256-gcm';
9
+ const bufferAsHex = buffer => buffer.toString('hex');
10
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
11
+ const iv = Buffer.alloc(16, 0);
12
+
13
+ /** Decrypt a trade with a secret preimage
14
+
15
+ {
16
+ auth: <Auth Tag Hex String>
17
+ from: <Trading With Node Id Public Key Hex String>
18
+ lnd: <Authenticated LND API Object>
19
+ payload: <Encrypted Data Hex String>
20
+ secret: <Preimage Hex String>
21
+ }
22
+
23
+ @returns via cbk or Promise
24
+ {
25
+ plain: <Plain Decrypted Secret Hex String>
26
+ }
27
+ */
28
+ module.exports = ({auth, from, lnd, payload, secret}, cbk) => {
29
+ return new Promise((resolve, reject) => {
30
+ return asyncAuto({
31
+ // Check arguments
32
+ validate: cbk => {
33
+ if (!auth) {
34
+ return cbk([400, 'ExpectedAuthToDecryptTradeSecret']);
35
+ }
36
+
37
+ if (!from) {
38
+ return cbk([400, 'ExpectedFromTradingNodeIdToDecryptTradeSecret']);
39
+ }
40
+
41
+ if (!lnd) {
42
+ return cbk([400, 'ExpectedAuthenticatedLndToDecryptTradeSecret']);
43
+ }
44
+
45
+ if (!payload) {
46
+ return cbk([400, 'ExpectedTradePayloadToDecryptTradeSecret']);
47
+ }
48
+
49
+ if (!secret) {
50
+ return cbk([400, 'ExpectedTradeUnlockKeyToDecryptTradeSecret']);
51
+ }
52
+
53
+ return cbk();
54
+ },
55
+
56
+ // Get identity public key
57
+ getId: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
58
+
59
+ // Compute a shared secret with the trading partner
60
+ computeSharedSecret: ['validate', ({}, cbk) => {
61
+ return diffieHellmanComputeSecret({
62
+ lnd,
63
+ partner_public_key: from,
64
+ },
65
+ cbk);
66
+ }],
67
+
68
+ // Combine the shared secret with a payment preimage for the final secret
69
+ key: ['computeSharedSecret', ({computeSharedSecret}, cbk) => {
70
+ const paidSecret = hexAsBuffer(secret);
71
+ const key = hexAsBuffer(computeSharedSecret.secret);
72
+
73
+ // Combine the shared secret with the payment preimage
74
+ for (let i = 0; i < key.length; ++i) {
75
+ key[i] = key[i] ^ paidSecret[i];
76
+ }
77
+
78
+ return cbk(null, key);
79
+ }],
80
+
81
+ // Decrypt the payload
82
+ decryptPayload: ['key', ({key}, cbk) => {
83
+ const decipher = createDecipheriv(algorithm, key, iv);
84
+ const encrypted = hexAsBuffer(payload);
85
+
86
+ decipher.setAuthTag(hexAsBuffer(auth));
87
+
88
+ // Try decrypting using the unlock key
89
+ try {
90
+ const elements = [decipher.update(encrypted), decipher.final()];
91
+
92
+ return cbk(null, {plain: bufferAsHex(Buffer.concat(elements))});
93
+ } catch (err) {
94
+ return cbk([503, 'FailedToDecryptEncryptedTradePayload', {err}]);
95
+ }
96
+ }],
97
+ },
98
+ returnResult({reject, resolve, of: 'decryptPayload'}, cbk));
99
+ });
100
+ };
@@ -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,98 @@
1
+ const {createCipheriv} = require('crypto');
2
+ const {createHash} = require('crypto');
3
+ const {randomBytes} = require('crypto');
4
+
5
+ const asyncAuto = require('async/auto');
6
+ const {diffieHellmanComputeSecret} = require('ln-service');
7
+ const {returnResult} = require('asyncjs-util');
8
+
9
+ const algorithm = 'aes-256-gcm';
10
+ const bufferAsHex = buffer => buffer.toString('hex');
11
+ const digest = 'sha512';
12
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
13
+ const iv = Buffer.alloc(16, 0);
14
+ const makePreimage = () => randomBytes(32).toString('hex');
15
+ const sha256 = n => createHash('sha256').update(n).digest();
16
+
17
+ /** Encode a secret for a trade
18
+
19
+ {
20
+ lnd: <Authenticated LND API Object>
21
+ secret: <Secret Hex String>
22
+ to: <Trading With Node Id Public Key Hex String>
23
+ }
24
+
25
+ @returns via cbk or Promise
26
+ {
27
+ payment_secret: <Payment Hash Preimage Hex String>
28
+ trade_auth_tag: <Trade Data Data Auth Tag Hex String>
29
+ trade_cipher: <Trade Data Encrypted Hex String>
30
+ }
31
+ */
32
+ module.exports = ({lnd, secret, to}, cbk) => {
33
+ return new Promise((resolve, reject) => {
34
+ return asyncAuto({
35
+ // Check arguments
36
+ validate: cbk => {
37
+ if (!lnd) {
38
+ return cbk([400, 'ExpectedAuthenticatedLndToEncryptTradeSecret']);
39
+ }
40
+
41
+ if (!secret) {
42
+ return cbk([400, 'ExpectedSecretToTradeToEncryptTradeSecret']);
43
+ }
44
+
45
+ if (!to) {
46
+ return cbk([400, 'ExpectedToPublicKeyToEncryptTradeSecret']);
47
+ }
48
+
49
+ return cbk();
50
+ },
51
+
52
+ // Generate a secret to use for the payment preimage
53
+ paymentSecret: ['validate', ({}, cbk) => cbk(null, makePreimage())],
54
+
55
+ // Compute a shared secret with the destination
56
+ computeSharedSecret: ['validate', ({}, cbk) => {
57
+ return diffieHellmanComputeSecret({lnd, partner_public_key: to}, cbk);
58
+ }],
59
+
60
+ // Combine the shared secret with a payment preimage for the final secret
61
+ key: [
62
+ 'computeSharedSecret',
63
+ 'paymentSecret',
64
+ ({computeSharedSecret, paymentSecret}, cbk) =>
65
+ {
66
+ const paidSecret = hexAsBuffer(paymentSecret);
67
+ const key = hexAsBuffer(computeSharedSecret.secret);
68
+
69
+ // Combine the shared secret with the payment preimage
70
+ for (let i = 0; i < paidSecret.length; ++i) {
71
+ key[i] = key[i] ^ paidSecret[i];
72
+ }
73
+
74
+ return cbk(null, key);
75
+ }],
76
+
77
+ // Encrypt the trade details with the secret
78
+ encrypt: ['key', 'paymentSecret', ({key, paymentSecret}, cbk) => {
79
+ try {
80
+ createCipheriv(algorithm, key, iv);
81
+ } catch (err) {
82
+ return cbk([500, 'FailedToCreateCipherEncryptingSecret', {err}]);
83
+ }
84
+
85
+ const cipher = createCipheriv(algorithm, key, iv);
86
+
87
+ const updated = [cipher.update(hexAsBuffer(secret)), cipher.final()];
88
+
89
+ return cbk(null, {
90
+ payment_secret: paymentSecret,
91
+ trade_auth_tag: bufferAsHex(cipher.getAuthTag()),
92
+ trade_cipher: bufferAsHex(Buffer.concat(updated)),
93
+ });
94
+ }],
95
+ },
96
+ returnResult({reject, resolve, of: 'encrypt'}, cbk));
97
+ });
98
+ };
@@ -0,0 +1,3 @@
1
+ const manageTrades = require('./manage_trades');
2
+
3
+ module.exports = {manageTrades};
@@ -0,0 +1,358 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncReflect = require('async/reflect');
3
+ const asyncRetry = require('async/retry');
4
+ const {cancelHodlInvoice} = require('ln-service');
5
+ const {getIdentity} = require('ln-service');
6
+ const {getInvoice} = require('ln-service');
7
+ const {getPayment} = require('ln-service');
8
+ const {parsePaymentRequest} = require('ln-service');
9
+ const {returnResult} = require('asyncjs-util');
10
+
11
+ const buyPreimage = require('./buy_preimage');
12
+ const decodeTrade = require('./decode_trade');
13
+ const decryptTradeSecret = require('./decrypt_trade_secret');
14
+
15
+ const hexAsUtf8 = hex => Buffer.from(hex, 'hex').toString();
16
+ const {isArray} = Array;
17
+ const isHexPreimage = n => !!n && /^[0-9A-F]{64}$/i.test(n);
18
+
19
+ /** Manage an individual trade
20
+
21
+ {
22
+ ask: <Ask Function>
23
+ lnd: <Authenticated LND API Object>
24
+ logger: <Winston Logger Object>
25
+ }
26
+
27
+ @returns via cbk or Promise
28
+ */
29
+ module.exports = ({ask, lnd, logger}, cbk) => {
30
+ return new Promise((resolve, reject) => {
31
+ return asyncAuto({
32
+ // Check arguments
33
+ validate: cbk => {
34
+ if (!ask) {
35
+ return cbk([400, 'ExpectedAskFunctionToManageTrade']);
36
+ }
37
+
38
+ if (!lnd) {
39
+ return cbk([400, 'ExpectedAuthenticatedLndToManageTrade']);
40
+ }
41
+
42
+ if (!logger) {
43
+ return cbk([400, 'ExpectedWinstonLoggerToManageTrade']);
44
+ }
45
+
46
+ return cbk();
47
+ },
48
+
49
+ // Get the trade data to decode it
50
+ askForTrade: ['validate', ({}, cbk) => {
51
+ return ask({
52
+ name: 'trade',
53
+ message: 'Enter encoded trade',
54
+ type: 'input',
55
+ validate: input => {
56
+ if (!input) {
57
+ return false;
58
+ }
59
+
60
+ return true;
61
+ },
62
+ },
63
+ cbk);
64
+ }],
65
+
66
+ // Derive the self public key
67
+ getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
68
+
69
+ // Decode the trade data
70
+ decodeDetails: ['askForTrade', ({askForTrade}, cbk) => {
71
+ try {
72
+ const details = decodeTrade({trade: askForTrade.trade});
73
+
74
+ return cbk(null, {
75
+ auth: details.auth,
76
+ payload: details.payload,
77
+ request: details.request,
78
+ });
79
+ } catch (err) {
80
+ return cbk([400, err.message]);
81
+ }
82
+ }],
83
+
84
+ // Parse the trade payment request
85
+ requestDetails: ['decodeDetails', ({decodeDetails}, cbk) => {
86
+ const {request} = decodeDetails;
87
+
88
+ try {
89
+ return cbk(null, parsePaymentRequest({request}));
90
+ } catch (err) {
91
+ return cbk([400, err.message]);
92
+ }
93
+ }],
94
+
95
+ // Get the invoice for the trade in case its our own trade
96
+ getRequest: ['requestDetails', asyncReflect(({requestDetails}, cbk) => {
97
+ return getInvoice({lnd, id: requestDetails.id}, cbk);
98
+ })],
99
+
100
+ // Get payment paid for the trade
101
+ getPurchase: ['requestDetails', asyncReflect(({requestDetails}, cbk) => {
102
+ return getPayment({lnd, id: requestDetails.id}, cbk);
103
+ })],
104
+
105
+ // Show resolved offer details
106
+ resolvedOffer: [
107
+ 'getIdentity',
108
+ 'getRequest',
109
+ 'requestDetails',
110
+ ({getIdentity, getRequest, requestDetails}, cbk) =>
111
+ {
112
+ // Exit early when this is not an own offer
113
+ if (requestDetails.destination !== getIdentity.public_key) {
114
+ return cbk();
115
+ }
116
+
117
+ // A missing invoice may signal that it is removed
118
+ const invoice = getRequest.value || {is_canceled: true};
119
+
120
+ // Exit early when request is still live
121
+ if (!invoice.is_canceled && !invoice.is_confirmed) {
122
+ return cbk();
123
+ }
124
+
125
+ logger.info({
126
+ id: requestDetails.id,
127
+ offering: requestDetails.description,
128
+ price: requestDetails.tokens,
129
+ is_canceled: invoice.is_canceled || undefined,
130
+ is_purchased: invoice.is_confirmed || undefined,
131
+ });
132
+
133
+ return cbk();
134
+ }],
135
+
136
+ // Select an offer action for the trade
137
+ selectOfferAction: [
138
+ 'getRequest',
139
+ 'getPurchase',
140
+ 'requestDetails',
141
+ ({getRequest, requestDetails}, cbk) =>
142
+ {
143
+ // Exit early when this is not our own offer
144
+ if (!getRequest.value) {
145
+ return cbk();
146
+ }
147
+
148
+ // Exit early when the payment was made
149
+ if (getRequest.value.is_confirmed) {
150
+ return cbk();
151
+ }
152
+
153
+ logger.info({
154
+ id: requestDetails.id,
155
+ offering: requestDetails.description,
156
+ price: requestDetails.tokens,
157
+ });
158
+
159
+ return ask({
160
+ choices: [
161
+ {name: 'Exit', value: 'exit'},
162
+ {name: 'Cancel offer', value: 'cancel'},
163
+ ],
164
+ message: ' ',
165
+ name: 'action',
166
+ type: 'list',
167
+ },
168
+ cbk);
169
+ }],
170
+
171
+ // Cancel past offer
172
+ cancelOffer: [
173
+ 'requestDetails',
174
+ 'selectOfferAction',
175
+ ({requestDetails, selectOfferAction}, cbk) =>
176
+ {
177
+ // Exit early when there is no action to take
178
+ if (!selectOfferAction || selectOfferAction.action !== 'cancel') {
179
+ return cbk();
180
+ }
181
+
182
+ return cancelHodlInvoice({lnd, id: requestDetails.id}, cbk);
183
+ }],
184
+
185
+ // Select a purchase option for the trade
186
+ selectPurchaseAction: [
187
+ 'decodeDetails',
188
+ 'getPurchase',
189
+ 'getRequest',
190
+ 'requestDetails',
191
+ ({decodeDetails, getPurchase, getRequest, requestDetails}, cbk) =>
192
+ {
193
+ // Exit early when the trade was already purchased
194
+ if (!!getPurchase.value && !!getPurchase.value.is_confirmed) {
195
+ return cbk();
196
+ }
197
+
198
+ // Exit early when this is a known outgoing trade
199
+ if (!!getRequest.value) {
200
+ return cbk();
201
+ }
202
+
203
+ logger.info({
204
+ request: decodeDetails.request,
205
+ purchase: requestDetails.description,
206
+ price: requestDetails.tokens,
207
+ to: requestDetails.destination,
208
+ });
209
+
210
+ // Select an action to take on the decoded trade
211
+ return ask({
212
+ choices: [
213
+ {name: 'Purchase', value: 'buy'},
214
+ {name: 'Already purchased?', value: 'reveal'},
215
+ ],
216
+ message: ' ',
217
+ name: 'action',
218
+ type: 'list',
219
+ },
220
+ cbk);
221
+ }],
222
+
223
+ // Ask for preimage in case it is separately known
224
+ askForPreimage: [
225
+ 'requestDetails',
226
+ 'selectPurchaseAction',
227
+ ({requestDetails, selectPurchaseAction}, cbk) =>
228
+ {
229
+ // Exit early when this is not a purchase
230
+ if (!selectPurchaseAction) {
231
+ return cbk();
232
+ }
233
+
234
+ // Exit early when preimage entry is not selected
235
+ if (selectPurchaseAction.action !== 'reveal') {
236
+ return cbk();
237
+ }
238
+
239
+ return ask({
240
+ message: `Enter the preimage for payment ${requestDetails.id}`,
241
+ name: 'secret',
242
+ type: 'input',
243
+ validate: input => {
244
+ if (!input) {
245
+ return false;
246
+ }
247
+
248
+ if (!isHexPreimage(input)) {
249
+ return 'Enter the secret preimage in hex encoded format';
250
+ }
251
+
252
+ return true;
253
+ },
254
+ },
255
+ cbk);
256
+ }],
257
+
258
+ // Pay and get the preimage through paying for it
259
+ payForPreimage: [
260
+ 'decodeDetails',
261
+ 'selectPurchaseAction',
262
+ ({decodeDetails, selectPurchaseAction}, cbk) =>
263
+ {
264
+ // Exit early when this is an offer
265
+ if (!selectPurchaseAction) {
266
+ return cbk();
267
+ }
268
+
269
+ // Exit early when buy is not selected
270
+ if (selectPurchaseAction.action !== 'buy') {
271
+ return cbk();
272
+ }
273
+
274
+ return asyncRetry({
275
+ errorFilter: err => !!isArray(err) && err.slice().shift() >= 500,
276
+ },
277
+ cbk => {
278
+ return buyPreimage({
279
+ ask,
280
+ lnd,
281
+ logger,
282
+ request: decodeDetails.request,
283
+ },
284
+ cbk);
285
+ },
286
+ cbk);
287
+ }],
288
+
289
+ // Use the preimage to decrypt the trade secret
290
+ decryptSecret: [
291
+ 'askForPreimage',
292
+ 'decodeDetails',
293
+ 'getPurchase',
294
+ 'payForPreimage',
295
+ 'requestDetails',
296
+ 'selectPurchaseAction',
297
+ ({
298
+ askForPreimage,
299
+ decodeDetails,
300
+ getPurchase,
301
+ payForPreimage,
302
+ requestDetails,
303
+ selectPurchaseAction,
304
+ },
305
+ cbk) =>
306
+ {
307
+ // Exit early when this is not a purchase
308
+ if (!payForPreimage && !askForPreimage && !getPurchase.value) {
309
+ return cbk();
310
+ }
311
+
312
+ // Exit early when there is no successful purchase
313
+ if (!!getPurchase.value && !getPurchase.value.payment) {
314
+ return cbk();
315
+ }
316
+
317
+ // Notify when a preimage was paid for
318
+ if (!!payForPreimage) {
319
+ logger.info({
320
+ paid: payForPreimage.tokens,
321
+ fee: payForPreimage.fee,
322
+ secret: payForPreimage.secret,
323
+ });
324
+ }
325
+
326
+ const {payment} = getPurchase.value || {};
327
+
328
+ const paid = payForPreimage || askForPreimage || payment;
329
+
330
+ return decryptTradeSecret({
331
+ lnd,
332
+ auth: decodeDetails.auth,
333
+ from: requestDetails.destination,
334
+ payload: decodeDetails.payload,
335
+ secret: paid.secret,
336
+ },
337
+ cbk);
338
+ }],
339
+
340
+ // Log the trade details
341
+ trade: [
342
+ 'decryptSecret',
343
+ 'selectPurchaseAction',
344
+ ({decryptSecret, selectPurchaseAction}, cbk) =>
345
+ {
346
+ // Exit early when not purchasing
347
+ if (!decryptSecret) {
348
+ return cbk();
349
+ }
350
+
351
+ logger.info({trade_complete: hexAsUtf8(decryptSecret.plain)});
352
+
353
+ return cbk();
354
+ }],
355
+ },
356
+ returnResult({reject, resolve}, cbk));
357
+ });
358
+ };
@@ -0,0 +1,88 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const createTrade = require('./create_trade');
5
+ const manageTrade = require('./manage_trade');
6
+
7
+ const createAction = 'create';
8
+ const decodeAction = 'decode';
9
+
10
+ /** Create, view, and accept trades
11
+
12
+ {
13
+ ask: <Ask Function>
14
+ lnd: <Authenticated LND API Object>
15
+ logger: <Winston Logger Object>
16
+ }
17
+
18
+ @returns via cbk or Promise
19
+ */
20
+ module.exports = ({ask, lnd, logger}, cbk) => {
21
+ return new Promise((resolve, reject) => {
22
+ return asyncAuto({
23
+ // Check arguments
24
+ validate: cbk => {
25
+ if (!ask) {
26
+ return cbk([400, 'ExpectedAskFunctionToManageTrades']);
27
+ }
28
+
29
+ if (!lnd) {
30
+ return cbk([400, 'ExpectedAuthenticatedLndToManageTrades']);
31
+ }
32
+
33
+ if (!logger) {
34
+ return cbk([400, 'ExpectedLoggerToManageTrades']);
35
+ }
36
+
37
+ return cbk();
38
+ },
39
+
40
+ // Select a trade option
41
+ select: ['validate', ({}, cbk) => {
42
+ return ask({
43
+ choices: [
44
+ {name: 'Create Trade', value: createAction},
45
+ {name: 'Decode Trade', value: decodeAction},
46
+ ],
47
+ message: 'Trade?',
48
+ name: 'action',
49
+ type: 'list',
50
+ },
51
+ cbk);
52
+ }],
53
+
54
+ // Create a new trade
55
+ create: ['select', ({select}, cbk) => {
56
+ // Exit early when not creating a new trade
57
+ if (select.action !== createAction) {
58
+ return cbk();
59
+ }
60
+
61
+ return createTrade({ask, lnd, logger}, cbk);
62
+ }],
63
+
64
+ // Trade was created
65
+ created: ['create', ({create}, cbk) => {
66
+ // Exit early when not creating a new trade
67
+ if (!create) {
68
+ return cbk();
69
+ }
70
+
71
+ logger.info({encoded_trade_created: create.trade});
72
+
73
+ return cbk();
74
+ }],
75
+
76
+ // View an existing trade
77
+ view: ['select', ({select}, cbk) => {
78
+ // Exit early when not decoding a trade
79
+ if (select.action !== decodeAction) {
80
+ return cbk();
81
+ }
82
+
83
+ return manageTrade({ask, lnd, logger}, cbk)
84
+ }],
85
+ },
86
+ returnResult({reject, resolve}, cbk));
87
+ });
88
+ };