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 +4 -0
- 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 +270 -40
- package/swaps/request_swap_out.js +189 -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 +2 -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.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.
|
|
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.2"
|
|
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,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 = (
|
|
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: [
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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: [
|
|
98
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
364
|
+
'respondToPush',
|
|
365
|
+
({makeRequest, makeResponse, recoverResponse, respondToPush}, cbk) =>
|
|
152
366
|
{
|
|
153
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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') {
|
|
@@ -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
|
};
|