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.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/index.js +6 -0
- package/p2p/encode_peer_request.js +59 -0
- package/p2p/encode_peer_response.js +45 -0
- package/p2p/encode_response_code.js +49 -0
- package/p2p/index.js +4 -0
- package/p2p/make_peer_request.js +151 -0
- package/p2p/parse_request_message.js +103 -0
- package/p2p/parse_response_code.js +60 -0
- package/p2p/return_failure_response.js +43 -0
- package/p2p/return_success_response.js +47 -0
- package/p2p/service_peer_requests.js +100 -0
- package/package.json +8 -7
- package/test/integration/test_service_peer_requests.js +59 -0
- package/trades/buy_preimage.js +145 -0
- package/trades/create_trade.js +188 -0
- package/trades/decode_trade.js +108 -0
- package/trades/decrypt_trade_secret.js +100 -0
- package/trades/encode_trade.js +50 -0
- package/trades/encrypt_trade_secret.js +98 -0
- package/trades/index.js +3 -0
- package/trades/manage_trade.js +358 -0
- package/trades/manage_trades.js +88 -0
- package/trades/network_from_network_record.js +26 -0
- package/trades/network_record_from_request.js +30 -0
|
@@ -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
|
+
};
|
package/trades/index.js
ADDED
|
@@ -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
|
+
};
|