paid-services 3.13.2 → 3.14.2

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Versions
2
2
 
3
+ ## Version 3.14.2
4
+
5
+ - `manageTrades`: Fix listing of trades that have fiat-pricing
6
+
7
+ ## Version 3.14.1
8
+
9
+ - `manageTrades`: Add support for dynamic fiat-priced trades
10
+
3
11
  ## Version 3.13.2
4
12
 
5
13
  - `acceptTrade`: Fix `logger` to be documented as an arg and optional
@@ -1,9 +1,8 @@
1
- const {encodeBigSize} = require('bolt01');
2
1
  const {encodeTlvStream} = require('bolt01');
3
2
 
4
- const encodeMessage = message => `626f73ff${message}`;
5
3
  const encodeResponseCode = require('./encode_response_code');
6
4
 
5
+ const encodeMessage = message => `626f73ff${message}`;
7
6
  const headers = null;
8
7
  const params = null;
9
8
  const version = null;
package/package.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "url": "https://github.com/alexbosworth/paid-services/issues"
8
8
  },
9
9
  "dependencies": {
10
+ "@alexbosworth/fiat": "1.0.1",
10
11
  "async": "3.2.3",
11
12
  "asyncjs-util": "1.2.8",
12
13
  "bolt01": "1.2.3",
@@ -42,5 +43,5 @@
42
43
  "integration-tests": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 180 test/integration/*.js",
43
44
  "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/balanced/*.js test/capacity/*.js test/client/*.js test/config/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/server/*.js test/services/*.js test/trades/*.js"
44
45
  },
45
- "version": "3.13.2"
46
+ "version": "3.14.2"
46
47
  }
@@ -3,6 +3,54 @@ const {test} = require('@alexbosworth/tap');
3
3
  const method = require('./../../trades/encode_open_trade');
4
4
 
5
5
  const tests = [
6
+ {
7
+ args: {},
8
+ description: 'A network name is required',
9
+ error: 'ExpectedNetworkNameToEncodeOpenTrade',
10
+ },
11
+ {
12
+ args: {network: 'btc'},
13
+ description: 'Nodes array is required',
14
+ error: 'ExpectedArrayOfNodesToEncodeOpenTrade',
15
+ },
16
+ {
17
+ args: {network: 'btc', nodes: []},
18
+ description: 'Nodes are required',
19
+ error: 'ExpectedNodeToReferToInOpenTrade',
20
+ },
21
+ {
22
+ args: {
23
+ network: 'btctestnet',
24
+ nodes: [{
25
+ channels: [{
26
+ id: '1x2x3',
27
+ partner_public_key: Buffer.alloc(33, 2).toString('hex'),
28
+ }],
29
+ id: Buffer.alloc(33, 3).toString('hex'),
30
+ sockets: [],
31
+ }],
32
+ },
33
+ description: 'A network record is added',
34
+ expected: {trade: '626f73ff000101010101040c000a01080000010000020003'},
35
+ },
36
+ {
37
+ args: {
38
+ id: Buffer.alloc(32).toString('hex'),
39
+ network: 'btctestnet',
40
+ nodes: [{
41
+ channels: [{
42
+ id: '1x2x3',
43
+ partner_public_key: Buffer.alloc(33, 2).toString('hex'),
44
+ }],
45
+ id: Buffer.alloc(33, 3).toString('hex'),
46
+ sockets: [],
47
+ }],
48
+ },
49
+ description: 'A trade id record is added',
50
+ expected: {
51
+ trade: '626f73ff000101010101040c000a0108000001000002000305200000000000000000000000000000000000000000000000000000000000000000',
52
+ },
53
+ },
6
54
  {
7
55
  args: {
8
56
  network: 'btc',
@@ -3,6 +3,29 @@ const {test} = require('@alexbosworth/tap');
3
3
  const method = require('./../../trades/encode_trade');
4
4
 
5
5
  const tests = [
6
+ {
7
+ args: {},
8
+ description: 'Either connect or secret is required',
9
+ error: 'ExpectedEitherConnectDetailsOrTradeSecret',
10
+ },
11
+ {
12
+ args: {connect: true, secret: true},
13
+ description: 'Cannot encode both connect and secret',
14
+ error: 'ExpectedOnlyOneConnectDetailsOrTradeSecretNotBoth',
15
+ },
16
+ {
17
+ args: {
18
+ secret: {
19
+ auth: '00',
20
+ payload: '11',
21
+ request: 'lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w',
22
+ },
23
+ },
24
+ description: 'Trade secret is encoded',
25
+ expected: {
26
+ trade: '626f73ff02960001e801910b25fe64410d00004080c1014181c20240004080c1014181c20240004080c1014181c202404081a1fa83632b0b9b29031b7b739b4b232b91039bab83837b93a34b733903a3434b990383937b532b1ba038ec6891345e204145be8a3a99de38e98a39d6a569434e1845c8af7205afcfcc7f425fcd1463e93c32881ead0d6e356d467ec8c02553f9aab15e5738b11f127f0003080006000111010100',
27
+ },
28
+ },
6
29
  {
7
30
  args: {
8
31
  connect: {
@@ -17,7 +40,7 @@ const tests = [
17
40
  }],
18
41
  },
19
42
  },
20
- description: 'Trade is encoded',
43
+ description: 'Trade connect is encoded',
21
44
  expected: {trade: '626f73ff000101040c000a01080000010000020003'},
22
45
  },
23
46
  ];
@@ -1,45 +1,58 @@
1
+ const {acceptsChannelOpen} = require('ln-sync');
1
2
  const asyncAuto = require('async/auto');
2
3
  const asyncEach = require('async/each');
3
4
  const {cancelHodlInvoice} = require('ln-service');
5
+ const {getChainFeeRate} = require('ln-service');
4
6
  const {getInvoice} = require('ln-service');
7
+ const {openChannel} = require('ln-service');
5
8
  const {returnResult} = require('asyncjs-util');
6
9
  const {settleHodlInvoice} = require('ln-service');
7
10
 
8
- const openChannel = require('./open_channel');
9
-
10
11
  const {isArray} = Array;
11
- const sellAction = 'sell';
12
+ const slowConf = 144;
12
13
 
13
14
  /** Accept an open ended trade
14
15
 
15
16
  {
16
17
  cancel: [<Alternative Invoice Id Hex String>]
18
+ [channel]: <Channel Capacity Tokens Number>
19
+ from: <Trade With Node Identity Public Key Hex String>
17
20
  id: <Trade Id Hex String>
18
21
  lnd: <Authenticated LND API Object>
19
- [logger]: <Winston Logger Object>
20
22
  secret: <Invoice to Settle Preimage Hex String>
21
23
  }
22
24
 
23
25
  @returns via cbk or Promise
26
+ {
27
+ [opening_channel]: {
28
+ fee_tokens_per_vbyte: <Chain Fee Rate Number>
29
+ transaction_id: <Channel Funding Transaction Id Hex String>
30
+ transaction_vout: <Channel Funding Transaction Output Index Number>
31
+ }
32
+ }
24
33
  */
25
- module.exports = (args, cbk) => {
34
+ module.exports = ({cancel, channel, from, id, lnd, secret}, cbk) => {
26
35
  return new Promise((resolve, reject) => {
27
36
  return asyncAuto({
28
37
  // Check arguments
29
38
  validate: cbk => {
30
- if (!isArray(args.cancel)) {
39
+ if (!isArray(cancel)) {
31
40
  return cbk([400, 'ExpectedArrayOfIdsToCancelToAcceptTrade']);
32
41
  }
33
42
 
34
- if (!args.id) {
43
+ if (!from) {
44
+ return cbk([400, 'ExpectedWithPeerPublicKeyToAcceptTrade']);
45
+ }
46
+
47
+ if (!id) {
35
48
  return cbk([400, 'ExpectedAnchorTradeIdToAcceptTrade']);
36
49
  }
37
50
 
38
- if (!args.lnd) {
51
+ if (!lnd) {
39
52
  return cbk([400, 'ExpectedAuthenticatedLndToAcceptTrade']);
40
53
  }
41
54
 
42
- if (!args.secret) {
55
+ if (!secret) {
43
56
  return cbk([400, 'ExpectedSettlementPreimageToAcceptTrade']);
44
57
  }
45
58
 
@@ -47,49 +60,86 @@ module.exports = (args, cbk) => {
47
60
  },
48
61
 
49
62
  // Fetch the anchor invoice to make sure it's still open
50
- getAnchor: ['validate', ({}, cbk) => {
51
- return getInvoice({id: args.id, lnd: args.lnd}, cbk);
63
+ getAnchor: ['validate', ({}, cbk) => getInvoice({id, lnd}, cbk)],
64
+
65
+ // Get the chain fee rate for channel opening trades
66
+ getFeeRate: ['validate', ({}, cbk) => {
67
+ // Exit early when not trading a channel open
68
+ if (!channel) {
69
+ return cbk();
70
+ }
71
+
72
+ return getChainFeeRate({lnd, confirmation_target: slowConf}, cbk);
52
73
  }],
53
74
 
54
75
  // Cancel alternative invoices so that only one resolves as settled
55
- cancel: ['getAnchor', ({}, cbk) => {
56
- return asyncEach([].concat(args.cancel).concat(args.id), (id, cbk) => {
57
- return cancelHodlInvoice({id, lnd: args.lnd}, cbk);
76
+ cancel: ['getAnchor', ({getAnchor}, cbk) => {
77
+ if (!!getAnchor.is_canceled) {
78
+ return cbk([404, 'OpenTradeNotFound']);
79
+ }
80
+
81
+ return asyncEach([].concat(cancel).concat(id), (alternative, cbk) => {
82
+ return cancelHodlInvoice({lnd, id: alternative}, cbk);
83
+ },
84
+ cbk);
85
+ }],
86
+
87
+ // Check opening for channel trades
88
+ acceptsOpen: ['cancel', ({}, cbk) => {
89
+ // Exit early when not opening a channel
90
+ if (!channel) {
91
+ return cbk();
92
+ }
93
+
94
+ return acceptsChannelOpen({
95
+ lnd,
96
+ capacity: channel,
97
+ partner_public_key: from,
58
98
  },
59
99
  cbk);
60
100
  }],
61
101
 
62
102
  // Settle the held invoice with the preimage
63
- settle: ['cancel', ({}, cbk) => {
64
- return settleHodlInvoice({lnd: args.lnd, secret: args.secret}, cbk);
103
+ settle: ['acceptsOpen', 'cancel', 'getFeeRate', ({}, cbk) => {
104
+ return settleHodlInvoice({lnd, secret}, cbk);
65
105
  }],
66
106
 
67
107
  // Open the channel
68
- openChannel: ['settle', ({}, cbk) => {
69
- // Exit early when the channel action is not a sell action
70
- if (args.action !== sellAction) {
108
+ openChannel: ['getFeeRate', 'settle', ({getFeeRate}, cbk) => {
109
+ // Exit early when this is not a channel trade
110
+ if (!channel) {
71
111
  return cbk();
72
112
  }
73
113
 
74
114
  return openChannel({
75
- id: args.partner_public_key,
76
- lnd: args.lnd,
77
- tokens: args.capacity,
115
+ lnd,
116
+ chain_fee_rate: getFeeRate.tokens_per_vbyte,
117
+ local_tokens: channel,
118
+ partner_public_key: from,
78
119
  },
79
- (err, res) => {
80
- if (!!err) {
81
- return cbk(err);
82
- }
120
+ cbk);
121
+ }],
83
122
 
84
- if (!!args.logger) {
85
- args.logger.info({channel_opened: res});
86
- }
123
+ // Trade is settled
124
+ settled: [
125
+ 'getFeeRate',
126
+ 'openChannel',
127
+ ({getFeeRate, openChannel}, cbk) =>
128
+ {
129
+ // Exit early when there is no trade result
130
+ if (!openChannel) {
131
+ return cbk(null, {});
132
+ }
87
133
 
88
- return cbk();
89
- },
90
- cbk);
134
+ return cbk(null, {
135
+ opening_channel: {
136
+ fee_tokens_per_vbyte: getFeeRate.tokens_per_vbyte,
137
+ transaction_id: openChannel.transaction_id,
138
+ transaction_vout: openChannel.transaction_vout,
139
+ },
140
+ });
91
141
  }],
92
142
  },
93
- returnResult({reject, resolve}, cbk));
143
+ returnResult({reject, resolve, of: 'settled'}, cbk));
94
144
  });
95
145
  };
@@ -0,0 +1,251 @@
1
+ const {randomBytes} = require('crypto');
2
+
3
+ const asyncAuto = require('async/auto');
4
+ const asyncRetry = require('async/retry');
5
+ const {parsePaymentRequest} = require('ln-service');
6
+ const {returnResult} = require('asyncjs-util');
7
+
8
+ const buyPreimage = require('./buy_preimage');
9
+ const connectToSeller = require('./connect_to_seller');
10
+ const decodeBasicTrade = require('./decode_basic_trade');
11
+ const decodeTrade = require('./decode_trade');
12
+ const {makePeerRequest} = require('./../p2p');
13
+ const {servicePeerRequests} = require('./../p2p');
14
+ const {serviceTypeReceiveChannelSale} = require('./../service_types');
15
+ const {serviceTypeRequestChannelSale} = require('./../service_types');
16
+
17
+ const findBasicRecord = records => records.find(n => n.type === '1');
18
+ const findIdRecord = records => records.find(n => n.type === '0');
19
+ const findTradeRecord = records => records.find(n => n.type === '1');
20
+ const {isArray} = Array;
21
+ const makeRequestId = () => randomBytes(32).toString('hex');
22
+ const requestTradeTimeoutMs = 1000 * 30;
23
+ const requestTradesTimeoutMs = 1000 * 30;
24
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
25
+ const tradeIdRecordType = '0';
26
+ const tradesRequestIdType = '1';
27
+
28
+ /** Purchase a channel
29
+
30
+ {
31
+ ask: <Ask Function>
32
+ lnd: <Authenticated LND API Object>
33
+ logger: <Winston Logger Object>
34
+ }
35
+
36
+ @returns via cbk or Promise
37
+ */
38
+ module.exports = ({ask, lnd, logger}, cbk) => {
39
+ return new Promise((resolve, reject) => {
40
+ return asyncAuto({
41
+ // Check arguments
42
+ validate: cbk => {
43
+ if (!ask) {
44
+ return cbk([400, 'ExpectedAskFunctionToBuyChannel']);
45
+ }
46
+
47
+ if (!lnd) {
48
+ return cbk([400, 'ExpectedAuthenticatedLndToBuyChannel']);
49
+ }
50
+
51
+ if (!logger) {
52
+ return cbk([400, 'ExpectedWinstonLoggerToBuyChannel']);
53
+ }
54
+
55
+ return cbk();
56
+ },
57
+
58
+ // Get the trade data to decode it
59
+ askForTrade: ['validate', ({}, cbk) => {
60
+ return ask({
61
+ name: 'trade',
62
+ message: 'Enter encoded trade',
63
+ type: 'input',
64
+ validate: trade => {
65
+ if (!trade) {
66
+ return false;
67
+ }
68
+
69
+ try {
70
+ decodeTrade({trade});
71
+ } catch (err) {
72
+ return 'Failed to parse trade details, try re-entering it?';
73
+ }
74
+
75
+ // The trade type should be a connect type
76
+ if (!decodeTrade({trade}).connect) {
77
+ return 'Incorrect type of trade, enter a channel sale trade?';
78
+ }
79
+
80
+ return true;
81
+ },
82
+ },
83
+ cbk);
84
+ }],
85
+
86
+ // Decode the connect details
87
+ connectDetails: ['askForTrade', ({askForTrade}, cbk) => {
88
+ try {
89
+ const {connect} = decodeTrade({trade: askForTrade.trade});
90
+
91
+ if (!connect) {
92
+ return cbk([400, 'ExpectedConnectDetailsInTradeInput']);
93
+ }
94
+
95
+ return cbk(null, connect);
96
+ } catch (err) {
97
+ return cbk([400, err.message]);
98
+ }
99
+ }],
100
+
101
+ // Connect to the selling node
102
+ connectToSeller: ['connectDetails', ({connectDetails}, cbk) => {
103
+ return connectToSeller({
104
+ lnd,
105
+ logger,
106
+ nodes: connectDetails.nodes,
107
+ },
108
+ cbk);
109
+ }],
110
+
111
+ // Get channels being sold
112
+ requestTrade: ['connectToSeller', ({connectToSeller}, cbk) => {
113
+ const requestId = makeRequestId();
114
+ const service = servicePeerRequests({lnd});
115
+
116
+ service.request({type: serviceTypeReceiveChannelSale}, (req, res) => {
117
+ const requestIdRecord = findIdRecord(req.records);
118
+
119
+ // Exit early when this is a request for something else
120
+ if (!requestIdRecord || requestIdRecord.value !== requestId) {
121
+ return;
122
+ }
123
+
124
+ // Make sure there is a basic trade record in the records
125
+ if (!findBasicRecord(req.records)) {
126
+ return res.failure([400, 'ExpectedBasicTradeRecord']);
127
+ }
128
+
129
+ // Make sure the basic trade record is valid
130
+ try {
131
+ decodeBasicTrade({records: req.records});
132
+ } catch (err) {
133
+ return res.failure([400, 'ExpectedValidBasicTradeRecord']);
134
+ }
135
+
136
+ const trade = decodeBasicTrade({records: req.records});
137
+
138
+ res.success({});
139
+
140
+ // Don't listen for more channel sales
141
+ service.stop({});
142
+
143
+ return cbk(null, trade);
144
+ });
145
+
146
+ logger.info({requesting_trade_details: true});
147
+
148
+ // Once connected, ask for the trade details
149
+ return makePeerRequest({
150
+ lnd,
151
+ records: [{type: tradesRequestIdType, value: requestId}],
152
+ timeout: requestTradesTimeoutMs,
153
+ to: connectToSeller.id,
154
+ type: serviceTypeRequestChannelSale,
155
+ },
156
+ (err, res) => {
157
+ if (!!err) {
158
+ service.stop({});
159
+
160
+ return cbk(err);
161
+ }
162
+
163
+ // A trade will be pinged back into the listening service
164
+ return;
165
+ });
166
+ }],
167
+
168
+ // Get the specific trade details
169
+ getTrade: [
170
+ 'connectToSeller',
171
+ 'requestTrade',
172
+ ({connectToSeller, requestTrade}, cbk) =>
173
+ {
174
+ return makePeerRequest({
175
+ lnd,
176
+ records: [{type: tradeIdRecordType, value: requestTrade.id}],
177
+ timeout: requestTradeTimeoutMs,
178
+ to: connectToSeller.id,
179
+ type: serviceTypeRequestChannelSale,
180
+ },
181
+ (err, res) => {
182
+ if (!!err) {
183
+ return cbk(err);
184
+ }
185
+
186
+ const tradeRecord = findTradeRecord(res.records);
187
+
188
+ if (!tradeRecord) {
189
+ return cbk([503, 'ExpectedTradeRecordFromPeer']);
190
+ }
191
+
192
+ try {
193
+ decodeTrade({trade: tradeRecord.value});
194
+ } catch (err) {
195
+ return cbk([503, err.message]);
196
+ }
197
+
198
+ const {payment} = decodeTrade({trade: tradeRecord.value});
199
+
200
+ if (!payment) {
201
+ return cbk([503, 'ExpectedTradePaymentInResponseForTradeById']);
202
+ }
203
+
204
+ return cbk(null, {request: payment.request});
205
+ });
206
+ }],
207
+
208
+ // Parse the trade payment request
209
+ requestDetails: ['getTrade', ({getTrade}, cbk) => {
210
+ try {
211
+ return cbk(null, parsePaymentRequest({request: getTrade.request}));
212
+ } catch (err) {
213
+ return cbk([503, 'FailedToParseBuyChannelPaymentRequest', {err}]);
214
+ }
215
+ }],
216
+
217
+ // Pay and get the preimage through paying for it
218
+ payForPreimage: [
219
+ 'getTrade',
220
+ 'requestDetails',
221
+ ({getTrade, requestDetails}, cbk) =>
222
+ {
223
+ logger.info({
224
+ request: getTrade.request,
225
+ purchase: requestDetails.description,
226
+ price: tokensAsBigUnit(requestDetails.tokens),
227
+ to: requestDetails.destination,
228
+ });
229
+
230
+ const {request} = getTrade;
231
+
232
+ return asyncRetry({
233
+ errorFilter: err => !!isArray(err) && err.slice().shift() >= 500,
234
+ },
235
+ cbk => buyPreimage({ask, lnd, logger, request}, cbk), cbk);
236
+ }],
237
+
238
+ // Signal that the payment was made
239
+ finished: ['payForPreimage', ({payForPreimage}, cbk) => {
240
+ logger.info({
241
+ price: payForPreimage.tokens,
242
+ payment_proof: payForPreimage.secret,
243
+ routing_fee: payForPreimage.fee,
244
+ });
245
+
246
+ return cbk();
247
+ }],
248
+ },
249
+ returnResult({reject, resolve}, cbk));
250
+ });
251
+ };
@@ -5,6 +5,7 @@ const {returnResult} = require('asyncjs-util');
5
5
  const {subscribeToProbeForRoute} = require('ln-service');
6
6
 
7
7
  const cltvDeltaBuffer = 5;
8
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
8
9
 
9
10
  /** Buy the preimage for a trade
10
11
 
@@ -134,9 +135,9 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
134
135
  // Final payment
135
136
  paid: ['parseRequest', 'pay', ({parseRequest, pay}, cbk) => {
136
137
  return cbk(null, {
137
- fee: pay.fee,
138
+ fee: tokensAsBigUnit(pay.fee),
138
139
  secret: pay.secret,
139
- tokens: parseRequest.tokens,
140
+ tokens: tokensAsBigUnit(parseRequest.tokens),
140
141
  });
141
142
  }],
142
143
  },