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 {sendMessageToPeer} = require('ln-service');
|
|
2
|
+
const {subscribeToPeerMessages} = require('ln-service');
|
|
3
|
+
|
|
4
|
+
const defaultServerError = [500, 'InternalServiceError'];
|
|
5
|
+
const encodePeerResponse = require('./encode_peer_response');
|
|
6
|
+
const parseRequestFailure = (report, n) => { report([503, n]); return {}; };
|
|
7
|
+
const parseRequestMessage = require('./parse_request_message');
|
|
8
|
+
const returnFailureResponse = require('./return_failure_response');
|
|
9
|
+
const returnSuccessResponse = require('./return_success_response');
|
|
10
|
+
|
|
11
|
+
/** Get a p2p message listener
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
lnd: <Authenticated LND API Object>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@returns
|
|
18
|
+
{
|
|
19
|
+
error: <Add an Error Listener Function>
|
|
20
|
+
request: <Add A Request Listener For Type Function>
|
|
21
|
+
stop: <Stop Listening Function> ({}) => {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
request:
|
|
25
|
+
{
|
|
26
|
+
type: <Type Number String>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@returns via cbk
|
|
30
|
+
{
|
|
31
|
+
from: <Request From Node Id Hex Encoded Public Key String>
|
|
32
|
+
[records]: [{
|
|
33
|
+
type: <Type Number String>
|
|
34
|
+
value: <Value Hex String>
|
|
35
|
+
}]
|
|
36
|
+
}
|
|
37
|
+
{
|
|
38
|
+
failure: <Return Failure Function>
|
|
39
|
+
success: <Return Success Function>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
failure:
|
|
43
|
+
[
|
|
44
|
+
<Return Error Code Number>
|
|
45
|
+
<Return Error Message String>
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
success:
|
|
49
|
+
{
|
|
50
|
+
[records]: [{
|
|
51
|
+
type: <Type Number String>
|
|
52
|
+
value: <Value Hex String>
|
|
53
|
+
}]
|
|
54
|
+
}
|
|
55
|
+
*/
|
|
56
|
+
module.exports = ({lnd}) => {
|
|
57
|
+
const listeners = {};
|
|
58
|
+
const service = {error: () => {}};
|
|
59
|
+
const sub = subscribeToPeerMessages({lnd});
|
|
60
|
+
|
|
61
|
+
sub.on('message_received', received => {
|
|
62
|
+
const from = received.public_key;
|
|
63
|
+
const {message} = received;
|
|
64
|
+
const {type} = received;
|
|
65
|
+
|
|
66
|
+
// Parse out the p2p request message details
|
|
67
|
+
const {request} = (() => {
|
|
68
|
+
try {
|
|
69
|
+
return parseRequestMessage({message, type});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return parseRequestFailure(service.error, err.message);
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
|
|
75
|
+
// Exit early when the received message isn't actually a p2p request
|
|
76
|
+
if (!request) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Exit early when nothing is listening for this type
|
|
81
|
+
if (!listeners[request.type]) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const {id} = request;
|
|
86
|
+
const req = {from, records: request.records};
|
|
87
|
+
|
|
88
|
+
const failure = returnFailureResponse({from, id, lnd, service});
|
|
89
|
+
const success = returnSuccessResponse({from, id, lnd, service});
|
|
90
|
+
|
|
91
|
+
// Call the listener to tell it about the request
|
|
92
|
+
return listeners[request.type](req, {failure, success});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
error: cbk => service[error] = cbk,
|
|
97
|
+
request: ({type}, cbk) => listeners[type] = cbk,
|
|
98
|
+
stop: ({}) => sub.removeAllListeners(),
|
|
99
|
+
};
|
|
100
|
+
};
|
package/package.json
CHANGED
|
@@ -7,16 +7,17 @@
|
|
|
7
7
|
"url": "https://github.com/alexbosworth/paid-services/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"async": "3.2.
|
|
11
|
-
"asyncjs-util": "1.2.
|
|
12
|
-
"bolt01": "1.2.
|
|
13
|
-
"invoices": "2.0.
|
|
14
|
-
"ln-service": "
|
|
10
|
+
"async": "3.2.2",
|
|
11
|
+
"asyncjs-util": "1.2.7",
|
|
12
|
+
"bolt01": "1.2.3",
|
|
13
|
+
"invoices": "2.0.2",
|
|
14
|
+
"ln-service": "53.1.2"
|
|
15
15
|
},
|
|
16
16
|
"description": "Lightning Paid Services library",
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@alexbosworth/tap": "15.0.10",
|
|
19
|
-
"
|
|
19
|
+
"ln-docker-daemons": "2.1.0",
|
|
20
|
+
"mock-lnd": "1.4.1",
|
|
20
21
|
"secp256k1": "4.0.2"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
@@ -36,5 +37,5 @@
|
|
|
36
37
|
"scripts": {
|
|
37
38
|
"test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/client/*.js test/config/*.js test/records/*.js test/respond/*.js test/server/*.js test/server/*.js test/services/*.js"
|
|
38
39
|
},
|
|
39
|
-
"version": "3.
|
|
40
|
+
"version": "3.2.0"
|
|
40
41
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const {addPeer} = require('ln-service');
|
|
2
|
+
const {spawnLightningCluster} = require('ln-docker-daemons');
|
|
3
|
+
const {test} = require('@alexbosworth/tap');
|
|
4
|
+
|
|
5
|
+
const {makePeerRequest} = require('./../../');
|
|
6
|
+
const {servicePeerRequests} = require('./../../');
|
|
7
|
+
|
|
8
|
+
const failure = [402, 'PurchaseRequired'];
|
|
9
|
+
const failureType = '1';
|
|
10
|
+
const records = [{type: '1', value: '01'}];
|
|
11
|
+
const size = 2;
|
|
12
|
+
const type = '0';
|
|
13
|
+
|
|
14
|
+
// Adding a listener for peer requests should allow responding to peer requests
|
|
15
|
+
test(`Listen for peer requests`, async ({end, equal, strictSame}) => {
|
|
16
|
+
const {kill, nodes} = await spawnLightningCluster({size});
|
|
17
|
+
|
|
18
|
+
const [{id, lnd}, target] = nodes;
|
|
19
|
+
|
|
20
|
+
await addPeer({lnd, public_key: target.id, socket: target.socket});
|
|
21
|
+
|
|
22
|
+
// Start the server and respond to requests
|
|
23
|
+
const listener = servicePeerRequests({lnd});
|
|
24
|
+
|
|
25
|
+
listener.request({type}, (req, res) => res.success({records}));
|
|
26
|
+
listener.request({type: failureType}, (req, res) => res.failure(failure));
|
|
27
|
+
|
|
28
|
+
// Make a request to the server and get a success response
|
|
29
|
+
try {
|
|
30
|
+
const got = await makePeerRequest({
|
|
31
|
+
type,
|
|
32
|
+
lnd: target.lnd,
|
|
33
|
+
timeout: 1000,
|
|
34
|
+
to: id,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
strictSame(records, got.records, 'Got response records');
|
|
38
|
+
} catch (err) {
|
|
39
|
+
equal(err, null, 'Expected no error making peer request');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Make a request to the server and get a failure response
|
|
43
|
+
try {
|
|
44
|
+
const got = await makePeerRequest({
|
|
45
|
+
type: failureType,
|
|
46
|
+
lnd: target.lnd,
|
|
47
|
+
timeout: 1000,
|
|
48
|
+
to: id,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
strictSame(got, null, 'Expected failure response for failure type');
|
|
52
|
+
} catch (err) {
|
|
53
|
+
strictSame(err, failure, 'Got failure response for failure type');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await kill({});
|
|
57
|
+
|
|
58
|
+
return end();
|
|
59
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {parsePaymentRequest} = require('ln-service');
|
|
3
|
+
const {payViaRoutes} = require('ln-service');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
const {subscribeToProbeForRoute} = require('ln-service');
|
|
6
|
+
|
|
7
|
+
const cltvDeltaBuffer = 5;
|
|
8
|
+
|
|
9
|
+
/** Buy the preimage for a trade
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
ask: <Ask Function>
|
|
13
|
+
lnd: <Authenticated LND API Object>
|
|
14
|
+
logger: <Winston Logger Object>
|
|
15
|
+
request: <BOLT 11 Encoded Payment Request String>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@returns via cbk or Promise
|
|
19
|
+
{
|
|
20
|
+
fee: <Fee Paid Number>
|
|
21
|
+
secret: <Payment Preimage Hex String>
|
|
22
|
+
tokens: <Tokens Paid Number>
|
|
23
|
+
}
|
|
24
|
+
*/
|
|
25
|
+
module.exports = ({ask, lnd, logger, request}, cbk) => {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
return asyncAuto({
|
|
28
|
+
// Check arguments
|
|
29
|
+
validate: cbk => {
|
|
30
|
+
if (!ask) {
|
|
31
|
+
return cbk([400, 'ExpectedAskFunctionToBuyTradePreimage']);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!lnd) {
|
|
35
|
+
return cbk([400, 'ExpectedAuthenticatedLndToBuyTradePreimage']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!logger) {
|
|
39
|
+
return cbk([400, 'ExpectedWinstonLoggerToBuyTradePreimage']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!request) {
|
|
43
|
+
return cbk([400, 'ExpectedPaymentRequestToPurchaseTradePreimage']);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cbk();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Decode the payment request
|
|
50
|
+
parseRequest: ['validate', ({}, cbk) => {
|
|
51
|
+
try {
|
|
52
|
+
const details = parsePaymentRequest({request});
|
|
53
|
+
|
|
54
|
+
return cbk(null, {
|
|
55
|
+
cltv_delta: details.cltv_delta,
|
|
56
|
+
destination: details.destination,
|
|
57
|
+
features: details.features,
|
|
58
|
+
id: details.id,
|
|
59
|
+
mtokens: details.mtokens,
|
|
60
|
+
payment: details.payment,
|
|
61
|
+
routes: details.routes,
|
|
62
|
+
tokens: details.tokens,
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return cbk([400, 'ExpectedValidPaymentRequestToBuyPreimage', {err}]);
|
|
66
|
+
}
|
|
67
|
+
}],
|
|
68
|
+
|
|
69
|
+
// Subscribe to a probe to a route
|
|
70
|
+
getRoute: ['parseRequest', ({parseRequest}, cbk) => {
|
|
71
|
+
const sub = subscribeToProbeForRoute({
|
|
72
|
+
lnd,
|
|
73
|
+
cltv_delta: parseRequest.cltv_delta + cltvDeltaBuffer,
|
|
74
|
+
destination: parseRequest.destination,
|
|
75
|
+
features: parseRequest.features,
|
|
76
|
+
mtokens: parseRequest.mtokens,
|
|
77
|
+
payment: parseRequest.payment,
|
|
78
|
+
routes: parseRequest.routes,
|
|
79
|
+
total_mtokens: !!parseRequest.payment ? parseRequest.mtokens : null,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const done = (err, res) => {
|
|
83
|
+
sub.removeAllListeners();
|
|
84
|
+
|
|
85
|
+
return cbk(err, res);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
sub.once('end', () => done([503, 'FailedToFindPathToDestination']));
|
|
89
|
+
sub.once('error', err => done(err));
|
|
90
|
+
sub.once('probe_success', ({route}) => done(null, route));
|
|
91
|
+
|
|
92
|
+
sub.on('probing', ({route}) => {
|
|
93
|
+
return logger.info({checking_path_with_fee: route.fee});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return;
|
|
97
|
+
}],
|
|
98
|
+
|
|
99
|
+
// Confirm transaction fee
|
|
100
|
+
confirmFee: [
|
|
101
|
+
'getRoute',
|
|
102
|
+
'parseRequest',
|
|
103
|
+
({getRoute, parseRequest}, cbk) =>
|
|
104
|
+
{
|
|
105
|
+
return ask({
|
|
106
|
+
name: 'pay',
|
|
107
|
+
message: `Pay ${parseRequest.tokens} and ${getRoute.fee} fee?`,
|
|
108
|
+
type: 'confirm',
|
|
109
|
+
},
|
|
110
|
+
cbk);
|
|
111
|
+
}],
|
|
112
|
+
|
|
113
|
+
// Pay the request
|
|
114
|
+
pay: [
|
|
115
|
+
'confirmFee',
|
|
116
|
+
'getRoute',
|
|
117
|
+
'parseRequest',
|
|
118
|
+
({confirmFee, getRoute, parseRequest}, cbk) =>
|
|
119
|
+
{
|
|
120
|
+
if (!confirmFee.pay) {
|
|
121
|
+
return cbk([400, 'PaymentCanceled']);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.info({paying: getRoute.tokens});
|
|
125
|
+
|
|
126
|
+
return payViaRoutes({
|
|
127
|
+
lnd,
|
|
128
|
+
id: parseRequest.id,
|
|
129
|
+
routes: [getRoute],
|
|
130
|
+
},
|
|
131
|
+
cbk);
|
|
132
|
+
}],
|
|
133
|
+
|
|
134
|
+
// Final payment
|
|
135
|
+
paid: ['parseRequest', 'pay', ({parseRequest, pay}, cbk) => {
|
|
136
|
+
return cbk(null, {
|
|
137
|
+
fee: pay.fee,
|
|
138
|
+
secret: pay.secret,
|
|
139
|
+
tokens: parseRequest.tokens,
|
|
140
|
+
});
|
|
141
|
+
}],
|
|
142
|
+
},
|
|
143
|
+
returnResult({reject, resolve, of: 'paid'}, cbk));
|
|
144
|
+
});
|
|
145
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {createInvoice} = require('ln-service');
|
|
3
|
+
const {getIdentity} = require('ln-service');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
const encodeTrade = require('./encode_trade');
|
|
7
|
+
const encryptTradeSecret = require('./encrypt_trade_secret');
|
|
8
|
+
|
|
9
|
+
const asNumber = n => parseFloat(n, 10);
|
|
10
|
+
const {floor} = Math;
|
|
11
|
+
const isNumber = n => !isNaN(n);
|
|
12
|
+
const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
|
|
13
|
+
const maxDescriptionLength = 100;
|
|
14
|
+
const maxSecretLength = 100;
|
|
15
|
+
const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
|
|
16
|
+
|
|
17
|
+
/** Create a new trade
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
ask: <Ask Function>
|
|
21
|
+
lnd: <Authenticated LND API Object>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@returns via cbk or Promise
|
|
25
|
+
{
|
|
26
|
+
trade: <Hex Encoded Trade String>
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = ({ask, lnd}, cbk) => {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
return asyncAuto({
|
|
32
|
+
// Check arguments
|
|
33
|
+
validate: cbk => {
|
|
34
|
+
if (!ask) {
|
|
35
|
+
return cbk([400, 'ExpectedAskFunctionToCreateTrade']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!lnd) {
|
|
39
|
+
return cbk([400, 'ExpectedAuthenticatedLndToCreateTrade']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return cbk();
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Get the node identity key
|
|
46
|
+
getNodeId: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
|
|
47
|
+
|
|
48
|
+
// Ask for the public key of the node to trade with
|
|
49
|
+
askForNodeId: ['getNodeId', ({getNodeId}, cbk) => {
|
|
50
|
+
return ask({
|
|
51
|
+
name: 'id',
|
|
52
|
+
message: 'Public key of node you are trading with?',
|
|
53
|
+
type: 'input',
|
|
54
|
+
validate: input => {
|
|
55
|
+
if (!input) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isPublicKey(input)) {
|
|
60
|
+
return 'Expected public key of node to trade with';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (input === getNodeId.public_key) {
|
|
64
|
+
return 'Expected public key of other node';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
cbk);
|
|
71
|
+
}],
|
|
72
|
+
|
|
73
|
+
// Ask for how to describe the payload
|
|
74
|
+
askForDescription: ['askForNodeId', ({}, cbk) => {
|
|
75
|
+
return ask({
|
|
76
|
+
name: 'description',
|
|
77
|
+
message: `Describe the secret you are offering:`,
|
|
78
|
+
type: 'input',
|
|
79
|
+
validate: input => {
|
|
80
|
+
if (!input) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (input.length > maxDescriptionLength) {
|
|
85
|
+
return 'Expected shorter description';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
cbk);
|
|
92
|
+
}],
|
|
93
|
+
|
|
94
|
+
// Ask for the actual payload of the trade
|
|
95
|
+
askForSecret: ['askForDescription', ({askForDescription}, cbk) => {
|
|
96
|
+
return ask({
|
|
97
|
+
name: 'secret',
|
|
98
|
+
message: 'Enter the secret you want to sell',
|
|
99
|
+
type: 'input',
|
|
100
|
+
validate: input => {
|
|
101
|
+
if (!input) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (input.length > maxSecretLength) {
|
|
106
|
+
return 'Expected shorter secret value';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
cbk);
|
|
113
|
+
}],
|
|
114
|
+
|
|
115
|
+
// Ask for the price of the secret
|
|
116
|
+
askForPrice: ['askForSecret', ({}, cbk) => {
|
|
117
|
+
return ask({
|
|
118
|
+
name: 'tokens',
|
|
119
|
+
message: 'How much do you want to charge?',
|
|
120
|
+
validate: input => {
|
|
121
|
+
// Only allow numeric input
|
|
122
|
+
if (!isNumber(input)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Disallow fractional values
|
|
127
|
+
if (floor(input) !== asNumber(input)) {
|
|
128
|
+
return 'Specified precision not supported';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
cbk);
|
|
135
|
+
}],
|
|
136
|
+
|
|
137
|
+
// Encrypt the secret data
|
|
138
|
+
encryptPayload: [
|
|
139
|
+
'askForNodeId',
|
|
140
|
+
'askForSecret',
|
|
141
|
+
({askForNodeId, askForSecret}, cbk) =>
|
|
142
|
+
{
|
|
143
|
+
return encryptTradeSecret({
|
|
144
|
+
lnd,
|
|
145
|
+
secret: utf8AsHex(askForSecret.secret),
|
|
146
|
+
to: askForNodeId.id,
|
|
147
|
+
},
|
|
148
|
+
cbk);
|
|
149
|
+
}],
|
|
150
|
+
|
|
151
|
+
// Create the invoice to purchase the unlocking secret
|
|
152
|
+
createPurchaseInvoice: [
|
|
153
|
+
'askForDescription',
|
|
154
|
+
'askForPrice',
|
|
155
|
+
'encryptPayload',
|
|
156
|
+
({askForDescription, askForPrice, encryptPayload}, cbk) =>
|
|
157
|
+
{
|
|
158
|
+
return createInvoice({
|
|
159
|
+
lnd,
|
|
160
|
+
description: askForDescription.description,
|
|
161
|
+
secret: encryptPayload.payment_secret,
|
|
162
|
+
tokens: asNumber(askForPrice.tokens),
|
|
163
|
+
},
|
|
164
|
+
cbk);
|
|
165
|
+
}],
|
|
166
|
+
|
|
167
|
+
// Encode all the trade data into wire format
|
|
168
|
+
encodeTradeToWireFormat: [
|
|
169
|
+
'createPurchaseInvoice',
|
|
170
|
+
'encryptPayload',
|
|
171
|
+
({createPurchaseInvoice, encryptPayload}, cbk) =>
|
|
172
|
+
{
|
|
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]);
|
|
183
|
+
}
|
|
184
|
+
}],
|
|
185
|
+
},
|
|
186
|
+
returnResult({reject, resolve, of: 'encodeTradeToWireFormat'}, cbk));
|
|
187
|
+
});
|
|
188
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const {decodeTlvStream} = require('bolt01');
|
|
2
|
+
|
|
3
|
+
const networkFromNetworkRecord = require('./network_from_network_record');
|
|
4
|
+
const {requestRecordsAsRequest} = require('./../records');
|
|
5
|
+
|
|
6
|
+
const findAuth = records => records.find(n => n.type === '1');
|
|
7
|
+
const findCipher = records => records.find(n => n.type === '0');
|
|
8
|
+
const findDetails = records => records.find(n => n.type === '3');
|
|
9
|
+
const findEncrypt = records => records.find(n => n.type === '0');
|
|
10
|
+
const findNetwork = records => records.find(n => n.type === '1');
|
|
11
|
+
const findRequest = records => records.find(n => n.type === '2');
|
|
12
|
+
const findVersion = records => records.find(n => n.type === '0');
|
|
13
|
+
const isTrade = trade => trade.toLowerCase().startsWith('626f73ff');
|
|
14
|
+
const tradeData = trade => trade.slice('626f73ff'.length);
|
|
15
|
+
|
|
16
|
+
/** Encode a trade
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
trade: <Hex Encoded Trade String>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@throws
|
|
23
|
+
<Error>
|
|
24
|
+
|
|
25
|
+
@returns
|
|
26
|
+
{
|
|
27
|
+
auth: <Encrypted Payload Auth Hex String>
|
|
28
|
+
payload: <Preimage Encrypted Payload Hex String>
|
|
29
|
+
request: <BOLT 11 Payment Request String>
|
|
30
|
+
}
|
|
31
|
+
*/
|
|
32
|
+
module.exports = ({trade}) => {
|
|
33
|
+
if (!trade) {
|
|
34
|
+
throw new Error('ExpectedTradeToDecode');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isTrade(trade)) {
|
|
38
|
+
throw new Error('UnexpectedFormatOfTradeToDecode');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
decodeTlvStream({encoded: tradeData(trade)});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error('ExpectedValidTlvStreamForTradeData');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Decode the overall packet
|
|
48
|
+
const tradeRecords = decodeTlvStream({encoded: tradeData(trade)}).records;
|
|
49
|
+
|
|
50
|
+
// Get the trade version
|
|
51
|
+
const version = findVersion(tradeRecords);
|
|
52
|
+
|
|
53
|
+
if (!!version) {
|
|
54
|
+
throw new Error('UnexpectedVersionOfTradeData');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get the network record
|
|
58
|
+
const networkRecord = findNetwork(tradeRecords) || {};
|
|
59
|
+
|
|
60
|
+
// Get the payment request record
|
|
61
|
+
const requestRecord = findRequest(tradeRecords);
|
|
62
|
+
|
|
63
|
+
if (!requestRecord) {
|
|
64
|
+
throw new Error('ExpectedRequestRecordInTradeData');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const {request} = requestRecordsAsRequest({
|
|
68
|
+
network: networkFromNetworkRecord({value: networkRecord.value}),
|
|
69
|
+
encoded: requestRecord.value,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Get the trade details
|
|
73
|
+
const detailsRecord = findDetails(tradeRecords);
|
|
74
|
+
|
|
75
|
+
if (!detailsRecord) {
|
|
76
|
+
throw new Error('ExpctedDetailsRecordToDecodeTrade');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Trade details
|
|
80
|
+
const details = decodeTlvStream({encoded: detailsRecord.value}).records;
|
|
81
|
+
|
|
82
|
+
// Encode the encrypted data
|
|
83
|
+
const encryptedRecord = findEncrypt(details);
|
|
84
|
+
|
|
85
|
+
if (!encryptedRecord) {
|
|
86
|
+
throw new Error('ExpectedEncryptedRecordToDecodeTrade');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const encrypted = decodeTlvStream({encoded: encryptedRecord.value}).records;
|
|
90
|
+
|
|
91
|
+
const encryptedDataRecord = findCipher(encrypted);
|
|
92
|
+
|
|
93
|
+
if (!encryptedDataRecord) {
|
|
94
|
+
throw new Error('ExpectedEncryptedDataRecordToDecodeTrade');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const authDataRecord = findAuth(encrypted);
|
|
98
|
+
|
|
99
|
+
if (!authDataRecord) {
|
|
100
|
+
throw new Error('ExpectedAuthDataRecordToDecodeTrade');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
request,
|
|
105
|
+
auth: authDataRecord.value,
|
|
106
|
+
payload: encryptedDataRecord.value,
|
|
107
|
+
};
|
|
108
|
+
};
|