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 {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.1",
11
- "asyncjs-util": "1.2.6",
12
- "bolt01": "1.2.2",
13
- "invoices": "2.0.0",
14
- "ln-service": "52.6.0"
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
- "mock-lnd": "1.4.0",
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.0.0"
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
+ };