paid-services 3.12.2 → 3.13.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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Versions
2
2
 
3
+ ## Version 3.13.0
4
+
5
+ - `manageTrades`: Add support for experimental channel trades
6
+
3
7
  ## Version 3.12.2
4
8
 
5
9
  - `serviceOpenTrade`: Fix regtest network support
package/package.json CHANGED
@@ -42,5 +42,5 @@
42
42
  "integration-tests": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 180 test/integration/*.js",
43
43
  "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
44
  },
45
- "version": "3.12.2"
45
+ "version": "3.13.0"
46
46
  }
@@ -3,6 +3,8 @@
3
3
  "serviceTypeChangeCapacity": "8050001",
4
4
  "serviceTypeReceiveTrades": "8050006",
5
5
  "serviceTypeRequestTrades": "8050005",
6
+ "serviceTypeReceiveChannelSale": "8050008",
7
+ "serviceTypeRequestChannelSale": "8050007",
6
8
  "serviceTypeSignCapacityChange": "8050004",
7
9
  "serviceTypeWaitOnConfirmation": "8050003",
8
10
  "types": {
@@ -11,6 +13,8 @@
11
13
  "8050003": "serviceTypeWaitOnConfirmation",
12
14
  "8050004": "serviceTypeSignCapacityChange",
13
15
  "8050005": "serviceTypeRequestTrades",
14
- "8050006": "serviceTypeReceiveTrades"
16
+ "8050006": "serviceTypeReceiveTrades",
17
+ "8050007": "serviceTypeRequestChannelSale",
18
+ "8050008": "serviceTypeReceiveChannelSale"
15
19
  }
16
20
  }
@@ -5,7 +5,10 @@ const {getInvoice} = require('ln-service');
5
5
  const {returnResult} = require('asyncjs-util');
6
6
  const {settleHodlInvoice} = require('ln-service');
7
7
 
8
+ const openChannel = require('./open_channel');
9
+
8
10
  const {isArray} = Array;
11
+ const sellAction = 'sell';
9
12
 
10
13
  /** Accept an open ended trade
11
14
 
@@ -18,24 +21,28 @@ const {isArray} = Array;
18
21
 
19
22
  @returns via cbk or Promise
20
23
  */
21
- module.exports = ({cancel, id, lnd, secret}, cbk) => {
24
+ module.exports = (args, cbk) => {
22
25
  return new Promise((resolve, reject) => {
23
26
  return asyncAuto({
24
27
  // Check arguments
25
28
  validate: cbk => {
26
- if (!isArray(cancel)) {
29
+ if (!isArray(args.cancel)) {
27
30
  return cbk([400, 'ExpectedArrayOfIdsToCancelToAcceptTrade']);
28
31
  }
29
32
 
30
- if (!id) {
33
+ if (!args.id) {
31
34
  return cbk([400, 'ExpectedAnchorTradeIdToAcceptTrade']);
32
35
  }
33
36
 
34
- if (!lnd) {
37
+ if (!args.lnd) {
35
38
  return cbk([400, 'ExpectedAuthenticatedLndToAcceptTrade']);
36
39
  }
37
40
 
38
- if (!secret) {
41
+ if (!args.logger) {
42
+ return cbk([400, 'ExpectedLoggerToAcceptTrade']);
43
+ }
44
+
45
+ if (!args.secret) {
39
46
  return cbk([400, 'ExpectedSettlementPreimageToAcceptTrade']);
40
47
  }
41
48
 
@@ -43,18 +50,46 @@ module.exports = ({cancel, id, lnd, secret}, cbk) => {
43
50
  },
44
51
 
45
52
  // Fetch the anchor invoice to make sure it's still open
46
- getAnchor: ['validate', ({}, cbk) => getInvoice({id, lnd}, cbk)],
53
+ getAnchor: ['validate', ({}, cbk) => {
54
+ return getInvoice({id: args.id, lnd: args.lnd}, cbk);
55
+ }],
47
56
 
48
57
  // Cancel alternative invoices so that only one resolves as settled
49
58
  cancel: ['getAnchor', ({}, cbk) => {
50
- return asyncEach([].concat(cancel).concat(id), (id, cbk) => {
51
- return cancelHodlInvoice({id, lnd}, cbk);
59
+ return asyncEach([].concat(args.cancel).concat(args.id), (id, cbk) => {
60
+ return cancelHodlInvoice({id, lnd: args.lnd}, cbk);
52
61
  },
53
62
  cbk);
54
63
  }],
55
64
 
56
65
  // Settle the held invoice with the preimage
57
- settle: ['cancel', ({}, cbk) => settleHodlInvoice({lnd, secret}, cbk)],
66
+ settle: ['cancel', ({}, cbk) => {
67
+ return settleHodlInvoice({lnd: args.lnd, secret: args.secret}, cbk);
68
+ }],
69
+
70
+ // Open the channel
71
+ openChannel: ['settle', ({}, cbk) => {
72
+ // Exit early when the channel action is not a sell action
73
+ if (args.action !== sellAction) {
74
+ return cbk();
75
+ }
76
+
77
+ return openChannel({
78
+ id: args.partner_public_key,
79
+ lnd: args.lnd,
80
+ tokens: args.capacity,
81
+ },
82
+ (err, res) => {
83
+ if (!!err) {
84
+ return cbk(err);
85
+ }
86
+
87
+ args.logger.info({channel_opened: res});
88
+
89
+ return cbk();
90
+ },
91
+ cbk);
92
+ }],
58
93
  },
59
94
  returnResult({reject, resolve}, cbk));
60
95
  });
@@ -0,0 +1,233 @@
1
+ const {randomBytes} = require('crypto');
2
+
3
+ const asyncAuto = require('async/auto');
4
+ const asyncReflect = require('async/reflect');
5
+ const {getChainFeeRate} = require('ln-service');
6
+ const {getChannels} = require('ln-service');
7
+ const {getNetwork} = require('ln-sync');
8
+ const {getWalletInfo} = require('ln-service');
9
+ const {signMessage} = require('ln-service');
10
+ const {returnResult} = require('asyncjs-util');
11
+
12
+ const createAnchoredTrade = require('./create_anchored_trade');
13
+ const serviceOpenTrade = require('./service_open_trade');
14
+
15
+ const asNumber = n => parseFloat(n, 10);
16
+ const daysAsMs = days => Number(days) * 1000 * 60 * 60 * 24;
17
+ const defaultExpirationDays = 1;
18
+ const futureDate = ms => new Date(Date.now() + ms).toISOString();
19
+ const isNumber = n => !isNaN(n);
20
+ const query = 'How much would you like to sell?';
21
+ const saleCost = (amount, rate) => (amount * rate / 1000000).toFixed(0);
22
+ const saleSecret = randomBytes(48).toString('hex');
23
+ const tradeDescription = (alias, tokens) => `channelsale:${alias}-${tokens}`;
24
+
25
+ /** Create a new channel sale
26
+
27
+ {
28
+ action: <Channel Sale Action String>
29
+ ask: <Ask Function>
30
+ balance: <Total Available Chain Confirmed Balance Tokens Number>
31
+ lnd: <Authenticated LND API Object>
32
+ logger: <Winston Logger Object>
33
+ }
34
+
35
+ @returns via cbk or Promise
36
+ */
37
+ module.exports = ({action, ask, balance, lnd, logger}, cbk) => {
38
+ return new Promise((resolve, reject) => {
39
+ return asyncAuto({
40
+ // Check arguments
41
+ validate: cbk => {
42
+ if (!action) {
43
+ return cbk([400, 'ExpectedActionTypeToCreateChannelSale']);
44
+ }
45
+
46
+ if (!ask) {
47
+ return cbk([400, 'ExpectedAskFunctionToCreateChannelSale']);
48
+ }
49
+
50
+ if (balance === undefined) {
51
+ return cbk([400, 'ExpectedOnChainBalanceToCreateChannelSale']);
52
+ }
53
+
54
+ if (!lnd) {
55
+ return cbk([400, 'ExpectedLndToCreateChannelSale']);
56
+ }
57
+
58
+ if (!logger) {
59
+ return cbk([400, 'ExpectedLoggerToCreateChannelSale']);
60
+ }
61
+
62
+ return cbk();
63
+ },
64
+
65
+ // Get the public channels to use for an open trade
66
+ getChannels: ['validate', ({}, cbk) => {
67
+ return getChannels({lnd, is_public: true}, cbk);
68
+ }],
69
+
70
+ // Get self identity, including alias
71
+ getIdentity: ['validate', ({}, cbk) => getWalletInfo({lnd}, cbk)],
72
+
73
+ // Get the network name to use for an open trade
74
+ getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
75
+
76
+ // Ask for sale amount
77
+ askForAmount: ['validate', ({}, cbk) => {
78
+ return ask({
79
+ message: `${query} (Available balance: ${balance})`,
80
+ name: 'amount',
81
+ type: 'input',
82
+ validate: input => {
83
+ if (!input) {
84
+ return false;
85
+ }
86
+
87
+ // The token amount should be numeric
88
+ if (!isNumber(input)) {
89
+ return 'Expected numeric amount for sale';
90
+ }
91
+
92
+ if (input > balance) {
93
+ return 'Sale amount cannot be more than available balance';
94
+ }
95
+
96
+ return true;
97
+ },
98
+ },
99
+ (err, result) => {
100
+ if (!!err) {
101
+ return cbk([503, 'UnexpectedErrorAskingForSaleAmount', {err}]);
102
+ }
103
+
104
+ return cbk(null, result);
105
+ });
106
+ }],
107
+
108
+ // Ask for the rate
109
+ askForRate: ['askForAmount', ({}, cbk) => {
110
+ return ask({
111
+ default: '1',
112
+ message: 'Price of channel (in ppm)?',
113
+ name: 'rate',
114
+ type: 'input',
115
+ validate: input => {
116
+ if (!input) {
117
+ return false;
118
+ }
119
+
120
+ // Price of sale should be numeric
121
+ if (!isNumber(input)) {
122
+ return 'Expected numeric fee rate for sale';
123
+ }
124
+
125
+ return true;
126
+ },
127
+ },
128
+ (err, result) => {
129
+ if (!!err) {
130
+ return cbk([503, 'UnexpectedErrorAskingForRate', err]);
131
+ }
132
+
133
+ return cbk(null, result);
134
+ });
135
+ }],
136
+
137
+ // Ask for the expiration of the channel sale
138
+ askForExpiration: ['askForRate', ({}, cbk) => {
139
+ return ask({
140
+ default: defaultExpirationDays,
141
+ name: 'days',
142
+ message: 'Days to offer this for?',
143
+ validate: input => {
144
+ if (!isNumber(input) || !Number(input)) {
145
+ return false;
146
+ }
147
+
148
+ return true;
149
+ },
150
+ },
151
+ cbk);
152
+ }],
153
+
154
+ // Calculate sale cost
155
+ saleCost: [
156
+ 'askForAmount',
157
+ 'askForRate',
158
+ ({askForRate, askForAmount}, cbk) =>
159
+ {
160
+ return cbk(null, saleCost(askForAmount.amount, askForRate.rate));
161
+ }],
162
+
163
+ // Description of sale
164
+ description: [
165
+ 'askForAmount',
166
+ 'getIdentity',
167
+ ({askForAmount, getIdentity}, cbk) =>
168
+ {
169
+ const alias = getIdentity.alias || getIdentity.public_key;
170
+
171
+ return cbk(null, tradeDescription(alias, askForAmount.amount));
172
+ }],
173
+
174
+ // Create an anchor invoice for the channel sale
175
+ createAnchor: [
176
+ 'askForExpiration',
177
+ 'askForRate',
178
+ 'description',
179
+ 'saleCost',
180
+ ({askForExpiration, description, saleCost}, cbk) =>
181
+ {
182
+ return createAnchoredTrade({
183
+ description,
184
+ lnd,
185
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
186
+ secret: saleSecret,
187
+ tokens: asNumber(saleCost),
188
+ },
189
+ cbk);
190
+ }],
191
+
192
+ // Wait for a peer to connect and ask for the channel sale details
193
+ serviceSaleRequests: [
194
+ 'askForAmount',
195
+ 'askForExpiration',
196
+ 'createAnchor',
197
+ 'description',
198
+ 'getChannels',
199
+ 'getNetwork',
200
+ 'getIdentity',
201
+ ({
202
+ askForAmount,
203
+ askForExpiration,
204
+ createAnchor,
205
+ description,
206
+ getChannels,
207
+ getNetwork,
208
+ getIdentity,
209
+ saleCost,
210
+ },
211
+ cbk) =>
212
+ {
213
+ return serviceOpenTrade({
214
+ action,
215
+ description,
216
+ lnd,
217
+ logger,
218
+ capacity: askForAmount.amount,
219
+ channels: getChannels.channels,
220
+ expires_at: futureDate(daysAsMs(askForExpiration.days)),
221
+ id: createAnchor.id,
222
+ network: getNetwork.network,
223
+ public_key: getIdentity.public_key,
224
+ secret: saleSecret,
225
+ tokens: asNumber(saleCost),
226
+ uris: (getIdentity.uris || []),
227
+ },
228
+ cbk);
229
+ }],
230
+ },
231
+ returnResult({reject, resolve}, cbk));
232
+ });
233
+ };
@@ -3,23 +3,30 @@ const {createHash} = require('crypto');
3
3
  const asyncAuto = require('async/auto');
4
4
  const {createHodlInvoice} = require('ln-service');
5
5
  const {createInvoice} = require('ln-service');
6
+ const {getChainFeeRate} = require('ln-service');
6
7
  const {returnResult} = require('asyncjs-util');
7
8
 
8
9
  const encodeTradeSecret = require('./encode_trade_secret');
9
10
  const encryptTradeSecret = require('./encrypt_trade_secret');
10
11
 
11
12
  const bufferAsHex = buffer => buffer.toString('hex');
13
+ const {ceil} = Math;
12
14
  const hexAsBuffer = hex => Buffer.from(hex, 'hex');
15
+ const openSizeVbytes = 250;
16
+ const sellAction = 'sell';
13
17
  const sha256 = preimage => createHash('sha256').update(preimage).digest();
18
+ const slowConf = 144;
14
19
  const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
15
20
 
16
21
  /** Create a trade secret for a node
17
22
 
18
23
  {
24
+ action: <Trade Action String>
19
25
  description: <Trade Description String>
20
26
  expires_at: <Trade Expires At ISO 8601 Date String>
21
27
  is_hold: <Create Hold Invoice Bool>
22
28
  lnd: <Authenticated LND API Object>
29
+ logger: <Winston Logger Object>
23
30
  secret: <Clear Secret String>
24
31
  to: <To Public Key Hex Encoded String>
25
32
  tokens: <Price Tokens Number>
@@ -64,6 +71,34 @@ module.exports = (args, cbk) => {
64
71
  return cbk();
65
72
  },
66
73
 
74
+ // Calculate dynamic sale price for channel sales
75
+ salePrice: ['validate', ({}, cbk) => {
76
+ // Exit early when this is not a channel sale
77
+ if (args.action !== sellAction) {
78
+ return cbk(null, {});
79
+ }
80
+
81
+ // Get a chain fee rate to dynamically price vs the chain cost
82
+ return getChainFeeRate({
83
+ confirmation_target: slowConf,
84
+ lnd: args.lnd,
85
+ },
86
+ (err, res) => {
87
+ if (!!err) {
88
+ return cbk(err);
89
+ }
90
+
91
+ const chainSurcharge = ceil(res.tokens_per_vbyte * openSizeVbytes);
92
+
93
+ const price = chainSurcharge + args.tokens;
94
+
95
+ args.logger.info({total_channel_sale_cost: price});
96
+
97
+ return cbk(null, {price});
98
+ },
99
+ cbk);
100
+ }],
101
+
67
102
  // Encrypt the secret data
68
103
  encrypt: ['validate', ({}, cbk) => {
69
104
  return encryptTradeSecret({
@@ -75,7 +110,7 @@ module.exports = (args, cbk) => {
75
110
  }],
76
111
 
77
112
  // Create the invoice to purchase the unlocking secret
78
- createInvoice: ['encrypt', ({encrypt}, cbk) => {
113
+ createInvoice: ['encrypt', 'salePrice', ({encrypt, salePrice}, cbk) => {
79
114
  // Exit early when this is a hold invoice
80
115
  if (!!args.is_hold) {
81
116
  return createHodlInvoice({
@@ -84,7 +119,7 @@ module.exports = (args, cbk) => {
84
119
  id: bufferAsHex(sha256(hexAsBuffer(encrypt.payment_secret))),
85
120
  lnd: args.lnd,
86
121
  secret: encrypt.payment_secret,
87
- tokens: args.tokens,
122
+ tokens: salePrice.price || args.tokens,
88
123
  },
89
124
  cbk);
90
125
  }
@@ -15,9 +15,12 @@ const decodeBasicTrade = require('./decode_basic_trade');
15
15
  const {makePeerRequest} = require('./../p2p');
16
16
  const requestTradeById = require('./request_trade_by_id');
17
17
  const {servicePeerRequests} = require('./../p2p');
18
+ const {serviceTypeReceiveChannelSale} = require('./../service_types');
19
+ const {serviceTypeRequestChannelSale} = require('./../service_types');
18
20
  const {serviceTypeReceiveTrades} = require('./../service_types');
19
21
  const {serviceTypeRequestTrades} = require('./../service_types');
20
22
 
23
+ const buyChannelAction = 'buy';
21
24
  const findBasicRecord = records => records.find(n => n.type === '1');
22
25
  const findIdRecord = records => records.find(n => n.type === '0');
23
26
  const {isArray} = Array;
@@ -55,11 +58,15 @@ const waitForTradesMs = 1000 * 5;
55
58
  trade: <Encoded Trade String>
56
59
  }
57
60
  */
58
- module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
61
+ module.exports = ({action, ask, id, identity, lnd, logger, nodes}, cbk) => {
59
62
  return new Promise((resolve, reject) => {
60
63
  return asyncAuto({
61
64
  // Check arguments
62
65
  validate: cbk => {
66
+ if (!action) {
67
+ return cbk([400, 'ExpectedActionTypeToFindTrade']);
68
+ }
69
+
63
70
  if (!ask) {
64
71
  return cbk([400, 'ExpectedAskMethodToFindTrade']);
65
72
  }
@@ -160,6 +167,26 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
160
167
  // Get the list of connected peers
161
168
  getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
162
169
 
170
+ // Determine the receive type to listen for
171
+ receiveType: ['validate', ({}, cbk) => {
172
+ // Exit early when the action is to buy a channel
173
+ if (action === buyChannelAction) {
174
+ return cbk(null, serviceTypeReceiveChannelSale);
175
+ }
176
+
177
+ return cbk(null, serviceTypeReceiveTrades);
178
+ }],
179
+
180
+ // Determine the request type to send
181
+ requestType: ['validate', ({}, cbk) => {
182
+ // Exit early when the action is to buy a channel
183
+ if (action === buyChannelAction) {
184
+ return cbk(null, serviceTypeRequestChannelSale);
185
+ }
186
+
187
+ return cbk(null, serviceTypeRequestTrades);
188
+ }],
189
+
163
190
  // Try and connect to a node in order to do p2p messaging
164
191
  connect: ['getNodes', 'getPeers', ({getNodes, getPeers}, cbk) => {
165
192
  const connected = getPeers.peers.map(n => n.public_key);
@@ -213,11 +240,17 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
213
240
  return cbk();
214
241
  }
215
242
 
216
- return requestTradeById({id, lnd, to}, cbk);
243
+ return requestTradeById({action, id, lnd, to}, cbk);
217
244
  })],
218
245
 
219
246
  // Request an inventory of trades
220
- requestTrades: ['requestTrade', 'to', ({requestTrade, to}, cbk) => {
247
+ requestTrades: [
248
+ 'receiveType',
249
+ 'requestTrade',
250
+ 'requestType',
251
+ 'to',
252
+ ({receiveType, requestTrade, requestType, to}, cbk) =>
253
+ {
221
254
  // Exit early when there was a successful request for a specific id
222
255
  if (!!requestTrade.value) {
223
256
  return cbk();
@@ -234,8 +267,7 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
234
267
  return !!err ? cbk(err) : cbk(null, {trades});
235
268
  };
236
269
 
237
- // Listen for trades
238
- service.request({type: serviceTypeReceiveTrades}, (req, res) => {
270
+ service.request({type: receiveType}, (req, res) => {
239
271
  const requestIdRecord = findIdRecord(req.records);
240
272
 
241
273
  // Exit early when this is a request for something else
@@ -268,7 +300,7 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
268
300
  to,
269
301
  records: [{type: tradesRequestIdType, value: requestId}],
270
302
  timeout: requestTradesTimeoutMs,
271
- type: serviceTypeRequestTrades,
303
+ type: requestType,
272
304
  },
273
305
  (err, res) => {
274
306
  if (!!err) {
@@ -323,7 +355,7 @@ module.exports = ({ask, id, identity, lnd, logger, nodes}, cbk) => {
323
355
  return cbk();
324
356
  }
325
357
 
326
- return requestTradeById({lnd, to, id: selectTrade.id}, cbk);
358
+ return requestTradeById({action, lnd, to, id: selectTrade.id}, cbk);
327
359
  }],
328
360
 
329
361
  // Final trade details
@@ -21,6 +21,7 @@ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
21
21
  /** Manage an individual trade
22
22
 
23
23
  {
24
+ action: <Action Type String>
24
25
  ask: <Ask Function>
25
26
  lnd: <Authenticated LND API Object>
26
27
  logger: <Winston Logger Object>
@@ -28,11 +29,15 @@ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
28
29
 
29
30
  @returns via cbk or Promise
30
31
  */
31
- module.exports = ({ask, lnd, logger}, cbk) => {
32
+ module.exports = ({action, ask, lnd, logger}, cbk) => {
32
33
  return new Promise((resolve, reject) => {
33
34
  return asyncAuto({
34
35
  // Check arguments
35
36
  validate: cbk => {
37
+ if (!action) {
38
+ return cbk([400, 'ExpectedActionTypeToManageTrade']);
39
+ }
40
+
36
41
  if (!ask) {
37
42
  return cbk([400, 'ExpectedAskFunctionToManageTrade']);
38
43
  }
@@ -90,6 +95,7 @@ module.exports = ({ask, lnd, logger}, cbk) => {
90
95
 
91
96
  // Get the trade details interactively
92
97
  return findTrade({
98
+ action,
93
99
  ask,
94
100
  lnd,
95
101
  logger,
@@ -2,12 +2,14 @@ const asyncAuto = require('async/auto');
2
2
  const asyncUntil = require('async/until');
3
3
  const {cancelHodlInvoice} = require('ln-service');
4
4
  const {diffieHellmanComputeSecret} = require('ln-service');
5
+ const {getChainBalance} = require('ln-service');
5
6
  const {getIdentity} = require('ln-service');
6
7
  const {getInvoices} = require('ln-service');
7
8
  const {getNetwork} = require('ln-sync');
8
9
  const {getNodeAlias} = require('ln-sync');
9
10
  const {returnResult} = require('asyncjs-util');
10
11
 
12
+ const createChannelSale = require('./create_channel_sale');
11
13
  const createTrade = require('./create_trade');
12
14
  const encodeTrade = require('./encode_trade');
13
15
  const manageTrade = require('./manage_trade');
@@ -15,11 +17,13 @@ const serviceAnchoredTrades = require('./service_anchored_trades');
15
17
  const serviceOpenTrade = require('./service_open_trade');
16
18
  const tradeFromInvoice = require('./trade_from_invoice');
17
19
 
20
+ const buyAction = 'buy';
18
21
  const cancelAction = 'cancel-';
19
22
  const createAction = 'create';
20
23
  const decodeAction = 'decode';
21
24
  const defaultInvoicesLimit = 100;
22
25
  const listAction = 'list';
26
+ const sellAction = 'sell';
23
27
  const serveAction = 'serve';
24
28
  const serveTradeAction = 'serve-trade-';
25
29
  const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
@@ -29,6 +33,7 @@ const viewAction = 'view';
29
33
 
30
34
  {
31
35
  ask: <Ask Function>
36
+ experimental: <Experiment Mode Bool>
32
37
  lnd: <Authenticated LND API Object>
33
38
  logger: <Winston Logger Object>
34
39
  separator: <Create Separator Function>
@@ -36,7 +41,7 @@ const viewAction = 'view';
36
41
 
37
42
  @returns via cbk or Promise
38
43
  */
39
- module.exports = ({ask, lnd, logger, separator}, cbk) => {
44
+ module.exports = ({ask, experimental, lnd, logger, separator}, cbk) => {
40
45
  return new Promise((resolve, reject) => {
41
46
  return asyncAuto({
42
47
  // Check arguments
@@ -66,12 +71,25 @@ module.exports = ({ask, lnd, logger, separator}, cbk) => {
66
71
  select: ['validate', ({}, cbk) => {
67
72
  return ask({
68
73
  choices: [
74
+ separator(),
69
75
  {name: 'Create Trade', value: createAction},
70
76
  {name: 'Decode Trade', value: decodeAction},
71
77
  separator(),
78
+ {
79
+ name: 'Buy Channel (experimental)',
80
+ value: buyAction,
81
+ disabled: !experimental,
82
+ },
83
+ {
84
+ name: 'Sell Channel (experimental)',
85
+ value: sellAction,
86
+ disabled: !experimental,
87
+ },
88
+ separator(),
72
89
  {name: 'Open Trades', value: listAction},
73
90
  {name: 'Serve Trades', value: serveAction},
74
91
  ],
92
+ loop: false,
75
93
  message: 'Trade?',
76
94
  name: 'action',
77
95
  type: 'list',
@@ -113,11 +131,11 @@ module.exports = ({ask, lnd, logger, separator}, cbk) => {
113
131
  // View an existing trade
114
132
  view: ['checkSigner', 'select', ({checkSigner, select}, cbk) => {
115
133
  // Exit early when not decoding a trade
116
- if (select.action !== decodeAction) {
134
+ if (select.action !== decodeAction && select.action !== buyAction) {
117
135
  return cbk();
118
136
  }
119
137
 
120
- return manageTrade({ask, lnd, logger}, cbk)
138
+ return manageTrade({ask, lnd, logger, action: select.action}, cbk)
121
139
  }],
122
140
 
123
141
  // Get open trades
@@ -289,6 +307,29 @@ module.exports = ({ask, lnd, logger, separator}, cbk) => {
289
307
  return cbk(err);
290
308
  });
291
309
  }],
310
+
311
+ // Sell a channel
312
+ sellChannel: ['select', ({select}, cbk) => {
313
+ // Exit early when not selling a channel
314
+ if (select.action !== sellAction) {
315
+ return cbk();
316
+ }
317
+
318
+ return getChainBalance({lnd}, (err, res) => {
319
+ if (!!err) {
320
+ return cbk(err);
321
+ }
322
+
323
+ return createChannelSale({
324
+ ask,
325
+ lnd,
326
+ logger,
327
+ action: select.action,
328
+ balance: res.chain_balance,
329
+ },
330
+ cbk);
331
+ });
332
+ }],
292
333
  },
293
334
  returnResult({reject, resolve}, cbk));
294
335
  });
@@ -1,7 +1,9 @@
1
- const defaultNetwork = 'lnbc';
2
- const regtestNetwork = 'lnbcrt';
1
+ const {parsePaymentRequest} = require('ln-service');
2
+
3
+ const defaultNetwork = 'bitcoin';
4
+ const regtestNetwork = 'regtest';
3
5
  const regtestNetworkType = '02';
4
- const testnetNetwork = 'lntb';
6
+ const testnetNetwork = 'testnet';
5
7
  const testnetNetworkType = '01';
6
8
 
7
9
  /** Derive a network record name from a payment request
@@ -20,17 +22,24 @@ module.exports = ({request}) => {
20
22
  throw new Error('ExpectedPaymentRequestToDeriveNetworkRecord');
21
23
  }
22
24
 
23
- if (request.startsWith(defaultNetwork)) {
24
- return {};
25
- }
25
+ try {
26
+ const {network} = parsePaymentRequest({request});
26
27
 
27
- if (request.startsWith(regtestNetwork)) {
28
- return {value: regtestNetworkType};
29
- }
28
+ if (network === defaultNetwork) {
29
+ return {};
30
+ }
30
31
 
31
- if (request.startsWith(testnetNetwork)) {
32
- return {value: testnetNetworkType};
33
- }
32
+ if (network === regtestNetwork) {
33
+ return {value: regtestNetworkType};
34
+ }
34
35
 
35
- throw new Error('UnknownNetworkToDeriveNetworkRecordFor');
36
+ if (network === testnetNetwork) {
37
+ return {value: testnetNetworkType};
38
+ }
39
+
40
+ throw new Error('UnknownNetworkToDeriveNetworkRecordFor');
41
+
42
+ } catch (err) {
43
+ throw new Error('FailedToParsePaymentRequestToDeriveNetworkRecord');
44
+ }
36
45
  };
@@ -0,0 +1,60 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {getPeers} = require('ln-service');
3
+ const {getChainFeeRate} = require('ln-service');
4
+ const {openChannel} = require('ln-service');
5
+ const {returnResult} = require('asyncjs-util');
6
+
7
+ const slowConf = 144;
8
+
9
+ /** Opens channel on invoice payment
10
+ {
11
+ id: <Partner Public Key Hex String>
12
+ lnd: <Authenticated LND API Object>
13
+ tokens: <Capacity of Channel To Open Tokens Number>
14
+ }
15
+
16
+ @returns via cbk or Promise
17
+ {
18
+ transaction_id: <Funding Transaction Id String>
19
+ transaction_vout: <Funding Transaction Output Index Number>
20
+ }
21
+ */
22
+ module.exports = ({id, lnd, tokens}, cbk) => {
23
+ return new Promise((resolve, reject) => {
24
+ return asyncAuto({
25
+ // Check arguments
26
+ validate: cbk => {
27
+ if (!id) {
28
+ return cbk([400, 'ExpectedPartnerPublicKeyToOpenNewChannel']);
29
+ }
30
+
31
+ if (!lnd) {
32
+ return cbk([400, 'ExpectedLndObjectToOpenNewChannel']);
33
+ }
34
+
35
+ if (!tokens) {
36
+ return cbk([400, 'ExpectedCapacityToOpenNewChannel']);
37
+ }
38
+
39
+ return cbk();
40
+ },
41
+
42
+ // Get low fee rate
43
+ getSlowFee: ['validate', ({}, cbk) => {
44
+ return getChainFeeRate({lnd, confirmation_target: slowConf}, cbk);
45
+ }],
46
+
47
+ // Select a peer and open a channel
48
+ openChannel: ['getSlowFee', ({getSlowFee}, cbk) => {
49
+ return openChannel({
50
+ lnd,
51
+ chain_fee_rate: getSlowFee.tokens_per_vbyte,
52
+ local_tokens: tokens,
53
+ partner_public_key: id,
54
+ },
55
+ cbk);
56
+ }],
57
+ },
58
+ returnResult({reject, resolve, of: 'openChannel'}, cbk));
59
+ });
60
+ };
@@ -4,14 +4,17 @@ const {returnResult} = require('asyncjs-util');
4
4
  const decodeTrade = require('./decode_trade');
5
5
  const {makePeerRequest} = require('./../p2p');
6
6
 
7
+ const buyChannelAction = 'buy';
7
8
  const findTradeRecord = records => records.find(n => n.type === '1');
8
9
  const requestTradeTimeoutMs = 1000 * 30;
10
+ const {serviceTypeRequestChannelSale} = require('./../service_types');
9
11
  const {serviceTypeRequestTrades} = require('./../service_types');
10
12
  const tradeIdRecordType = '0';
11
13
 
12
14
  /** Request a specific trade by its id
13
15
 
14
16
  {
17
+ action: <Purchase Action String>
15
18
  id: <Trade Identifier Hex String>
16
19
  lnd: <Authenticated LND API Object>
17
20
  to: <Make Request To Public Key Hex Encoded String>
@@ -25,11 +28,15 @@ const tradeIdRecordType = '0';
25
28
  trade: <Encoded Trade Record String>
26
29
  }
27
30
  */
28
- module.exports = ({id, lnd, to}, cbk) => {
31
+ module.exports = ({action, id, lnd, to}, cbk) => {
29
32
  return new Promise((resolve, reject) => {
30
33
  return asyncAuto({
31
34
  // Check arguments
32
35
  validate: cbk => {
36
+ if (!action) {
37
+ return cbk([400, 'ExpectedActionTypeToRequestTradeById']);
38
+ }
39
+
33
40
  if (!id) {
34
41
  return cbk([400, 'ExpectedTradeIdentifierToRequestTradeById']);
35
42
  }
@@ -45,14 +52,23 @@ module.exports = ({id, lnd, to}, cbk) => {
45
52
  return cbk();
46
53
  },
47
54
 
55
+ // Derive request type
56
+ requestType: ['validate', ({}, cbk) => {
57
+ if (action === buyChannelAction) {
58
+ return cbk(null, serviceTypeRequestChannelSale);
59
+ }
60
+
61
+ return cbk(null, serviceTypeRequestTrades);
62
+ }],
63
+
48
64
  // Get trade
49
- getTrade: ['validate', ({}, cbk) => {
65
+ getTrade: ['requestType', ({requestType}, cbk) => {
50
66
  return makePeerRequest({
51
67
  lnd,
52
68
  to,
53
69
  records: [{type: tradeIdRecordType, value: id}],
54
70
  timeout: requestTradeTimeoutMs,
55
- type: serviceTypeRequestTrades,
71
+ type: requestType,
56
72
  },
57
73
  (err, res) => {
58
74
  if (!!err) {
@@ -14,6 +14,8 @@ const uriAsSocket = n => n.substring(67);
14
14
  /** Service an individual trade
15
15
 
16
16
  {
17
+ [action]: <Trade Action String>
18
+ [capacity]: <Channel Open Capacity Tokens Number>
17
19
  channels: [{
18
20
  id: <Standard Format Channel Id String>
19
21
  partner_public_key: <Node Public Key Hex String>
@@ -102,10 +104,13 @@ module.exports = (args, cbk) => {
102
104
  args.logger.info({trade_expiry: new Date(args.expires_at)});
103
105
 
104
106
  const sub = serviceTradeRequests({
107
+ action: args.action,
108
+ capacity: args.capacity,
105
109
  description: args.description,
106
110
  expires_at: args.expires_at,
107
111
  id: args.id,
108
112
  lnd: args.lnd,
113
+ logger: args.logger,
109
114
  secret: args.secret,
110
115
  tokens: asNumber(args.tokens),
111
116
  });
@@ -1,6 +1,7 @@
1
1
  const EventEmitter = require('events');
2
2
  const {randomBytes} = require('crypto');
3
3
 
4
+ const {acceptsChannelOpen} = require('ln-sync');
4
5
  const {getInvoice} = require('ln-service');
5
6
  const {subscribeToInvoice} = require('ln-service');
6
7
 
@@ -8,6 +9,8 @@ const acceptTrade = require('./accept_trade');
8
9
  const finalizeTradeSecret = require('./finalize_trade_secret');
9
10
  const {makePeerRequest} = require('./../p2p');
10
11
  const {servicePeerRequests} = require('./../p2p');
12
+ const {serviceTypeReceiveChannelSale} = require('./../service_types');
13
+ const {serviceTypeRequestChannelSale} = require('./../service_types');
11
14
  const {serviceTypeRequestTrades} = require('./../service_types');
12
15
  const {serviceTypeReceiveTrades} = require('./../service_types');
13
16
 
@@ -20,6 +23,7 @@ const idBackType = '0';
20
23
  const idRecord = id => ({type: '1', value: id});
21
24
  const msRemainingToTime = time => time - Date.now();
22
25
  const postOpenTradeTimeoutMs = 1000 * 30;
26
+ const sellAction = 'sell';
23
27
  const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
24
28
  const tradeSecretType = '1';
25
29
  const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
@@ -29,10 +33,12 @@ const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
29
33
  The maximum expiration date is three weeks
30
34
 
31
35
  {
36
+ action: <Trade Action String>
32
37
  description: <Trade Description String>
33
38
  expires_at: <Trade Expires At ISO 8601 Date String>
34
39
  id: <Trade Id Hex String>
35
40
  lnd: <Authenticated LND API Object>
41
+ logger: <Winston Logger Object>
36
42
  secret: <Secret Payload String>
37
43
  tokens: <Tokens Number>
38
44
  }
@@ -126,8 +132,12 @@ module.exports = args => {
126
132
  descRecord(utf8AsHex(args.description)),
127
133
  ];
128
134
 
135
+ const requestType = !!args.action ?
136
+ serviceTypeRequestChannelSale :
137
+ serviceTypeRequestTrades;
138
+
129
139
  // Wait for a request for the open trade
130
- service.request({type: serviceTypeRequestTrades}, (req, res) => {
140
+ service.request({type: requestType}, (req, res) => {
131
141
  const {failure, success} = res;
132
142
  const requestTradeId = findIdRecord(req.records);
133
143
 
@@ -163,13 +173,17 @@ module.exports = args => {
163
173
  return;
164
174
  }
165
175
 
176
+ const receiveType = args.action === sellAction ?
177
+ serviceTypeReceiveChannelSale :
178
+ serviceTypeReceiveTrades;
179
+
166
180
  // Ping the node with trade details
167
181
  return makePeerRequest({
168
182
  records,
169
183
  lnd: args.lnd,
170
184
  timeout: postOpenTradeTimeoutMs,
171
185
  to: req.from,
172
- type: serviceTypeReceiveTrades,
186
+ type: receiveType,
173
187
  },
174
188
  err => {
175
189
  if (!!err) {
@@ -191,10 +205,12 @@ module.exports = args => {
191
205
 
192
206
  // Create a trade secret for the requesting peer and return that
193
207
  return finalizeTradeSecret({
208
+ action: args.action,
194
209
  description: args.description,
195
210
  expires_at: args.expires_at,
196
211
  is_hold: true,
197
212
  lnd: args.lnd,
213
+ logger: args.logger,
198
214
  secret: args.secret,
199
215
  to: req.from,
200
216
  tokens: args.tokens,
@@ -230,9 +246,13 @@ module.exports = args => {
230
246
 
231
247
  // Settle the held invoice with the preimage
232
248
  return acceptTrade({
249
+ action: args.action,
250
+ capacity: args.capacity,
233
251
  cancel: holds.filter(n => n.id !== updated.id).map(n => n.id),
234
252
  id: args.id,
235
253
  lnd: args.lnd,
254
+ logger: args.logger,
255
+ partner_public_key: req.from,
236
256
  secret: res.secret,
237
257
  },
238
258
  err => {
@@ -246,6 +266,23 @@ module.exports = args => {
246
266
  });
247
267
  });
248
268
 
269
+ // Exit early on channel sale, confirming acceptance
270
+ if (args.action === sellAction) {
271
+ return acceptsChannelOpen({
272
+ capacity: args.capacity,
273
+ lnd: args.lnd,
274
+ partner_public_key: req.from,
275
+ },
276
+ err => {
277
+ if (!!err) {
278
+ return failure([503, 'FailedToAcceptChannelOpen', {err}]);
279
+ }
280
+
281
+ // Return details about the trade
282
+ return success({records: [record]});
283
+ });
284
+ }
285
+
249
286
  // Return details about the trade
250
287
  return success({records: [record]});
251
288
  });