paid-services 3.15.4 → 3.16.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,9 @@
1
1
  # Versions
2
2
 
3
+ ## Version 3.16.2
4
+
5
+ - `manageSwap`: Add external funding, conf target setting, keysend pushes
6
+
3
7
  ## Version 3.15.4
4
8
 
5
9
  - `manageSwap`: Add method to execute submarine swaps on testnet
package/package.json CHANGED
@@ -8,24 +8,24 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@alexbosworth/fiat": "1.0.2",
11
- "async": "3.2.3",
11
+ "async": "3.2.4",
12
12
  "asyncjs-util": "1.2.9",
13
13
  "bech32": "2.0.0",
14
- "bolt01": "1.2.4",
15
- "bolt07": "1.8.1",
14
+ "bolt01": "1.2.5",
15
+ "bolt07": "1.8.2",
16
16
  "ecpair": "2.0.1",
17
- "goldengate": "11.2.1",
18
- "invoices": "2.0.6",
19
- "ln-service": "53.15.0",
20
- "ln-sync": "3.12.0",
21
- "psbt": "2.0.1",
17
+ "goldengate": "11.2.2",
18
+ "invoices": "2.0.7",
19
+ "ln-service": "53.17.3",
20
+ "ln-sync": "3.12.1",
21
+ "psbt": "2.6.0",
22
22
  "p2tr": "1.3.1",
23
23
  "tiny-secp256k1": "2.2.1"
24
24
  },
25
25
  "description": "Lightning Paid Services library",
26
26
  "devDependencies": {
27
27
  "@alexbosworth/tap": "15.0.11",
28
- "ln-docker-daemons": "2.2.9",
28
+ "ln-docker-daemons": "2.3.0",
29
29
  "mock-lnd": "1.4.1",
30
30
  "secp256k1": "4.0.3"
31
31
  },
@@ -47,5 +47,5 @@
47
47
  "integration-tests": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 180 test/integration/*.js",
48
48
  "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"
49
49
  },
50
- "version": "3.15.4"
50
+ "version": "3.16.2"
51
51
  }
@@ -6,6 +6,7 @@
6
6
  "serviceTypeReceiveChannelSale": "8050008",
7
7
  "serviceTypeRequestChannelSale": "8050007",
8
8
  "serviceTypeSignCapacityChange": "8050004",
9
+ "serviceTypeSwapResponse": "8050009",
9
10
  "serviceTypeWaitOnConfirmation": "8050003",
10
11
  "types": {
11
12
  "8050001": "serviceTypeChangeCapacity",
@@ -15,6 +16,7 @@
15
16
  "8050005": "serviceTypeRequestTrades",
16
17
  "8050006": "serviceTypeReceiveTrades",
17
18
  "8050007": "serviceTypeRequestChannelSale",
18
- "8050008": "serviceTypeReceiveChannelSale"
19
+ "8050008": "serviceTypeReceiveChannelSale",
20
+ "8050009": "serviceTypeSwapResponse"
19
21
  }
20
22
  }
@@ -81,6 +81,7 @@ const v1AddressWords = key => [].concat(1).concat(bech32m.toWords(key));
81
81
  {
82
82
  emitter: <Event Emitter Object>
83
83
  [is_avoiding_broadcast]: <Avoid Sweep Broadcast Bool>
84
+ [is_external_funding]: <Externally Fund Swap Bool>
84
85
  [is_uncooperative]: <Avoid Cooperative Signing Bool>
85
86
  lnd: <Autenticated LND API Object>
86
87
  max_fee_deposit: <Max Routing Fee Tokens For Deposit Number>
@@ -236,6 +237,12 @@ module.exports = (args, cbk) => {
236
237
  payToFund: ['responseDetails', asyncReflect(({responseDetails}, cbk) => {
237
238
  const request = responseDetails.request;
238
239
 
240
+ if (!!args.is_external_funding) {
241
+ args.emitter.emit('update', {pay_to_fund_swap_offchain: request});
242
+
243
+ return cbk();
244
+ }
245
+
239
246
  args.emitter.emit('update', {funding_swap_offchain: request});
240
247
 
241
248
  return pay({
@@ -906,9 +913,9 @@ module.exports = (args, cbk) => {
906
913
 
907
914
  return cbk(null, {
908
915
  paid_execution_fee: deposit.tokens,
909
- paid_execution_routing: deposit.fee,
916
+ paid_execution_routing: deposit.fee || undefined,
910
917
  swap_payment_sent: funding.tokens,
911
- swap_payment_routing_fee: funding.fee,
918
+ swap_payment_routing_fee: funding.fee || undefined,
912
919
  });
913
920
  }],
914
921
  },
@@ -68,9 +68,9 @@ const witnessLengthTimeoutSweep = 3;
68
68
  /** Complete the on-chain side of the swap by taking the off-chain funds
69
69
 
70
70
  {
71
+ [confirmation_target]: <Funding Confirmation Target Number>
71
72
  emitter: <Event Emitter Object>
72
73
  [is_ignoring_deposit]: <Ignore Held Deposit Funds Bool>
73
- [fund_fee_rate]: <Fund Chain Fee Rate Number>
74
74
  lnd: <Authenticated LND API Object>
75
75
  recovery: <Swap Recovery Hex String>
76
76
  [request]: <Request Function>
@@ -486,8 +486,8 @@ module.exports = (args, cbk) => {
486
486
 
487
487
  return fundPsbt({
488
488
  psbt,
489
- fee_tokens_per_vbyte: args.fund_fee_rate,
490
489
  lnd: args.lnd,
490
+ target_confirmations: args.confirmation_target,
491
491
  },
492
492
  cbk);
493
493
  }],
@@ -1,18 +1,32 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncMap = require('async/map');
3
+ const {findKey} = require('ln-sync');
2
4
  const {getMasterPublicKeys} = require('ln-service');
5
+ const {getAllInvoices} = require('ln-sync');
6
+ const {getInvoice} = require('ln-service');
3
7
  const {getNetwork} = require('ln-sync');
8
+ const {getNodeAlias} = require('ln-sync');
4
9
  const {returnResult} = require('asyncjs-util');
5
10
 
11
+ const decodeOffToOnRequest = require('./decode_off_to_on_request');
12
+ const {decodeTrade} = require('./../trades');
6
13
  const recoverResponseToSwapOut = require('./recover_response_to_swap_out');
7
14
  const requestSwapOut = require('./request_swap_out');
8
15
  const respondToSwapOutRequest = require('./respond_to_swap_out_request');
9
16
 
17
+ const actionPushRequest = 'push-request';
10
18
  const allowedNetwork = 'btctestnet';
11
19
  const bip86Path = `m/86'/0'/0'`;
20
+ const flatten = arr => [].concat(...arr);
12
21
  const minConfirmations = {btc: 2, btcregtest: 3, btctestnet: 1};
22
+ const niceId = id => id.substring(0, 8);
23
+ const niceName = node => `${node.alias} ${node.id.substring(0, 8)}`.trim();
24
+ const pushesReceivedAfter = () => new Date(Date.now() - 1000 * 60 * 60 * 24);
13
25
  const recoverRespondAction = 'recover-response';
14
26
  const requestAction = 'request';
15
27
  const respondAction = 'respond';
28
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
29
+ const typeKeySendTrade = '805805';
16
30
 
17
31
  /** Manage a swap
18
32
 
@@ -25,37 +39,118 @@ const respondAction = 'respond';
25
39
 
26
40
  @returns via cbk or Promise
27
41
  */
28
- module.exports = ({ask, lnd, logger, request}, cbk) => {
42
+ module.exports = (args, cbk) => {
29
43
  return new Promise((resolve, reject) => {
30
44
  return asyncAuto({
31
45
  // Check arguments
32
46
  validate: cbk => {
33
- if (!ask) {
47
+ if (!args.ask) {
34
48
  return cbk([400, 'ExpectedAskFunctionToManageSwap']);
35
49
  }
36
50
 
37
- if (!lnd) {
51
+ if (!args.lnd) {
38
52
  return cbk([400, 'ExpectedAuthenticatedLndToManageSwap']);
39
53
  }
40
54
 
41
- if (!logger) {
55
+ if (!args.logger) {
42
56
  return cbk([400, 'ExpectedWinstonLoggerToManageSwap']);
43
57
  }
44
58
 
45
- if (!request) {
59
+ if (!args.request) {
46
60
  return cbk([400, 'ExpectedRequestFunctionToManageSwap']);
47
61
  }
48
62
 
49
63
  return cbk();
50
64
  },
51
65
 
66
+ // Get invoices to find past swap requests
67
+ getInvoices: ['validate', ({}, cbk) => {
68
+ return getAllInvoices({
69
+ created_after: pushesReceivedAfter().toISOString(),
70
+ lnd: args.lnd,
71
+ },
72
+ cbk);
73
+ }],
74
+
52
75
  // Look for a p2tr master public key to detect p2tr support
53
76
  getMasterPublicKeys: ['validate', ({}, cbk) => {
54
- return getMasterPublicKeys({lnd}, cbk);
77
+ return getMasterPublicKeys({lnd: args.lnd}, cbk);
55
78
  }],
56
79
 
57
80
  // Get the network to make sure this is on testnet
58
- getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
81
+ getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
82
+
83
+ // Received swap pushes
84
+ swapPushes: ['getInvoices', ({getInvoices}, cbk) => {
85
+ const pushes = getInvoices.invoices.map(invoice => {
86
+ // A swap push is a KeySend payment
87
+ if (!invoice.is_confirmed || !invoice.is_push) {
88
+ return;
89
+ }
90
+
91
+ const messages = flatten(invoice.payments.map(n => n.messages));
92
+
93
+ // Convert messages to swaps
94
+ const swaps = messages.map(message => {
95
+ // Swaps have the trade type wrapper
96
+ if (message.type !== typeKeySendTrade) {
97
+ return;
98
+ }
99
+
100
+ // The trade type should be a swap
101
+ try {
102
+ const {swap} = decodeTrade({trade: message.value});
103
+
104
+ if (!swap) {
105
+ return;
106
+ }
107
+
108
+ const details = decodeOffToOnRequest({request: swap.request});
109
+
110
+ // Return relevant swap details for acceptance
111
+ return {
112
+ confirmed_at: invoice.confirmed_at,
113
+ id: details.hash,
114
+ tokens: details.tokens,
115
+ node: swap.node,
116
+ trade: message.value,
117
+ };
118
+ } catch (err) {
119
+ return;
120
+ }
121
+ });
122
+
123
+ const [swap] = swaps.filter(n => !!n);
124
+
125
+ return swap;
126
+ });
127
+
128
+ return cbk(null, pushes.filter(n => !!n));
129
+ }],
130
+
131
+ // Get node aliases for pushes
132
+ getAliases: ['swapPushes', ({swapPushes}, cbk) => {
133
+ return asyncMap(swapPushes, ({node}, cbk) => {
134
+ return getNodeAlias({id: node, lnd: args.lnd}, cbk);
135
+ },
136
+ cbk);
137
+ }],
138
+
139
+ // Get existing swaps
140
+ getExisting: ['swapPushes', ({swapPushes}, cbk) => {
141
+ return asyncMap(swapPushes, ({id}, cbk) => {
142
+ return getInvoice({id, lnd: args.lnd}, (err, res) => {
143
+ // Error indicates that the invoice could not be looked up
144
+ if (!!err) {
145
+ return cbk();
146
+ }
147
+
148
+ // Result indicates the invoice is known so the swap is in progress
149
+ return cbk(null, id);
150
+ });
151
+ },
152
+ cbk);
153
+ }],
59
154
 
60
155
  // Determine if taproot is supported or not
61
156
  hasTr: ['getMasterPublicKeys', ({getMasterPublicKeys}, cbk) => {
@@ -65,27 +160,53 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
65
160
  }],
66
161
 
67
162
  // Select a swap action
68
- selectAction: ['getNetwork', 'hasTr', ({getNetwork, hasTr}, cbk) => {
163
+ selectAction: [
164
+ 'getAliases',
165
+ 'getExisting',
166
+ 'getNetwork',
167
+ 'hasTr',
168
+ 'swapPushes',
169
+ ({getAliases, getExisting, getNetwork, hasTr, swapPushes}, cbk) =>
170
+ {
69
171
  // Check to make sure the network is supported
70
172
  if (!hasTr && getNetwork.network !== allowedNetwork) {
71
173
  return cbk([501, 'CurrentlyUnsupportedChainForSwap']);
72
174
  }
73
175
 
74
- return ask({
75
- choices: [
76
- {
77
- name: 'Make a new swap request',
78
- value: requestAction,
79
- },
80
- {
81
- name: 'Respond to a swap request',
82
- value: respondAction,
83
- },
84
- {
85
- name: 'Recovery for swap response',
86
- value: recoverRespondAction,
87
- },
88
- ],
176
+ const pushes = swapPushes
177
+ .filter(push => !getExisting.includes(push.id))
178
+ .map(push => {
179
+ const amount = tokensAsBigUnit(push.tokens);
180
+ const id = niceId(push.id);
181
+ const node = niceName(getAliases.find(n => n.id === push.node));
182
+
183
+ return {
184
+ name: `Accept request ${id} to swap ${amount} with ${node}`,
185
+ value: push.trade,
186
+ };
187
+ });
188
+
189
+ const choices = [
190
+ {
191
+ name: 'Make a new swap request',
192
+ value: requestAction,
193
+ },
194
+ {
195
+ name: 'Send a new swap request via KeySend',
196
+ value: actionPushRequest,
197
+ },
198
+ {
199
+ name: 'Respond to a swap request',
200
+ value: respondAction,
201
+ },
202
+ {
203
+ name: 'Recovery for swap response',
204
+ value: recoverRespondAction,
205
+ },
206
+ ];
207
+
208
+ return args.ask({
209
+ choices: choices.concat(pushes),
89
210
  message: 'Off-chain to on-chain swap:',
90
211
  name: 'action',
91
212
  type: 'list',
@@ -93,33 +214,125 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
93
214
  ({action}) => cbk(null, action));
94
215
  }],
95
216
 
217
+ // Ask for the remote to send to
218
+ askForRemote: ['selectAction', ({selectAction}, cbk) => {
219
+ // Exit early when the request is not KeySend
220
+ if (selectAction !== actionPushRequest) {
221
+ return cbk();
222
+ }
223
+
224
+ return args.ask({
225
+ message: 'Node key or channel peer alias to send request to?',
226
+ name: 'remote',
227
+ validate: input => !!input,
228
+ },
229
+ ({remote}) => cbk(null, remote));
230
+ }],
231
+
232
+ // Find the remote identity key when specified
233
+ findKey: ['askForRemote', ({askForRemote}, cbk) => {
234
+ if (!askForRemote) {
235
+ return cbk(null, {});
236
+ }
237
+
238
+ return findKey({lnd: args.lnd, query: askForRemote}, cbk);
239
+ }],
240
+
241
+ // Ask if the offchain funding should be external
242
+ askForExternal: [
243
+ 'askForRemote',
244
+ 'selectAction',
245
+ ({selectAction}, cbk) =>
246
+ {
247
+ switch (selectAction) {
248
+ case actionPushRequest:
249
+ case requestAction:
250
+ return args.ask({
251
+ message: 'Use internal funds to pay offchain payment request?',
252
+ name: 'internal',
253
+ type: 'confirm',
254
+ },
255
+ ({internal}) => cbk(null, !internal));
256
+
257
+ default:
258
+ // External funding not supported
259
+ return cbk();
260
+ }
261
+ }],
262
+
96
263
  // Make swap request
97
- makeRequest: ['hasTr', 'selectAction', ({hasTr, selectAction}, cbk) => {
98
- if (selectAction !== requestAction) {
264
+ makeRequest: [
265
+ 'askForExternal',
266
+ 'askForRemote',
267
+ 'findKey',
268
+ 'hasTr',
269
+ 'selectAction',
270
+ ({askForExternal, askForRemote, findKey, hasTr, selectAction}, cbk) =>
271
+ {
272
+ switch (selectAction) {
273
+ case actionPushRequest:
274
+ if (!findKey.public_key) {
275
+ return cbk([400, 'UnknownNodeToPushSwapRequestTo']);
276
+ }
277
+ break;
278
+
279
+ case requestAction:
280
+ break;
281
+
282
+ default:
99
283
  return cbk();
100
284
  }
101
285
 
102
286
  return requestSwapOut({
103
- ask,
104
- lnd,
105
- logger,
287
+ ask: args.ask,
288
+ is_external_funding: askForExternal,
289
+ lnd: args.lnd,
290
+ logger: args.logger,
291
+ push_to: findKey.public_key,
106
292
  min_confirmations: minConfirmations[getNetwork.network],
107
- request: !hasTr ? request : undefined,
293
+ request: !hasTr ? args.request : undefined,
108
294
  },
109
295
  cbk);
110
296
  }],
111
297
 
112
- // Respond to swap request
298
+ // Make a response for a swap request
113
299
  makeResponse: ['hasTr', 'selectAction', ({hasTr, selectAction}, cbk) => {
114
300
  if (selectAction !== respondAction) {
115
301
  return cbk();
116
302
  }
117
303
 
118
304
  return respondToSwapOutRequest({
119
- ask,
120
- lnd,
121
- logger,
122
- request: !hasTr ? request : undefined,
305
+ ask: args.ask,
306
+ is_external_funding: true,
307
+ lnd: args.lnd,
308
+ logger: args.logger,
309
+ request: !hasTr ? args.request : undefined,
310
+ },
311
+ cbk);
312
+ }],
313
+
314
+ // Respond to a pushed swap request
315
+ respondToPush: [
316
+ 'hasTr',
317
+ 'selectAction',
318
+ ({hasTr, selectAction}, cbk) =>
319
+ {
320
+ // Exit early when not responding to a push
321
+ try {
322
+ decodeTrade({trade: selectAction});
323
+ } catch (err) {
324
+ return cbk();
325
+ }
326
+
327
+ const {swap} = decodeTrade({trade: selectAction});
328
+
329
+ return respondToSwapOutRequest({
330
+ ask: args.ask,
331
+ lnd: args.lnd,
332
+ logger: args.logger,
333
+ request: !hasTr ? args.request : undefined,
334
+ swap: swap.request,
335
+ to: swap.node,
123
336
  },
124
337
  cbk);
125
338
  }],
@@ -135,10 +348,10 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
135
348
  }
136
349
 
137
350
  return recoverResponseToSwapOut({
138
- ask,
139
- lnd,
140
- logger,
141
- request: !hasTr ? request : undefined,
351
+ ask: args.ask,
352
+ lnd: args.lnd,
353
+ logger: args.logger,
354
+ request: !hasTr ? args.request : undefined,
142
355
  },
143
356
  cbk);
144
357
  }],
@@ -148,9 +361,26 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
148
361
  'makeRequest',
149
362
  'makeResponse',
150
363
  'recoverResponse',
151
- ({makeRequest, makeResponse, recoverResponse}, cbk) =>
364
+ 'respondToPush',
365
+ ({makeRequest, makeResponse, recoverResponse, respondToPush}, cbk) =>
152
366
  {
153
- return cbk(null, makeRequest || makeResponse || recoverResponse);
367
+ if (!!makeRequest) {
368
+ return cbk(null, makeRequest);
369
+ }
370
+
371
+ if (!!makeResponse) {
372
+ return cbk(null, makeResponse);
373
+ }
374
+
375
+ if (!!recoverResponse) {
376
+ return cbk(null, recoverResponse);
377
+ }
378
+
379
+ if (!!respondToPush) {
380
+ return cbk(null, respondToPush);
381
+ }
382
+
383
+ return cbk();
154
384
  }],
155
385
  },
156
386
  returnResult({reject, resolve, of: 'result'}, cbk));
@@ -1,32 +1,55 @@
1
+ const {createHash} = require('crypto');
1
2
  const EventEmitter = require('events');
3
+ const {randomBytes} = require('crypto');
2
4
 
3
5
  const asyncAuto = require('async/auto');
6
+ const asyncReflect = require('async/reflect');
7
+ const {connectPeer} = require('ln-sync');
8
+ const {getIdentity} = require('ln-service');
4
9
  const {getNetwork} = require('ln-sync');
5
10
  const {parsePaymentRequest} = require('ln-service');
11
+ const {payViaPaymentDetails} = require('ln-service');
6
12
  const {returnResult} = require('asyncjs-util');
7
13
 
8
14
  const completeOffToOnSwap = require('./complete_off_to_on_swap');
9
15
  const decodeOffToOnResponse = require('./decode_off_to_on_response');
16
+ const {encodeTrade} = require('./../trades');
10
17
  const makeRequest = require('./start_off_to_on_swap');
18
+ const {pushTypes} = require('./swap_field_types');
19
+ const {servicePeerRequests} = require('./../p2p');
20
+ const serviceTypes = require('./../service_types');
11
21
 
22
+ const bufferAsHex = buffer => buffer.toString('hex');
12
23
  const defaultFeeRate = 5000;
13
24
  const defaultMaxFeeForDeposit = 1337;
14
25
  const defaultSwapAmount = 2500000;
26
+ const findRecord = (r, type) => (r.find(n => n.type === type) || {}).value;
27
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
15
28
  const isNumber = n => !isNaN(n) && !isNaN(parseFloat(n));
29
+ const makeSecret = () => randomBytes(32);
16
30
  const minAmount = 10000;
17
31
  const minRate = 0;
18
32
  const mtokensAsTokens = n => Number(BigInt(n) / BigInt(1e3));
33
+ const pushSwapRequestMaxFee = 10;
34
+ const pushSwapRequestTokens = 10;
19
35
  const rateDenominator = 1e6;
36
+ const rateForFee = (tokens, fee) => fee * 1e6 / tokens;
37
+ const sha256 = preimage => createHash('sha256').update(preimage).digest();
38
+ const typeKeySendPreimage = '5482373484';
39
+ const typeKeySendTrade = '805805';
40
+ const typeSwapResponse = serviceTypes.serviceTypeSwapResponse;
20
41
 
21
42
  /** Request a swap out
22
43
 
23
44
  {
24
45
  ask: <Ask Function>
25
46
  [is_avoiding_broadcast]: <Avoid Broadcasting Bool>
47
+ [is_external_funding]: <Externally Fund Swap Bool>
26
48
  [is_uncooperative]: <Avoid Cooperative Resolution Bool>
27
49
  lnd: <Authenticated LND API Object>
28
50
  logger: <Winston Logger Object>
29
51
  [min_confirmations]: <Minimum Confirmations to Wait Number>
52
+ [push_to]: <Push Swap Request to Node with Identity Public Key Hex String>
30
53
  [request]: <Request Function>
31
54
  }
32
55
 
@@ -73,11 +96,36 @@ module.exports = (args, cbk) => {
73
96
  ({tokens}) => cbk(null, Number(tokens)));
74
97
  }],
75
98
 
76
- // Get the network name
99
+ // Attempt to connect to the push peer
100
+ connect: ['validate', asyncReflect(({}, cbk) => {
101
+ // Exit early when there is no push
102
+ if (!args.push_to) {
103
+ return cbk();
104
+ }
105
+
106
+ return connectPeer({id: args.push_to, lnd: args.lnd}, cbk);
107
+ })],
108
+
109
+ // Get the public key for node identity attachment
110
+ getIdentity: ['validate', ({}, cbk) => {
111
+ // Exit early when not pushing a request
112
+ if (!args.push_to) {
113
+ return cbk();
114
+ }
115
+
116
+ return getIdentity({lnd: args.lnd}, cbk);
117
+ }],
118
+
119
+ // Get the network name to decode the encoded swap payment request
77
120
  getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
78
121
 
79
122
  // Ask for routing fee rate
80
123
  askForRate: ['askForTokens', ({}, cbk) => {
124
+ // Exit earlyw hen there is no internal routing
125
+ if (!!args.is_external_funding) {
126
+ return cbk();
127
+ }
128
+
81
129
  return args.ask({
82
130
  default: defaultFeeRate,
83
131
  message: 'Max routing fee rate for swap funds in parts per million?',
@@ -97,6 +145,32 @@ module.exports = (args, cbk) => {
97
145
  ({rate}) => cbk(null, Number(rate)));
98
146
  }],
99
147
 
148
+ // Ask for the max swap fee rate that will be paid automatically
149
+ askForSwapRate: ['askForRate', ({}, cbk) => {
150
+ // Exit early when not pushing a swap request
151
+ if (!args.push_to) {
152
+ return cbk();
153
+ }
154
+
155
+ return args.ask({
156
+ default: defaultFeeRate,
157
+ message: 'Max auto-accept fee rate for funds in parts per million?',
158
+ name: 'max',
159
+ validate: input => {
160
+ if (!isNumber(input)) {
161
+ return false;
162
+ }
163
+
164
+ if (Number(input) < minRate) {
165
+ return `A larger rate is required, minimum: ${minRate}`;
166
+ }
167
+
168
+ return true;
169
+ },
170
+ },
171
+ ({max}) => cbk(null, Number(max)));
172
+ }],
173
+
100
174
  // Make a swap request
101
175
  makeRequest: ['askForTokens', ({askForTokens}, cbk) => {
102
176
  return makeRequest({
@@ -107,13 +181,118 @@ module.exports = (args, cbk) => {
107
181
  cbk);
108
182
  }],
109
183
 
184
+ // Listen on p2p for a response to the swap request
185
+ listenForResponse: [
186
+ 'getNetwork',
187
+ 'makeRequest',
188
+ ({getNetwork, makeRequest}, cbk) =>
189
+ {
190
+ // Exit early when not pushing a request
191
+ if (!args.push_to) {
192
+ return cbk();
193
+ }
194
+
195
+ const service = servicePeerRequests({lnd: args.lnd});
196
+
197
+ // The identifier of the swap is the hash over the swap request
198
+ const id = bufferAsHex(sha256(hexAsBuffer(makeRequest.request)));
199
+
200
+ // Listen for an accepted swap request
201
+ service.request({type: typeSwapResponse}, (req, res) => {
202
+ // Exit early when the request is from a different peer
203
+ if (req.from !== args.push_to) {
204
+ return;
205
+ }
206
+
207
+ // Exit early when the request is for a different swap
208
+ if (findRecord(req.records, pushTypes.typeSwapId) !== id) {
209
+ return;
210
+ }
211
+
212
+ // The peer will message the response to the swap in a record
213
+ const response = findRecord(req.records, pushTypes.typeSwapResponse);
214
+
215
+ // Check the swap response is valid
216
+ try {
217
+ decodeOffToOnResponse({response, network: getNetwork.bitcoinjs});
218
+ } catch (err) {
219
+ return res.failure([400, err.message]);
220
+ }
221
+
222
+ args.logger.info({got_response: true});
223
+
224
+ // Tell the peer that the swap response was received
225
+ res.success({});
226
+
227
+ // Don't listen for additional results
228
+ service.stop({});
229
+
230
+ return cbk(null, response);
231
+ });
232
+ }],
233
+
234
+ // Push the request to the destination
235
+ pushRequest: [
236
+ 'askForSwapRate',
237
+ 'getIdentity',
238
+ 'makeRequest',
239
+ ({getIdentity, makeRequest}, cbk) =>
240
+ {
241
+ // Exit early when this is not a push request
242
+ if (!args.push_to) {
243
+ return cbk();
244
+ }
245
+
246
+ const secret = makeSecret();
247
+
248
+ const id = bufferAsHex(sha256(secret));
249
+
250
+ // Include local peer details so the node can p2p message the response
251
+ const {trade} = encodeTrade({
252
+ swap: {node: getIdentity.public_key, request: makeRequest.request},
253
+ });
254
+
255
+ args.logger.info({
256
+ sending_swap_request_to: args.push_to,
257
+ push_request_id: id,
258
+ });
259
+
260
+ // Push the swap request
261
+ return payViaPaymentDetails({
262
+ id,
263
+ destination: args.push_to,
264
+ lnd: args.lnd,
265
+ max_fee: pushSwapRequestMaxFee,
266
+ messages: [
267
+ {type: typeKeySendPreimage, value: bufferAsHex(secret)},
268
+ {type: typeKeySendTrade, value: trade},
269
+ ],
270
+ tokens: pushSwapRequestTokens,
271
+ },
272
+ (err, res) => {
273
+ if (!!err) {
274
+ return cbk(err);
275
+ }
276
+
277
+ args.logger.info({sent_swap_request_waiting_for_response: true});
278
+
279
+ return cbk();
280
+ });
281
+ }],
282
+
110
283
  // Wait for a response
111
284
  getResponse: [
112
285
  'askForRate',
113
286
  'getNetwork',
287
+ 'listenForResponse',
114
288
  'makeRequest',
115
- ({getNetwork, makeRequest}, cbk) =>
289
+ ({getNetwork, listenForResponse, makeRequest}, cbk) =>
116
290
  {
291
+ // Exit early when there is a p2p request
292
+ if (!!listenForResponse) {
293
+ return cbk(null, listenForResponse);
294
+ }
295
+
117
296
  args.logger.info({swap_request: makeRequest.request});
118
297
 
119
298
  return args.ask({
@@ -138,10 +317,11 @@ module.exports = (args, cbk) => {
138
317
 
139
318
  // Confirm the swap details
140
319
  okSwap: [
320
+ 'askForSwapRate',
141
321
  'askForTokens',
142
322
  'getNetwork',
143
323
  'getResponse',
144
- ({askForTokens, getNetwork, getResponse}, cbk) =>
324
+ ({askForSwapRate, askForTokens, getNetwork, getResponse}, cbk) =>
145
325
  {
146
326
  const response = decodeOffToOnResponse({
147
327
  network: getNetwork.bitcoinjs,
@@ -155,6 +335,11 @@ module.exports = (args, cbk) => {
155
335
  const fee = tokens - askForTokens;
156
336
  const pricing = `Execution cost ${deposit}, plus liquidity fee ${fee}`;
157
337
 
338
+ // Exit early when swap fee is below the pre-specified tolerance
339
+ if (rateForFee(askForTokens, deposit + fee) <= askForSwapRate) {
340
+ return cbk(null, true)
341
+ }
342
+
158
343
  return args.ask({
159
344
  default: true,
160
345
  message: `Start swap ${timeout}? ${pricing}?`,
@@ -184,6 +369,7 @@ module.exports = (args, cbk) => {
184
369
  return completeOffToOnSwap({
185
370
  emitter,
186
371
  is_avoiding_broadcast: args.is_avoiding_broadcast,
372
+ is_external_funding: args.is_external_funding,
187
373
  is_uncooperative: args.is_uncooperative,
188
374
  lnd: args.lnd,
189
375
  max_fee_deposit: defaultMaxFeeForDeposit,
@@ -1,3 +1,4 @@
1
+ const {createHash} = require('crypto');
1
2
  const EventEmitter = require('events');
2
3
 
3
4
  const asyncAuto = require('async/auto');
@@ -5,17 +6,30 @@ const {getChainFeeRate} = require('ln-service');
5
6
  const {returnResult} = require('asyncjs-util');
6
7
 
7
8
  const completeOnToOffSwap = require('./complete_on_to_off_swap');
9
+ const {connectPeer} = require('ln-sync');
8
10
  const decodeOffToOnRequest = require('./decode_off_to_on_request');
11
+ const {makePeerRequest} = require('./../p2p');
12
+ const serviceTypes = require('./../service_types');
9
13
  const startOnToOffSwap = require('./start_on_to_off_swap');
10
14
 
15
+ const bufferAsHex = buffer => buffer.toString('hex');
11
16
  const {ceil} = Math;
17
+ const defaultConfirmationTarget = 6;
12
18
  const {floor} = Math;
13
19
  const defaultCltvDelta = 400;
14
- const defaultFeeRate = 5000;
20
+ const defaultFeeRate = 2500;
15
21
  const estimatedVirtualSize = 300;
22
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
16
23
  const isNumber = n => !isNaN(n) && !isNaN(parseFloat(n));
24
+ const maxTarget = 144;
17
25
  const minRate = 1;
26
+ const minTarget = 2;
27
+ const peerRequestTimeoutMs = 1000 * 60 * 3;
18
28
  const rateDenominator = 1e6;
29
+ const sha256 = preimage => createHash('sha256').update(preimage).digest();
30
+ const typeSwapId = '0';
31
+ const typeSwapReply = '1';
32
+ const typeSwapResponse = serviceTypes.serviceTypeSwapResponse;
19
33
 
20
34
  /** Respond to a swap out request
21
35
 
@@ -24,11 +38,13 @@ const rateDenominator = 1e6;
24
38
  lnd: <Authenticated LND API Object>
25
39
  logger: <Winston Logger Object>
26
40
  [request]: <Request Function>
41
+ [swap]: <Swap Hex String>
42
+ [to]: <Send Swap Response To Public Key Id Hex String>
27
43
  }
28
44
 
29
45
  @returns via cbk or Promise
30
46
  */
31
- module.exports = ({ask, lnd, logger, request}, cbk) => {
47
+ module.exports = ({ask, lnd, logger, request, swap, to}, cbk) => {
32
48
  return new Promise((resolve, reject) => {
33
49
  return asyncAuto({
34
50
  // Check arguments
@@ -45,11 +61,24 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
45
61
  return cbk([400, 'ExpectedLoggerToRespondToSwapOutRequest']);
46
62
  }
47
63
 
64
+ if (!!swap) {
65
+ try {
66
+ decodeOffToOnRequest({request: swap});
67
+ } catch (err) {
68
+ return cbk([400, err.message]);
69
+ }
70
+ }
71
+
48
72
  return cbk();
49
73
  },
50
74
 
51
75
  // Ask for a request
52
76
  askForRequest: ['validate', ({}, cbk) => {
77
+ // Exit early when a swap request is defined
78
+ if (!!swap) {
79
+ return cbk(null, swap);
80
+ }
81
+
53
82
  return ask({
54
83
  message: 'Swap request?',
55
84
  name: 'req',
@@ -70,8 +99,14 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
70
99
  ({req}) => cbk(null, req));
71
100
  }],
72
101
 
73
- // Get the chain fee rate
74
- getRate: ['validate', ({}, cbk) => getChainFeeRate({lnd}, cbk)],
102
+ // Connect to the peer when a swap is pushed
103
+ connect: ['validate', ({}, cbk) => {
104
+ if (!to) {
105
+ return cbk();
106
+ }
107
+
108
+ return connectPeer({lnd, id: to}, cbk);
109
+ }],
75
110
 
76
111
  // Ask for pricing
77
112
  askForRate: ['askForRequest', ({askForRequest}, cbk) => {
@@ -98,6 +133,36 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
98
133
  ({rate}) => cbk(null, Number(rate)));
99
134
  }],
100
135
 
136
+ // Ask for chain fee target
137
+ askForTarget: ['askForRate', ({}, cbk) => {
138
+ return ask({
139
+ default: defaultConfirmationTarget,
140
+ message: 'Confirm on-chain funding within how many blocks?',
141
+ name: 'target',
142
+ validate: input => {
143
+ if (!isNumber(input)) {
144
+ return false;
145
+ }
146
+
147
+ if (Number(input) > maxTarget) {
148
+ return `A smaller number is required, maximum: ${maxTarget}`;
149
+ }
150
+
151
+ if (Number(input) < minTarget) {
152
+ return `A larger number is required, minimum: ${minTarget}`;
153
+ }
154
+
155
+ return true;
156
+ },
157
+ },
158
+ ({target}) => cbk(null, Number(target)));
159
+ }],
160
+
161
+ // Get the chain fee rate
162
+ getRate: ['askForTarget', ({askForTarget}, cbk) => {
163
+ return getChainFeeRate({lnd, confirmation_target: askForTarget}, cbk);
164
+ }],
165
+
101
166
  // Make a response
102
167
  makeResponse: [
103
168
  'askForRate',
@@ -119,10 +184,14 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
119
184
  }],
120
185
 
121
186
  // Complete the swap
122
- completeSwap: ['makeResponse', ({makeResponse}, cbk) => {
187
+ completeSwap: [
188
+ 'askForTarget',
189
+ 'makeResponse',
190
+ ({askForTarget, makeResponse}, cbk) =>
191
+ {
123
192
  logger.info({
124
193
  recovery: makeResponse.recovery,
125
- response: makeResponse.response,
194
+ response: !to ? makeResponse.response : undefined,
126
195
  });
127
196
 
128
197
  const emitter = new EventEmitter();
@@ -133,10 +202,31 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
133
202
  emitter,
134
203
  lnd,
135
204
  request,
205
+ confirmation_target: askForTarget,
136
206
  recovery: makeResponse.recovery,
137
207
  },
138
208
  cbk);
139
209
  }],
210
+
211
+ // Send swap response over p2p so the peer can start the payment
212
+ sendResponse: ['connect', 'makeResponse', ({makeResponse}, cbk) => {
213
+ // Exit early when a swap response destination is not defined
214
+ if (!swap || !to) {
215
+ return cbk();
216
+ }
217
+
218
+ return makePeerRequest({
219
+ lnd,
220
+ to,
221
+ records: [
222
+ {type: typeSwapId, value: bufferAsHex(sha256(hexAsBuffer(swap)))},
223
+ {type: typeSwapReply, value: makeResponse.response},
224
+ ],
225
+ timeout: peerRequestTimeoutMs,
226
+ type: typeSwapResponse,
227
+ },
228
+ cbk);
229
+ }],
140
230
  },
141
231
  returnResult({reject, resolve, of: 'completeSwap'}, cbk));
142
232
  });
@@ -23,5 +23,9 @@
23
23
  "typeTokens": "13",
24
24
  "typeVersion": "0"
25
25
  },
26
+ "pushTypes": {
27
+ "typeSwapId": "0",
28
+ "typeSwapResponse": "1"
29
+ },
26
30
  "typePayMetadata": "805001"
27
31
  }
@@ -99,8 +99,8 @@ test(`Start offchain swap`, async ({end, equal, strictSame}) => {
99
99
  if (args.name === 'response') {
100
100
  return await respondToSwapOut({
101
101
  ask: (args, cbk) => {
102
- if (args.name === 'rate') {
103
- return cbk({rate: '100'});
102
+ if (args.default) {
103
+ return cbk({[args.name]: args.default});
104
104
  }
105
105
 
106
106
  if (args.name === 'req') {
@@ -99,8 +99,8 @@ test(`Swap with claim path`, async ({end, equal, strictSame}) => {
99
99
  if (args.name === 'response') {
100
100
  return await respondToSwapOut({
101
101
  ask: (args, cbk) => {
102
- if (args.name === 'rate') {
103
- return cbk({rate: '100'});
102
+ if (args.default) {
103
+ return cbk({[args.name]: args.default});
104
104
  }
105
105
 
106
106
  if (args.name === 'req') {
@@ -100,8 +100,8 @@ test(`Timeout a swap`, async ({end, equal, strictSame}) => {
100
100
  try {
101
101
  return await respondToSwapOut({
102
102
  ask: (args, cbk) => {
103
- if (args.name === 'rate') {
104
- return cbk({rate: '100'});
103
+ if (args.default) {
104
+ return cbk({[args.name]: args.default});
105
105
  }
106
106
 
107
107
  if (args.name === 'req') {
@@ -5,13 +5,8 @@ const method = require('./../../trades/encode_trade');
5
5
  const tests = [
6
6
  {
7
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',
8
+ description: 'Trade details are required',
9
+ error: 'ExpectedTradeDetailsToEncode',
15
10
  },
16
11
  {
17
12
  args: {
@@ -0,0 +1,55 @@
1
+ const {decodeTlvStream} = require('bolt01');
2
+
3
+ const recordAsNode = require('./record_as_node');
4
+
5
+ const findSwapRecord = records => records.find(n => n.type === '6');
6
+ const {isArray} = Array;
7
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
8
+ const nodeForSwap = record => record.substring(0, 66);
9
+ const requestForSwap = record => record.substring(66);
10
+
11
+ /** Decode swap trade records
12
+
13
+ 6: <Swap Request>
14
+
15
+ {
16
+ records: [{
17
+ type: <Type Number String>
18
+ value: <Value Hex Encoded String>
19
+ }]
20
+ }
21
+
22
+ @throws
23
+ <Error>
24
+
25
+ @returns
26
+ {
27
+ node: <Node Public Key Id Hex String>
28
+ request: <Swap Request Hex String>
29
+ }
30
+ */
31
+ module.exports = ({records}) => {
32
+ if (!isArray(records)) {
33
+ throw new Error('ExpectedArrayOfRecordsToDecodeSwapTrade');
34
+ }
35
+
36
+ const swapRecord = findSwapRecord(records);
37
+
38
+ if (!swapRecord) {
39
+ throw new Error('ExpectedSwapRecordInSwapTrade');
40
+ }
41
+
42
+ const node = nodeForSwap(swapRecord.value)
43
+
44
+ if (!isPublicKey(node)) {
45
+ throw new Error('ExpectedIdentityPublicKeyInSwapTrade');
46
+ }
47
+
48
+ const request = requestForSwap(swapRecord.value);
49
+
50
+ if (!request) {
51
+ throw new Error('ExpectedSwapRequestInSwapTrade');
52
+ }
53
+
54
+ return {node, request};
55
+ };
@@ -2,6 +2,7 @@ const {decodeBigSize} = require('bolt01');
2
2
  const {decodeTlvStream} = require('bolt01');
3
3
 
4
4
  const decodeOpenTrade = require('./decode_open_trade');
5
+ const decodeSwapTrade = require('./decode_swap_trade');
5
6
  const decodeTradePayment = require('./decode_trade_payment');
6
7
  const decodeTradeSecret = require('./decode_trade_secret');
7
8
  const networkFromNetworkRecord = require('./network_from_network_record');
@@ -10,8 +11,9 @@ const findDetailsRecord = records => records.find(n => n.type === '3');
10
11
  const findNetwork = records => records.find(n => n.type === '1');
11
12
  const findVer = records => records.find(n => n.type === '0') || {value: '00'};
12
13
  const findRequestRecord = records => records.find(n => n.type === '2');
14
+ const findSwapRecord = records => records.find(n => n.type === '6');
13
15
  const isTrade = trade => trade.toLowerCase().startsWith('626f73ff');
14
- const isValidVersion = version => BigInt(version) <= BigInt(1);
16
+ const isValidVersion = version => BigInt(version) <= BigInt(2);
15
17
  const tradeData = trade => trade.slice('626f73ff'.length);
16
18
 
17
19
  /** Decode a trade record
@@ -22,6 +24,7 @@ const tradeData = trade => trade.slice('626f73ff'.length);
22
24
  [3]: <Trade Details Record
23
25
  [4]: <Nodes Records>
24
26
  [5]: <Trade Identifier>
27
+ [6]: <Swap Request>
25
28
 
26
29
  {
27
30
  trade: <Trade Record Hex String>
@@ -49,6 +52,10 @@ const tradeData = trade => trade.slice('626f73ff'.length);
49
52
  payload: <Preimage Encrypted Payload Hex String>
50
53
  request: <BOLT 11 Payment Request String>
51
54
  }
55
+ [swap]: {
56
+ node: <Node Public Key Id Hex String>
57
+ request: <Swap Request Hex String>
58
+ }
52
59
  }
53
60
  */
54
61
  module.exports = ({trade}) => {
@@ -98,5 +105,10 @@ module.exports = ({trade}) => {
98
105
  return {payment: decodeTradePayment({network, records})};
99
106
  }
100
107
 
108
+ // Exit early when there is a swap encoded
109
+ if (!!findSwapRecord(records)) {
110
+ return {swap: decodeSwapTrade({records})};
111
+ }
112
+
101
113
  return {connect: decodeOpenTrade({network, records})};
102
114
  };
@@ -0,0 +1,38 @@
1
+ const {encodeTlvStream} = require('bolt01');
2
+
3
+ const networkRecordFromNetwork = require('./network_record_from_network');
4
+ const nodeAsRecord = require('./node_as_record');
5
+
6
+ const asTrade = hex => `626f73ff${hex}`;
7
+ const typeSwap = '6';
8
+ const typeVersion = '0';
9
+ const versionRecord = '02';
10
+
11
+ /** Encode records for a swap trade request
12
+
13
+ A swap trade request
14
+
15
+ 0: version (v2)
16
+ 6: swap request
17
+
18
+ {
19
+ node: <Swap Node Identity Public Key Hex String>
20
+ request: <Swap Request Hex String>
21
+ }
22
+
23
+ @throws
24
+ <Error>
25
+
26
+ @returns
27
+ {
28
+ trade: <Encoded Swap Trade Request Hex String>
29
+ }
30
+ */
31
+ module.exports = ({node, request}) => {
32
+ const records = [
33
+ {type: typeSwap, value: node + request},
34
+ {type: typeVersion, value: versionRecord},
35
+ ];
36
+
37
+ return {trade: asTrade(encodeTlvStream({records}).encoded)};
38
+ };
@@ -1,4 +1,5 @@
1
1
  const encodeOpenTrade = require('./encode_open_trade');
2
+ const encodeSwapTrade = require('./encode_swap_trade');
2
3
  const encodeTradeSecret = require('./encode_trade_secret');
3
4
 
4
5
  /** Encode v1 trade records
@@ -12,6 +13,7 @@ const encodeTradeSecret = require('./encode_trade_secret');
12
13
  [3]: trade details
13
14
  [4]: nodes records
14
15
  [5]: trade identifier
16
+ [6]: swap request
15
17
 
16
18
  {
17
19
  [connect]: {
@@ -28,6 +30,10 @@ const encodeTradeSecret = require('./encode_trade_secret');
28
30
  payload: <Preimage Encrypted Payload Hex String>
29
31
  request: <BOLT 11 Payment Request String>
30
32
  }
33
+ [swap]: {
34
+ node: <Node Public Key Id Hex String>
35
+ request: <Swap Request Hex String>
36
+ }
31
37
  }
32
38
 
33
39
  @throws
@@ -38,16 +44,11 @@ const encodeTradeSecret = require('./encode_trade_secret');
38
44
  trade: <Hex Encoded Trade String>
39
45
  }
40
46
  */
41
- module.exports = ({connect, secret}) => {
42
- if (!connect && !secret) {
43
- throw new Error('ExpectedEitherConnectDetailsOrTradeSecret');
44
- }
45
-
46
- if (!!connect && !!secret) {
47
- throw new Error('ExpectedOnlyOneConnectDetailsOrTradeSecretNotBoth');
47
+ module.exports = ({connect, secret, swap}) => {
48
+ if (!connect && !secret && !swap) {
49
+ throw new Error('ExpectedTradeDetailsToEncode');
48
50
  }
49
51
 
50
- // Exit early when this is an open ended trade
51
52
  if (!!connect) {
52
53
  return encodeOpenTrade({
53
54
  id: connect.id,
@@ -56,10 +57,17 @@ module.exports = ({connect, secret}) => {
56
57
  });
57
58
  }
58
59
 
59
- // Encode the trade secret
60
- return encodeTradeSecret({
61
- auth: secret.auth,
62
- payload: secret.payload,
63
- request: secret.request,
64
- });
60
+ if (!!secret) {
61
+ return encodeTradeSecret({
62
+ auth: secret.auth,
63
+ payload: secret.payload,
64
+ request: secret.request,
65
+ });
66
+ }
67
+
68
+ if (!!swap) {
69
+ return encodeSwapTrade({node: swap.node, request: swap.request});
70
+ }
71
+
72
+ throw new Error('ExpectedTradeTypeToEncode');
65
73
  };