paid-services 3.15.3 → 3.16.1
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 +5 -1
- package/package.json +10 -10
- package/service_types.json +3 -1
- package/swaps/complete_off_to_on_swap.js +9 -2
- package/swaps/complete_on_to_off_swap.js +2 -2
- package/swaps/manage_swap.js +272 -40
- package/swaps/request_swap_out.js +191 -3
- package/swaps/respond_to_swap_out_request.js +96 -6
- package/swaps/swap_field_types.json +4 -0
- package/test/integration/test_request_swap_out.js +3 -2
- package/test/integration/test_swap_with_claim_path.js +2 -2
- package/test/integration/test_swap_with_refund_path.js +2 -2
- package/test/trades/test_encode_trade.js +2 -7
- package/trades/decode_swap_trade.js +55 -0
- package/trades/decode_trade.js +13 -1
- package/trades/encode_swap_trade.js +38 -0
- package/trades/encode_trade.js +22 -14
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -8,24 +8,24 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@alexbosworth/fiat": "1.0.2",
|
|
11
|
-
"async": "3.2.
|
|
11
|
+
"async": "3.2.4",
|
|
12
12
|
"asyncjs-util": "1.2.9",
|
|
13
13
|
"bech32": "2.0.0",
|
|
14
|
-
"bolt01": "1.2.
|
|
15
|
-
"bolt07": "1.8.
|
|
14
|
+
"bolt01": "1.2.5",
|
|
15
|
+
"bolt07": "1.8.2",
|
|
16
16
|
"ecpair": "2.0.1",
|
|
17
|
-
"goldengate": "11.2.
|
|
18
|
-
"invoices": "2.0.
|
|
19
|
-
"ln-service": "53.
|
|
20
|
-
"ln-sync": "3.12.
|
|
21
|
-
"psbt": "2.0
|
|
17
|
+
"goldengate": "11.2.2",
|
|
18
|
+
"invoices": "2.0.7",
|
|
19
|
+
"ln-service": "53.17.2",
|
|
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.
|
|
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.
|
|
50
|
+
"version": "3.16.1"
|
|
51
51
|
}
|
package/service_types.json
CHANGED
|
@@ -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
|
}],
|
package/swaps/manage_swap.js
CHANGED
|
@@ -1,17 +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);
|
|
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);
|
|
12
25
|
const recoverRespondAction = 'recover-response';
|
|
13
26
|
const requestAction = 'request';
|
|
14
27
|
const respondAction = 'respond';
|
|
28
|
+
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
|
|
29
|
+
const typeKeySendTrade = '805805';
|
|
15
30
|
|
|
16
31
|
/** Manage a swap
|
|
17
32
|
|
|
@@ -24,37 +39,118 @@ const respondAction = 'respond';
|
|
|
24
39
|
|
|
25
40
|
@returns via cbk or Promise
|
|
26
41
|
*/
|
|
27
|
-
module.exports = (
|
|
42
|
+
module.exports = (args, cbk) => {
|
|
28
43
|
return new Promise((resolve, reject) => {
|
|
29
44
|
return asyncAuto({
|
|
30
45
|
// Check arguments
|
|
31
46
|
validate: cbk => {
|
|
32
|
-
if (!ask) {
|
|
47
|
+
if (!args.ask) {
|
|
33
48
|
return cbk([400, 'ExpectedAskFunctionToManageSwap']);
|
|
34
49
|
}
|
|
35
50
|
|
|
36
|
-
if (!lnd) {
|
|
51
|
+
if (!args.lnd) {
|
|
37
52
|
return cbk([400, 'ExpectedAuthenticatedLndToManageSwap']);
|
|
38
53
|
}
|
|
39
54
|
|
|
40
|
-
if (!logger) {
|
|
55
|
+
if (!args.logger) {
|
|
41
56
|
return cbk([400, 'ExpectedWinstonLoggerToManageSwap']);
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
if (!request) {
|
|
59
|
+
if (!args.request) {
|
|
45
60
|
return cbk([400, 'ExpectedRequestFunctionToManageSwap']);
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
return cbk();
|
|
49
64
|
},
|
|
50
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
|
+
|
|
51
75
|
// Look for a p2tr master public key to detect p2tr support
|
|
52
76
|
getMasterPublicKeys: ['validate', ({}, cbk) => {
|
|
53
|
-
return getMasterPublicKeys({lnd}, cbk);
|
|
77
|
+
return getMasterPublicKeys({lnd: args.lnd}, cbk);
|
|
54
78
|
}],
|
|
55
79
|
|
|
56
80
|
// Get the network to make sure this is on testnet
|
|
57
|
-
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
|
+
}],
|
|
58
154
|
|
|
59
155
|
// Determine if taproot is supported or not
|
|
60
156
|
hasTr: ['getMasterPublicKeys', ({getMasterPublicKeys}, cbk) => {
|
|
@@ -64,27 +160,53 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
|
|
|
64
160
|
}],
|
|
65
161
|
|
|
66
162
|
// Select a swap action
|
|
67
|
-
selectAction: [
|
|
163
|
+
selectAction: [
|
|
164
|
+
'getAliases',
|
|
165
|
+
'getExisting',
|
|
166
|
+
'getNetwork',
|
|
167
|
+
'hasTr',
|
|
168
|
+
'swapPushes',
|
|
169
|
+
({getAliases, getExisting, getNetwork, hasTr, swapPushes}, cbk) =>
|
|
170
|
+
{
|
|
68
171
|
// Check to make sure the network is supported
|
|
69
172
|
if (!hasTr && getNetwork.network !== allowedNetwork) {
|
|
70
173
|
return cbk([501, 'CurrentlyUnsupportedChainForSwap']);
|
|
71
174
|
}
|
|
72
175
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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),
|
|
88
210
|
message: 'Off-chain to on-chain swap:',
|
|
89
211
|
name: 'action',
|
|
90
212
|
type: 'list',
|
|
@@ -92,32 +214,125 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
|
|
|
92
214
|
({action}) => cbk(null, action));
|
|
93
215
|
}],
|
|
94
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
|
+
|
|
95
263
|
// Make swap request
|
|
96
|
-
makeRequest: [
|
|
97
|
-
|
|
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:
|
|
98
283
|
return cbk();
|
|
99
284
|
}
|
|
100
285
|
|
|
101
286
|
return requestSwapOut({
|
|
102
|
-
ask,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
287
|
+
ask: args.ask,
|
|
288
|
+
is_external_funding: askForExternal,
|
|
289
|
+
lnd: args.lnd,
|
|
290
|
+
logger: args.logger,
|
|
291
|
+
push_to: findKey.public_key,
|
|
292
|
+
min_confirmations: minConfirmations[getNetwork.network],
|
|
293
|
+
request: !hasTr ? args.request : undefined,
|
|
106
294
|
},
|
|
107
295
|
cbk);
|
|
108
296
|
}],
|
|
109
297
|
|
|
110
|
-
//
|
|
298
|
+
// Make a response for a swap request
|
|
111
299
|
makeResponse: ['hasTr', 'selectAction', ({hasTr, selectAction}, cbk) => {
|
|
112
300
|
if (selectAction !== respondAction) {
|
|
113
301
|
return cbk();
|
|
114
302
|
}
|
|
115
303
|
|
|
116
304
|
return respondToSwapOutRequest({
|
|
117
|
-
ask,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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,
|
|
121
336
|
},
|
|
122
337
|
cbk);
|
|
123
338
|
}],
|
|
@@ -133,10 +348,10 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
|
|
|
133
348
|
}
|
|
134
349
|
|
|
135
350
|
return recoverResponseToSwapOut({
|
|
136
|
-
ask,
|
|
137
|
-
lnd,
|
|
138
|
-
logger,
|
|
139
|
-
request: !hasTr ? request : undefined,
|
|
351
|
+
ask: args.ask,
|
|
352
|
+
lnd: args.lnd,
|
|
353
|
+
logger: args.logger,
|
|
354
|
+
request: !hasTr ? args.request : undefined,
|
|
140
355
|
},
|
|
141
356
|
cbk);
|
|
142
357
|
}],
|
|
@@ -146,9 +361,26 @@ module.exports = ({ask, lnd, logger, request}, cbk) => {
|
|
|
146
361
|
'makeRequest',
|
|
147
362
|
'makeResponse',
|
|
148
363
|
'recoverResponse',
|
|
149
|
-
|
|
364
|
+
'respondToPush',
|
|
365
|
+
({makeRequest, makeResponse, recoverResponse, respondToPush}, cbk) =>
|
|
150
366
|
{
|
|
151
|
-
|
|
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();
|
|
152
384
|
}],
|
|
153
385
|
},
|
|
154
386
|
returnResult({reject, resolve, of: 'result'}, cbk));
|
|
@@ -1,31 +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>
|
|
51
|
+
[min_confirmations]: <Minimum Confirmations to Wait Number>
|
|
52
|
+
[push_to]: <Push Swap Request to Node with Identity Public Key Hex String>
|
|
29
53
|
[request]: <Request Function>
|
|
30
54
|
}
|
|
31
55
|
|
|
@@ -72,11 +96,36 @@ module.exports = (args, cbk) => {
|
|
|
72
96
|
({tokens}) => cbk(null, Number(tokens)));
|
|
73
97
|
}],
|
|
74
98
|
|
|
75
|
-
//
|
|
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
|
|
76
120
|
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
|
|
77
121
|
|
|
78
122
|
// Ask for routing fee rate
|
|
79
123
|
askForRate: ['askForTokens', ({}, cbk) => {
|
|
124
|
+
// Exit earlyw hen there is no internal routing
|
|
125
|
+
if (!!args.is_external_funding) {
|
|
126
|
+
return cbk();
|
|
127
|
+
}
|
|
128
|
+
|
|
80
129
|
return args.ask({
|
|
81
130
|
default: defaultFeeRate,
|
|
82
131
|
message: 'Max routing fee rate for swap funds in parts per million?',
|
|
@@ -96,6 +145,32 @@ module.exports = (args, cbk) => {
|
|
|
96
145
|
({rate}) => cbk(null, Number(rate)));
|
|
97
146
|
}],
|
|
98
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
|
+
|
|
99
174
|
// Make a swap request
|
|
100
175
|
makeRequest: ['askForTokens', ({askForTokens}, cbk) => {
|
|
101
176
|
return makeRequest({
|
|
@@ -106,13 +181,118 @@ module.exports = (args, cbk) => {
|
|
|
106
181
|
cbk);
|
|
107
182
|
}],
|
|
108
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
|
+
|
|
109
283
|
// Wait for a response
|
|
110
284
|
getResponse: [
|
|
111
285
|
'askForRate',
|
|
112
286
|
'getNetwork',
|
|
287
|
+
'listenForResponse',
|
|
113
288
|
'makeRequest',
|
|
114
|
-
({getNetwork, makeRequest}, cbk) =>
|
|
289
|
+
({getNetwork, listenForResponse, makeRequest}, cbk) =>
|
|
115
290
|
{
|
|
291
|
+
// Exit early when there is a p2p request
|
|
292
|
+
if (!!listenForResponse) {
|
|
293
|
+
return cbk(null, listenForResponse);
|
|
294
|
+
}
|
|
295
|
+
|
|
116
296
|
args.logger.info({swap_request: makeRequest.request});
|
|
117
297
|
|
|
118
298
|
return args.ask({
|
|
@@ -137,10 +317,11 @@ module.exports = (args, cbk) => {
|
|
|
137
317
|
|
|
138
318
|
// Confirm the swap details
|
|
139
319
|
okSwap: [
|
|
320
|
+
'askForSwapRate',
|
|
140
321
|
'askForTokens',
|
|
141
322
|
'getNetwork',
|
|
142
323
|
'getResponse',
|
|
143
|
-
({askForTokens, getNetwork, getResponse}, cbk) =>
|
|
324
|
+
({askForSwapRate, askForTokens, getNetwork, getResponse}, cbk) =>
|
|
144
325
|
{
|
|
145
326
|
const response = decodeOffToOnResponse({
|
|
146
327
|
network: getNetwork.bitcoinjs,
|
|
@@ -154,6 +335,11 @@ module.exports = (args, cbk) => {
|
|
|
154
335
|
const fee = tokens - askForTokens;
|
|
155
336
|
const pricing = `Execution cost ${deposit}, plus liquidity fee ${fee}`;
|
|
156
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
|
+
|
|
157
343
|
return args.ask({
|
|
158
344
|
default: true,
|
|
159
345
|
message: `Start swap ${timeout}? ${pricing}?`,
|
|
@@ -183,10 +369,12 @@ module.exports = (args, cbk) => {
|
|
|
183
369
|
return completeOffToOnSwap({
|
|
184
370
|
emitter,
|
|
185
371
|
is_avoiding_broadcast: args.is_avoiding_broadcast,
|
|
372
|
+
is_external_funding: args.is_external_funding,
|
|
186
373
|
is_uncooperative: args.is_uncooperative,
|
|
187
374
|
lnd: args.lnd,
|
|
188
375
|
max_fee_deposit: defaultMaxFeeForDeposit,
|
|
189
376
|
max_fee_funding: askForTokens * askForRate / rateDenominator,
|
|
377
|
+
min_confirmations: args.min_confirmations,
|
|
190
378
|
recovery: makeRequest.recovery,
|
|
191
379
|
request: args.request,
|
|
192
380
|
response: getResponse,
|
|
@@ -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 =
|
|
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
|
-
//
|
|
74
|
-
|
|
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: [
|
|
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
|
});
|
|
@@ -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.
|
|
103
|
-
return cbk({
|
|
102
|
+
if (args.default) {
|
|
103
|
+
return cbk({[args.name]: args.default});
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
if (args.name === 'req') {
|
|
@@ -140,6 +140,7 @@ test(`Start offchain swap`, async ({end, equal, strictSame}) => {
|
|
|
140
140
|
return messages.push(message);
|
|
141
141
|
},
|
|
142
142
|
},
|
|
143
|
+
min_confirmations: 3,
|
|
143
144
|
});
|
|
144
145
|
} catch (err) {
|
|
145
146
|
strictSame(err, null, 'Expected no failure');
|
|
@@ -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.
|
|
103
|
-
return cbk({
|
|
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.
|
|
104
|
-
return cbk({
|
|
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: '
|
|
9
|
-
error: '
|
|
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
|
+
};
|
package/trades/decode_trade.js
CHANGED
|
@@ -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(
|
|
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
|
+
};
|
package/trades/encode_trade.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
};
|