paid-services 3.17.2 → 3.19.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 +9 -0
- package/groups/ask_to_confirm_group_join.js +7 -1
- package/groups/assemble_channel_group.js +3 -0
- package/groups/attach_to_channel_group.js +3 -1
- package/groups/funding/confirm_incoming_channel.js +2 -0
- package/groups/funding/sign_and_fund_group_channel.js +135 -5
- package/groups/join_channel_group.js +54 -5
- package/groups/p2p/register_pending_open.js +26 -6
- package/package.json +3 -3
- package/swaps/complete_off_to_on_swap.js +17 -2
- package/swaps/complete_on_to_off_swap.js +65 -3
- package/swaps/decode_off_to_on_response.js +23 -1
- package/swaps/decode_on_to_off_recovery.js +19 -2
- package/swaps/encode_off_to_on_response.js +22 -2
- package/swaps/manage_swap.js +48 -1
- package/swaps/request_swap_out.js +6 -1
- package/swaps/respond_to_swap_out_request.js +26 -1
- package/swaps/start_on_to_off_swap.js +2 -0
- package/swaps/swap_field_types.json +3 -1
- package/test/integration/test_request_swap_out.js +4 -0
- package/test/integration/test_swap_with_claim_path.js +4 -0
- package/test/integration/test_swap_with_refund_path.js +4 -0
- package/trades/connect_to_seller.js +57 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## Version 3.19.1
|
|
4
|
+
|
|
5
|
+
- `manageSwap`: Add support for inbound peer constraint
|
|
6
|
+
- `manageSwap`: Add support for external sweep address
|
|
7
|
+
|
|
8
|
+
## Version 3.18.0
|
|
9
|
+
|
|
10
|
+
- `manageGroupJoin`: Add conflict tx for safe funded pending channel deletion
|
|
11
|
+
|
|
3
12
|
## Version 3.17.2
|
|
4
13
|
|
|
5
14
|
- `manageGroupJoin`: Add method to coordinate or join a channels group
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncRetry = require('async/retry');
|
|
2
3
|
const {connectPeer} = require('ln-sync');
|
|
3
4
|
const {getChainBalance} = require('ln-service');
|
|
4
5
|
const {getNodeAlias} = require('ln-sync');
|
|
@@ -8,9 +9,11 @@ const {getGroupDetails} = require('./p2p');
|
|
|
8
9
|
|
|
9
10
|
const coordinatorFromJoinCode = n => n.slice(0, 66);
|
|
10
11
|
const groupIdFromJoinCode = n => n.slice(66);
|
|
12
|
+
const interval = 500;
|
|
11
13
|
const isCode = n => !!n && n.length === 98;
|
|
12
14
|
const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
|
|
13
15
|
const niceName = n => `${n.alias} ${n.id}`.trim();
|
|
16
|
+
const times = 2 * 60;
|
|
14
17
|
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
|
|
15
18
|
|
|
16
19
|
/** Ask to confirm joining a group
|
|
@@ -73,7 +76,10 @@ module.exports = ({ask, lnd}, cbk) => {
|
|
|
73
76
|
|
|
74
77
|
// Connect to the coordinator
|
|
75
78
|
connect: ['group', ({group}, cbk) => {
|
|
76
|
-
return
|
|
79
|
+
return asyncRetry({interval, times}, cbk => {
|
|
80
|
+
return connectPeer({lnd, id: group.coordinator}, cbk);
|
|
81
|
+
},
|
|
82
|
+
cbk);
|
|
77
83
|
}],
|
|
78
84
|
|
|
79
85
|
// Get the coordinator node alias
|
|
@@ -78,6 +78,9 @@ module.exports = ({capacity, count, ecp, identity, lnd, rate}) => {
|
|
|
78
78
|
return emitter.emit('error', err);
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// An error was encountered
|
|
82
|
+
coordinator.events.once('error', errored);
|
|
83
|
+
|
|
81
84
|
// Group members have registered themselves
|
|
82
85
|
coordinator.events.once('joined', async ({ids}) => {
|
|
83
86
|
emitter.emit('filled', {ids});
|
|
@@ -70,7 +70,9 @@ module.exports = ({ask, lnd, logger}, cbk) => {
|
|
|
70
70
|
return logger.info({peering_with: formatNodes(nodes)});
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
join.once('publishing', ({signed}) =>
|
|
73
|
+
join.once('publishing', ({refund, signed}) => {
|
|
74
|
+
return logger.info({refund, signed});
|
|
75
|
+
});
|
|
74
76
|
|
|
75
77
|
return;
|
|
76
78
|
}],
|
|
@@ -13,6 +13,8 @@ const half = n => n / 2;
|
|
|
13
13
|
lnd: <Authenticated LND API Object>
|
|
14
14
|
to: <Look for Outgoing Channel To Identity Public Key Hex String>
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
@returns via cbk or Promise
|
|
16
18
|
*/
|
|
17
19
|
module.exports = ({capacity, from, id, lnd, to}, cbk) => {
|
|
18
20
|
return new Promise((resolve, reject) => {
|
|
@@ -1,18 +1,28 @@
|
|
|
1
|
+
const {address} = require('bitcoinjs-lib');
|
|
1
2
|
const asyncAuto = require('async/auto');
|
|
2
3
|
const asyncRetry = require('async/retry');
|
|
4
|
+
const {createChainAddress} = require('ln-service');
|
|
5
|
+
const {createPsbt} = require('psbt');
|
|
3
6
|
const {decodePsbt} = require('psbt');
|
|
4
7
|
const {extendPsbt} = require('psbt');
|
|
5
8
|
const {fundPendingChannels} = require('ln-service');
|
|
9
|
+
const {getChainFeeRate} = require('ln-service');
|
|
10
|
+
const {getMaxFundAmount} = require('ln-sync');
|
|
6
11
|
const {getPendingChannels} = require('ln-service');
|
|
7
12
|
const {partiallySignPsbt} = require('ln-service');
|
|
13
|
+
const {payments} = require('bitcoinjs-lib');
|
|
8
14
|
const {returnResult} = require('asyncjs-util');
|
|
15
|
+
const {signPsbt} = require('ln-service');
|
|
9
16
|
const tinysecp = require('tiny-secp256k1');
|
|
10
17
|
const {Transaction} = require('bitcoinjs-lib');
|
|
11
18
|
const {unextractTransaction} = require('psbt');
|
|
12
19
|
|
|
20
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
13
21
|
const {concat} = Buffer;
|
|
14
22
|
const dummySignature = Buffer.alloc(1);
|
|
23
|
+
const format = 'p2wpkh';
|
|
15
24
|
const {from} = Buffer;
|
|
25
|
+
const {fromBech32} = address;
|
|
16
26
|
const {fromHex} = Transaction;
|
|
17
27
|
const hashAll = Transaction.SIGHASH_ALL;
|
|
18
28
|
const hashDefault = Transaction.SIGHASH_DEFAULT;
|
|
@@ -21,6 +31,9 @@ const inputAsOutpoint = n => `${n.transaction_id}:${n.transaction_vout}`;
|
|
|
21
31
|
const interval = 10;
|
|
22
32
|
const {isArray} = Array;
|
|
23
33
|
const notEmpty = arr => arr.filter(n => !!n);
|
|
34
|
+
const {p2wpkh} = payments;
|
|
35
|
+
const {random} = Math;
|
|
36
|
+
const slowConf = 144;
|
|
24
37
|
const spendAsOutpoint = n => `${n.hash.reverse().toString('hex')}:${n.index}`;
|
|
25
38
|
const times = 500;
|
|
26
39
|
|
|
@@ -49,6 +62,7 @@ const times = 500;
|
|
|
49
62
|
|
|
50
63
|
@returns via cbk or Promise
|
|
51
64
|
{
|
|
65
|
+
conflict: <Conflict Transaction Hex String>
|
|
52
66
|
psbt: <Partially Signed PSBT Hex String>
|
|
53
67
|
}
|
|
54
68
|
*/
|
|
@@ -79,11 +93,94 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
|
|
|
79
93
|
return cbk();
|
|
80
94
|
},
|
|
81
95
|
|
|
96
|
+
// Create a conflicting address to refund funds to
|
|
97
|
+
createConflictAddress: ['validate', ({}, cbk) => {
|
|
98
|
+
return createChainAddress({format, lnd}, cbk);
|
|
99
|
+
}],
|
|
100
|
+
|
|
101
|
+
// Get the conflicting tx fee rate
|
|
102
|
+
getRate: ['validate', ({}, cbk) => {
|
|
103
|
+
return getChainFeeRate({lnd, confirmation_target: slowConf}, cbk);
|
|
104
|
+
}],
|
|
105
|
+
|
|
106
|
+
// Find the conflicting amount to send to the refund address
|
|
107
|
+
getConflictAmount: [
|
|
108
|
+
'createConflictAddress',
|
|
109
|
+
'getRate',
|
|
110
|
+
({createConflictAddress, getRate}, cbk) =>
|
|
111
|
+
{
|
|
112
|
+
const [input] = utxos;
|
|
113
|
+
|
|
114
|
+
return getMaxFundAmount({
|
|
115
|
+
lnd,
|
|
116
|
+
addresses: [createConflictAddress.address],
|
|
117
|
+
fee_tokens_per_vbyte: getRate.tokens_per_vbyte,
|
|
118
|
+
inputs: [{
|
|
119
|
+
tokens: input.witness_utxo.tokens,
|
|
120
|
+
transaction_id: input.transaction_id,
|
|
121
|
+
transaction_vout: input.transaction_vout,
|
|
122
|
+
}],
|
|
123
|
+
},
|
|
124
|
+
cbk);
|
|
125
|
+
}],
|
|
126
|
+
|
|
127
|
+
// Create a conflicting PSBT to sign
|
|
128
|
+
conflict: [
|
|
129
|
+
'createConflictAddress',
|
|
130
|
+
'ecp',
|
|
131
|
+
'getConflictAmount',
|
|
132
|
+
({createConflictAddress, ecp, getConflictAmount}, cbk) =>
|
|
133
|
+
{
|
|
134
|
+
const hash = fromBech32(createConflictAddress.address).data;
|
|
135
|
+
const [input] = utxos;
|
|
136
|
+
|
|
137
|
+
const {psbt} = createPsbt({
|
|
138
|
+
outputs: [{
|
|
139
|
+
script: bufferAsHex(p2wpkh({hash}).output),
|
|
140
|
+
tokens: getConflictAmount.max_tokens,
|
|
141
|
+
}],
|
|
142
|
+
utxos: [{
|
|
143
|
+
id: input.transaction_id,
|
|
144
|
+
vout: input.transaction_vout,
|
|
145
|
+
}],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const base = decodePsbt({ecp, psbt});
|
|
149
|
+
|
|
150
|
+
const tx = fromHex(base.unsigned_transaction);
|
|
151
|
+
|
|
152
|
+
const inputs = base.inputs.map((input, vin) => {
|
|
153
|
+
const outpoint = spendAsOutpoint(tx.ins[vin]);
|
|
154
|
+
|
|
155
|
+
// Look for relevant signing instructions
|
|
156
|
+
const utxo = utxos.find(n => inputAsOutpoint(n) === outpoint) || {};
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
bip32_derivations: utxo.bip32_derivations,
|
|
160
|
+
non_witness_utxo: utxo.non_witness_utxo,
|
|
161
|
+
sighash_type: !!utxo.non_witness_utxo ? hashAll : hashDefault,
|
|
162
|
+
witness_utxo: utxo.witness_utxo,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Extend the base PSBT with relevant signing information
|
|
167
|
+
return cbk(null, extendPsbt({ecp, inputs, psbt}).psbt);
|
|
168
|
+
}],
|
|
169
|
+
|
|
170
|
+
// Decode the PSBT to get the unsigned funding transaction
|
|
171
|
+
funding: ['ecp', 'validate', ({ecp}, cbk) => {
|
|
172
|
+
try {
|
|
173
|
+
return cbk(null, decodePsbt({ecp, psbt}));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return cbk([400, 'ExpectedValidPsbtToSignAndFundChannel', {err}]);
|
|
176
|
+
}
|
|
177
|
+
}],
|
|
178
|
+
|
|
82
179
|
// Extend the PSBT with the derivation paths
|
|
83
|
-
psbtToSign: ['ecp', '
|
|
84
|
-
const tx = fromHex(
|
|
180
|
+
psbtToSign: ['ecp', 'funding', ({ecp, funding}, cbk) => {
|
|
181
|
+
const tx = fromHex(funding.unsigned_transaction);
|
|
85
182
|
|
|
86
|
-
const inputs =
|
|
183
|
+
const inputs = funding.inputs.map((input, vin) => {
|
|
87
184
|
const outpoint = spendAsOutpoint(tx.ins[vin]);
|
|
88
185
|
|
|
89
186
|
// Look for relevant signing instructions
|
|
@@ -101,6 +198,11 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
|
|
|
101
198
|
return cbk(null, {psbt: extendPsbt({ecp, inputs, psbt}).psbt});
|
|
102
199
|
}],
|
|
103
200
|
|
|
201
|
+
// Sign and finalize the conflicting PSBT
|
|
202
|
+
signConflict: ['conflict', ({conflict}, cbk) => {
|
|
203
|
+
return signPsbt({lnd, psbt: conflict}, cbk);
|
|
204
|
+
}],
|
|
205
|
+
|
|
104
206
|
// Partially sign the PSBT that funds the channel open
|
|
105
207
|
signPsbt: ['psbtToSign', ({psbtToSign}, cbk) => {
|
|
106
208
|
return partiallySignPsbt({lnd, psbt: psbtToSign.psbt}, cbk);
|
|
@@ -151,7 +253,7 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
|
|
|
151
253
|
}],
|
|
152
254
|
|
|
153
255
|
// Fund the pending channel with the finalized PSBT
|
|
154
|
-
fundChannel: ['finalizePsbt', ({finalizePsbt}, cbk) => {
|
|
256
|
+
fundChannel: ['conflict', 'finalizePsbt', ({finalizePsbt}, cbk) => {
|
|
155
257
|
return fundPendingChannels({
|
|
156
258
|
lnd,
|
|
157
259
|
channels: [id],
|
|
@@ -182,7 +284,35 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
|
|
|
182
284
|
},
|
|
183
285
|
cbk);
|
|
184
286
|
}],
|
|
287
|
+
|
|
288
|
+
// Final group funding transaction resolution
|
|
289
|
+
result: [
|
|
290
|
+
'ecp',
|
|
291
|
+
'signConflict',
|
|
292
|
+
'signPsbt',
|
|
293
|
+
({ecp, signConflict, signPsbt}, cbk) =>
|
|
294
|
+
{
|
|
295
|
+
const signed = decodePsbt({ecp, psbt: signPsbt.psbt});
|
|
296
|
+
|
|
297
|
+
const extended = extendPsbt({
|
|
298
|
+
ecp,
|
|
299
|
+
psbt,
|
|
300
|
+
inputs: signed.inputs.map(input => {
|
|
301
|
+
return {
|
|
302
|
+
non_witness_utxo: input.non_witness_utxo,
|
|
303
|
+
partial_sig: input.partial_sig,
|
|
304
|
+
taproot_key_spend_sig: input.taproot_key_spend_sig,
|
|
305
|
+
witness_utxo: input.witness_utxo,
|
|
306
|
+
};
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return cbk(null, {
|
|
311
|
+
conflict: signConflict.transaction,
|
|
312
|
+
psbt: extended.psbt,
|
|
313
|
+
});
|
|
314
|
+
}],
|
|
185
315
|
},
|
|
186
|
-
returnResult({reject, resolve, of: '
|
|
316
|
+
returnResult({reject, resolve, of: 'result'}, cbk));
|
|
187
317
|
});
|
|
188
318
|
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
2
|
|
|
3
3
|
const asyncAuto = require('async/auto');
|
|
4
|
+
const asyncReflect = require('async/reflect');
|
|
4
5
|
const asyncRetry = require('async/retry');
|
|
6
|
+
const {cancelPendingChannel} = require('ln-service');
|
|
5
7
|
const {decodePsbt} = require('psbt');
|
|
8
|
+
const {deletePendingChannel} = require('ln-service');
|
|
6
9
|
const {returnResult} = require('asyncjs-util');
|
|
7
10
|
const tinysecp = require('tiny-secp256k1');
|
|
8
11
|
const {Transaction} = require('bitcoinjs-lib');
|
|
@@ -16,6 +19,7 @@ const {registerPendingOpen} = require('./p2p');
|
|
|
16
19
|
const {registerSignedOpen} = require('./p2p');
|
|
17
20
|
|
|
18
21
|
const {fromHex} = Transaction;
|
|
22
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
19
23
|
const interval = 1000;
|
|
20
24
|
const times = 60 * 10;
|
|
21
25
|
|
|
@@ -138,10 +142,22 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
|
|
|
138
142
|
}],
|
|
139
143
|
|
|
140
144
|
// Decode the unsigned PSBT
|
|
141
|
-
transaction: [
|
|
145
|
+
transaction: [
|
|
146
|
+
'ecp',
|
|
147
|
+
'propose',
|
|
148
|
+
'register',
|
|
149
|
+
({ecp, propose, register}, cbk) =>
|
|
150
|
+
{
|
|
151
|
+
const funding = hexAsBuffer(propose.funding);
|
|
142
152
|
const psbt = decodePsbt({ecp, psbt: register.psbt});
|
|
143
153
|
|
|
144
|
-
|
|
154
|
+
const tx = fromHex(psbt.unsigned_transaction);
|
|
155
|
+
|
|
156
|
+
return cbk(null, {
|
|
157
|
+
id: tx.getId(),
|
|
158
|
+
raw: psbt.unsigned_transaction,
|
|
159
|
+
vout: tx.outs.findIndex(n => n.script.equals(funding)),
|
|
160
|
+
});
|
|
145
161
|
}],
|
|
146
162
|
|
|
147
163
|
// Confirm the incoming channel
|
|
@@ -150,7 +166,7 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
|
|
|
150
166
|
'partners',
|
|
151
167
|
'register',
|
|
152
168
|
'transaction',
|
|
153
|
-
({ecp, partners, register, transaction}, cbk) =>
|
|
169
|
+
asyncReflect(({ecp, partners, register, transaction}, cbk) =>
|
|
154
170
|
{
|
|
155
171
|
// Make sure that there is an inbound channel
|
|
156
172
|
return asyncRetry({interval, times}, cbk => {
|
|
@@ -164,12 +180,45 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
|
|
|
164
180
|
cbk);
|
|
165
181
|
},
|
|
166
182
|
cbk);
|
|
183
|
+
})],
|
|
184
|
+
|
|
185
|
+
// Clean up funding pending channels when the channel group fails
|
|
186
|
+
clean: [
|
|
187
|
+
'incoming',
|
|
188
|
+
'propose',
|
|
189
|
+
'register',
|
|
190
|
+
'transaction',
|
|
191
|
+
({incoming, propose, register, transaction}, cbk) =>
|
|
192
|
+
{
|
|
193
|
+
// Exit early when incoming channel is seen
|
|
194
|
+
if (!incoming.error) {
|
|
195
|
+
return cbk();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// When there was no incoming channel detected, fail and clean up
|
|
199
|
+
return deletePendingChannel({
|
|
200
|
+
lnd,
|
|
201
|
+
confirmed_transaction: register.conflict,
|
|
202
|
+
pending_transaction: transaction.raw,
|
|
203
|
+
pending_transaction_vout: transaction.vout,
|
|
204
|
+
},
|
|
205
|
+
err => {
|
|
206
|
+
if (!!err) {
|
|
207
|
+
return cbk([503, 'UnexpectedErrorCleaningUpGroupChannel', {err}]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Pass back the original error
|
|
211
|
+
return cbk(incoming.error);
|
|
212
|
+
});
|
|
167
213
|
}],
|
|
168
214
|
|
|
169
215
|
// Publish partial signatures to coordinator
|
|
170
|
-
reveal: ['incoming', 'register', ({register}, cbk) => {
|
|
216
|
+
reveal: ['clean', 'incoming', 'register', ({register}, cbk) => {
|
|
171
217
|
// Let listeners know that the signature will be sent to coordinator
|
|
172
|
-
emitter.emit('publishing', {
|
|
218
|
+
emitter.emit('publishing', {
|
|
219
|
+
refund: register.conflict,
|
|
220
|
+
signed: register.psbt,
|
|
221
|
+
});
|
|
173
222
|
|
|
174
223
|
return registerSignedOpen({
|
|
175
224
|
coordinator,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncReflect = require('async/reflect');
|
|
2
3
|
const asyncRetry = require('async/retry');
|
|
4
|
+
const {cancelPendingChannel} = require('ln-service');
|
|
3
5
|
const {connectPeer} = require('ln-sync');
|
|
4
6
|
const {decodePsbt} = require('psbt');
|
|
5
7
|
const {returnResult} = require('asyncjs-util');
|
|
@@ -46,7 +48,8 @@ const typeGroupChannelId = '1';
|
|
|
46
48
|
|
|
47
49
|
@returns via cbk or Promise
|
|
48
50
|
{
|
|
49
|
-
|
|
51
|
+
conflict: <Conflict Transaction Hex String>
|
|
52
|
+
psbt: <Partially Signed PSBT Hex String>
|
|
50
53
|
}
|
|
51
54
|
*/
|
|
52
55
|
module.exports = (args, cbk) => {
|
|
@@ -94,7 +97,7 @@ module.exports = (args, cbk) => {
|
|
|
94
97
|
}],
|
|
95
98
|
|
|
96
99
|
// Send connection confirmation request
|
|
97
|
-
request: ['connect', ({}, cbk) => {
|
|
100
|
+
request: ['connect', asyncReflect(({}, cbk) => {
|
|
98
101
|
const {records} = encodePendingProposal({
|
|
99
102
|
change: args.change,
|
|
100
103
|
funding: args.funding,
|
|
@@ -145,17 +148,34 @@ module.exports = (args, cbk) => {
|
|
|
145
148
|
});
|
|
146
149
|
},
|
|
147
150
|
cbk);
|
|
151
|
+
})],
|
|
152
|
+
|
|
153
|
+
// Clean up the pending channel if registration fails
|
|
154
|
+
clean: ['request', ({request}, cbk) => {
|
|
155
|
+
// Exit early when there was no error registering the pending channel
|
|
156
|
+
if (!request.error) {
|
|
157
|
+
return cbk();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return cancelPendingChannel({id: args.pending, lnd: args.lnd}, err => {
|
|
161
|
+
if (!!err) {
|
|
162
|
+
return cbk([503, 'UnexpectedErrorCleaningPendingChannel', {err}]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Return the original registration error
|
|
166
|
+
return cbk(request.error);
|
|
167
|
+
});
|
|
148
168
|
}],
|
|
149
169
|
|
|
150
170
|
// Check the unsigned funding transaction represents the partial open
|
|
151
|
-
check: ['ecp', 'request', ({ecp, request}, cbk) => {
|
|
171
|
+
check: ['clean', 'ecp', 'request', ({ecp, request}, cbk) => {
|
|
152
172
|
try {
|
|
153
|
-
decodePsbt({ecp, psbt: request});
|
|
173
|
+
decodePsbt({ecp, psbt: request.value});
|
|
154
174
|
} catch (err) {
|
|
155
175
|
return cbk([503, 'ExpectedValidUnsignedResponsePsbt', {err}]);
|
|
156
176
|
}
|
|
157
177
|
|
|
158
|
-
const psbt = decodePsbt({ecp, psbt: request});
|
|
178
|
+
const psbt = decodePsbt({ecp, psbt: request.value});
|
|
159
179
|
|
|
160
180
|
const tx = fromHex(psbt.unsigned_transaction);
|
|
161
181
|
|
|
@@ -207,7 +227,7 @@ module.exports = (args, cbk) => {
|
|
|
207
227
|
return signAndFundGroupChannel({
|
|
208
228
|
id: args.pending,
|
|
209
229
|
lnd: args.lnd,
|
|
210
|
-
psbt: request,
|
|
230
|
+
psbt: request.value,
|
|
211
231
|
utxos: args.utxos,
|
|
212
232
|
},
|
|
213
233
|
cbk);
|
package/package.json
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"bolt07": "1.8.2",
|
|
16
16
|
"ecpair": "2.0.1",
|
|
17
17
|
"goldengate": "11.2.3",
|
|
18
|
-
"invoices": "2.0
|
|
18
|
+
"invoices": "2.1.0",
|
|
19
19
|
"ln-service": "53.17.4",
|
|
20
20
|
"ln-sync": "3.13.0",
|
|
21
|
-
"psbt": "2.
|
|
21
|
+
"psbt": "2.7.0",
|
|
22
22
|
"p2tr": "1.3.1",
|
|
23
23
|
"tiny-secp256k1": "2.2.1"
|
|
24
24
|
},
|
|
@@ -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.19.1"
|
|
51
51
|
}
|
|
@@ -90,6 +90,7 @@ const v1AddressWords = key => [].concat(1).concat(bech32m.toWords(key));
|
|
|
90
90
|
recovery: <Swap Request Recovery Hex String>
|
|
91
91
|
[request]: <Request Function>
|
|
92
92
|
response: <Swap Response Hex String>
|
|
93
|
+
[sweep_address]: <Sweep Chain Address String>
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
@returns via cbk or Promise
|
|
@@ -134,6 +135,11 @@ module.exports = (args, cbk) => {
|
|
|
134
135
|
|
|
135
136
|
// Create a sweep address
|
|
136
137
|
createAddress: ['validate', ({}, cbk) => {
|
|
138
|
+
// Exit early when there is no need to create a sweep address
|
|
139
|
+
if (!!args.sweep_address) {
|
|
140
|
+
return cbk(null, {address: args.sweep_address});
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
return createChainAddress({lnd: args.lnd}, cbk);
|
|
138
144
|
}],
|
|
139
145
|
|
|
@@ -204,6 +210,7 @@ module.exports = (args, cbk) => {
|
|
|
204
210
|
coop_public_key: decoded.coop_public_key,
|
|
205
211
|
deposit_mtokens: decoded.deposit_mtokens,
|
|
206
212
|
deposit_payment: decoded.deposit_payment,
|
|
213
|
+
incoming_peer: decoded.incoming_peer,
|
|
207
214
|
push: decoded.push,
|
|
208
215
|
refund_public_key: decoded.refund_public_key,
|
|
209
216
|
request: decoded.request,
|
|
@@ -238,7 +245,10 @@ module.exports = (args, cbk) => {
|
|
|
238
245
|
const request = responseDetails.request;
|
|
239
246
|
|
|
240
247
|
if (!!args.is_external_funding) {
|
|
241
|
-
args.emitter.emit('update', {
|
|
248
|
+
args.emitter.emit('update', {
|
|
249
|
+
external_funding_pay_to_fund_swap_offchain: request,
|
|
250
|
+
must_be_in_through: responseDetails.incoming_peer || undefined,
|
|
251
|
+
});
|
|
242
252
|
|
|
243
253
|
return cbk();
|
|
244
254
|
}
|
|
@@ -247,6 +257,7 @@ module.exports = (args, cbk) => {
|
|
|
247
257
|
|
|
248
258
|
return pay({
|
|
249
259
|
request,
|
|
260
|
+
incoming_peer: responseDetails.incoming_peer || undefined,
|
|
250
261
|
lnd: args.lnd,
|
|
251
262
|
max_fee: args.max_fee_funding,
|
|
252
263
|
},
|
|
@@ -381,6 +392,7 @@ module.exports = (args, cbk) => {
|
|
|
381
392
|
cltv_delta: to.cltv_delta,
|
|
382
393
|
destination: to.destination,
|
|
383
394
|
features: to.features,
|
|
395
|
+
incoming_peer: requestDetails.incoming_peer || undefined,
|
|
384
396
|
lnd: args.lnd,
|
|
385
397
|
max_fee: args.max_fee_deposit,
|
|
386
398
|
messages: [{
|
|
@@ -412,7 +424,10 @@ module.exports = (args, cbk) => {
|
|
|
412
424
|
|
|
413
425
|
const address = encodeAddress(prefix, v1AddressWords(key));
|
|
414
426
|
|
|
415
|
-
args.emitter.emit('update', {
|
|
427
|
+
args.emitter.emit('update', {
|
|
428
|
+
waiting_for_chain_funding: address,
|
|
429
|
+
required_confirmations: args.min_confirmations || defaultConfsCount,
|
|
430
|
+
});
|
|
416
431
|
|
|
417
432
|
if (!!args.request) {
|
|
418
433
|
return findDeposit({
|
|
@@ -14,6 +14,7 @@ const {diffieHellmanComputeSecret} = require('ln-service');
|
|
|
14
14
|
const {fundPsbt} = require('ln-service');
|
|
15
15
|
const {getChainFeeRate} = require('ln-service');
|
|
16
16
|
const {getChainTransactions} = require('ln-service');
|
|
17
|
+
const {getChannels} = require('ln-service');
|
|
17
18
|
const {getHeight} = require('ln-service');
|
|
18
19
|
const {getIdentity} = require('ln-service');
|
|
19
20
|
const {getInvoice} = require('ln-service');
|
|
@@ -159,6 +160,7 @@ module.exports = (args, cbk) => {
|
|
|
159
160
|
claim_solo_public_key: details.claim_solo_public_key,
|
|
160
161
|
hash: details.hash,
|
|
161
162
|
key_index: details.key_index,
|
|
163
|
+
incoming_peer: details.incoming_peer || undefined,
|
|
162
164
|
refund_coop_private_key: details.refund_coop_private_key,
|
|
163
165
|
refund_coop_private_key_hash: details.refund_coop_private_key_hash,
|
|
164
166
|
solo_private_key: details.refund_solo_private_key,
|
|
@@ -170,6 +172,19 @@ module.exports = (args, cbk) => {
|
|
|
170
172
|
}
|
|
171
173
|
}],
|
|
172
174
|
|
|
175
|
+
// Lookup channel ids with the incoming peer when applicable
|
|
176
|
+
getChannels: ['recoveryDetails', ({recoveryDetails}, cbk) => {
|
|
177
|
+
if (!recoveryDetails.incoming_peer) {
|
|
178
|
+
return cbk();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return getChannels({
|
|
182
|
+
lnd: args.lnd,
|
|
183
|
+
partner_public_key: recoveryDetails.incoming_peer,
|
|
184
|
+
},
|
|
185
|
+
cbk);
|
|
186
|
+
}],
|
|
187
|
+
|
|
173
188
|
// Get the refund key
|
|
174
189
|
getRefundKey: [
|
|
175
190
|
'ecp',
|
|
@@ -213,6 +228,7 @@ module.exports = (args, cbk) => {
|
|
|
213
228
|
return;
|
|
214
229
|
}
|
|
215
230
|
|
|
231
|
+
const channels = invoice.payments.map(n => n.in_channel);
|
|
216
232
|
const hash = recoveryDetails.claim_coop_public_key_hash;
|
|
217
233
|
const timeout = min(...invoice.payments.map(n => n.timeout));
|
|
218
234
|
|
|
@@ -231,7 +247,11 @@ module.exports = (args, cbk) => {
|
|
|
231
247
|
|
|
232
248
|
args.emitter.emit('update', {execution_payment_held: id});
|
|
233
249
|
|
|
234
|
-
return cbk(null, {
|
|
250
|
+
return cbk(null, {
|
|
251
|
+
channels,
|
|
252
|
+
delta,
|
|
253
|
+
claim_coop_public_key: message.value,
|
|
254
|
+
});
|
|
235
255
|
});
|
|
236
256
|
}],
|
|
237
257
|
|
|
@@ -257,13 +277,14 @@ module.exports = (args, cbk) => {
|
|
|
257
277
|
|
|
258
278
|
args.emitter.emit('update', {offchain_funding_held: id});
|
|
259
279
|
|
|
280
|
+
const channels = invoice.payments.map(n => n.in_channel);
|
|
260
281
|
const timeout = min(...invoice.payments.map(n => n.timeout));
|
|
261
282
|
|
|
262
283
|
const delta = timeout - recoveryDetails.timeout;
|
|
263
284
|
|
|
264
285
|
sub.removeAllListeners();
|
|
265
286
|
|
|
266
|
-
return cbk(null, {delta});
|
|
287
|
+
return cbk(null, {channels, delta});
|
|
267
288
|
});
|
|
268
289
|
|
|
269
290
|
return;
|
|
@@ -423,12 +444,52 @@ module.exports = (args, cbk) => {
|
|
|
423
444
|
cbk);
|
|
424
445
|
}],
|
|
425
446
|
|
|
447
|
+
// Check incoming peer constraints
|
|
448
|
+
checkIncomingPeer: [
|
|
449
|
+
'getChannels',
|
|
450
|
+
'waitForDepositHold',
|
|
451
|
+
'waitForFundHold',
|
|
452
|
+
({getChannels, waitForDepositHold, waitForFundHold}, cbk) =>
|
|
453
|
+
{
|
|
454
|
+
// Exit early when there are no incoming peer constraints
|
|
455
|
+
if (!args.incoming_peer) {
|
|
456
|
+
return cbk();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const channels = getChannels.channels.map(n => n.id);
|
|
460
|
+
|
|
461
|
+
// The deposit must come in under a channel with the incoming peer
|
|
462
|
+
const invalidDeposit = waitForDepositHold.channels.find(id => {
|
|
463
|
+
return !channels.includes(id);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (!!invalidDeposit) {
|
|
467
|
+
return cbk([503, 'UnexpectedInboundChannelForHeldDeposit']);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// The funding must come in under a channel with the incoming peer
|
|
471
|
+
const invalidFunding = waitForFundHold.channels.find(id => {
|
|
472
|
+
return !channels.includes(id);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (!!invalidFunding) {
|
|
476
|
+
return cbk([503, 'UnexpectedInboundChannelForHeldFunding']);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return cbk();
|
|
480
|
+
}],
|
|
481
|
+
|
|
426
482
|
// Check that there are sufficient remaining blocks
|
|
427
483
|
checkTimeRemaining: [
|
|
428
484
|
'recoveryDetails',
|
|
429
485
|
'waitForDepositHold',
|
|
430
486
|
'waitForFundHold',
|
|
431
|
-
({
|
|
487
|
+
({
|
|
488
|
+
recoveryDetails,
|
|
489
|
+
waitForDepositHold,
|
|
490
|
+
waitForFundHold,
|
|
491
|
+
},
|
|
492
|
+
cbk) =>
|
|
432
493
|
{
|
|
433
494
|
if (waitForDepositHold.delta < defaultMinDelta) {
|
|
434
495
|
return cbk([503, 'InsufficientDepositDeltaBlocksToFundSwap']);
|
|
@@ -458,6 +519,7 @@ module.exports = (args, cbk) => {
|
|
|
458
519
|
|
|
459
520
|
// Lock chain funds to fund the swap with
|
|
460
521
|
lockFunding: [
|
|
522
|
+
'checkIncomingPeer',
|
|
461
523
|
'checkTimeRemaining',
|
|
462
524
|
'getTransactions',
|
|
463
525
|
'swap',
|
|
@@ -3,6 +3,7 @@ const {decodeTlvStream} = require('bolt01');
|
|
|
3
3
|
|
|
4
4
|
const {requestRecordsAsRequest} = require('./../records');
|
|
5
5
|
const {publicTypes} = require('./swap_field_types');
|
|
6
|
+
const {swapVersion} = require('./swap_field_types');
|
|
6
7
|
|
|
7
8
|
const decodeNumber = encoded => decodeBigSize({encoded}).decoded;
|
|
8
9
|
const findRecord = (records, type) => records.find(n => n.type === type);
|
|
@@ -13,6 +14,7 @@ const paymentNonceLength = 64;
|
|
|
13
14
|
const startIndex = 0;
|
|
14
15
|
const typeCoopPrivateKeyHash = publicTypes.typeRefundCoopPrivateKeyHash;
|
|
15
16
|
const {typeDeposit} = publicTypes;
|
|
17
|
+
const {typeInboundPeer} = publicTypes;
|
|
16
18
|
const {typePush} = publicTypes;
|
|
17
19
|
const {typeRefundCoopPublicKey} = publicTypes;
|
|
18
20
|
const {typeRefundSoloPublicKey} = publicTypes;
|
|
@@ -33,6 +35,7 @@ const {typeVersion} = publicTypes;
|
|
|
33
35
|
coop_public_key: <Refund Cooperative Key Hex String>
|
|
34
36
|
deposit_mtokens: <Deposit Amount Millitokens Number String>
|
|
35
37
|
deposit_payment: <Deposit Payment Nonce Hex String>
|
|
38
|
+
[incoming_peer]: <Constrained to Inbound Peer Public Key Id Hex String>
|
|
36
39
|
push: <Push Payment Nonce Hex String>
|
|
37
40
|
refund_public_key: <Refund Unilateral Public Key Hex String>
|
|
38
41
|
request: <BOLT 11 Encoded Funding Request String>
|
|
@@ -56,10 +59,22 @@ module.exports = ({network, response}) => {
|
|
|
56
59
|
|
|
57
60
|
const {records} = decodeTlvStream({encoded: response});
|
|
58
61
|
|
|
59
|
-
if (
|
|
62
|
+
if (!findRecord(records, typeVersion)) {
|
|
60
63
|
throw new Error('UnexpectedVersionOfOffToOnResponse');
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
const versionRecord = findRecord(records, typeVersion);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
decodeNumber(versionRecord.value);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error('ExpectedValidVersionRecordForOffToOnResponse');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Number(decodeNumber(versionRecord.value)) !== swapVersion) {
|
|
75
|
+
throw new Error('UnsupportedSwapVersionNumberForOffchainToOnchainSwap');
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
const coopPrivateKeyHashRecord = findRecord(records, typeCoopPrivateKeyHash);
|
|
64
79
|
|
|
65
80
|
if (!coopPrivateKeyHashRecord) {
|
|
@@ -106,6 +121,12 @@ module.exports = ({network, response}) => {
|
|
|
106
121
|
throw new Error('ExpectedSmallerDepositAmountInOffToOnResponse');
|
|
107
122
|
}
|
|
108
123
|
|
|
124
|
+
const inboundRecord = findRecord(records, typeInboundPeer);
|
|
125
|
+
|
|
126
|
+
if (!!inboundRecord && !isPublicKey(inboundRecord.value)) {
|
|
127
|
+
throw new Error('ExpectectedPublicKeyForInboundPeerConstraint');
|
|
128
|
+
}
|
|
129
|
+
|
|
109
130
|
const pushRecord = findRecord(records, typePush);
|
|
110
131
|
|
|
111
132
|
if (!pushRecord) {
|
|
@@ -150,6 +171,7 @@ module.exports = ({network, response}) => {
|
|
|
150
171
|
coop_public_key: coopPublicKeyRecord.value,
|
|
151
172
|
deposit_mtokens: decodeBigSize({encoded: depositAmount}).decoded,
|
|
152
173
|
deposit_payment: depositRecord.value.slice(startIndex, paymentNonceLength),
|
|
174
|
+
incoming_peer: !!inboundRecord ? inboundRecord.value : undefined,
|
|
153
175
|
push: pushRecord.value,
|
|
154
176
|
refund_public_key: soloPublicKeyRecord.value,
|
|
155
177
|
request: funding.request,
|
|
@@ -5,6 +5,7 @@ const {decodeTlvStream} = require('bolt01');
|
|
|
5
5
|
|
|
6
6
|
const decodeSwapSecrets = require('./decode_swap_secrets');
|
|
7
7
|
const {publicTypes} = require('./swap_field_types');
|
|
8
|
+
const {swapVersion} = require('./swap_field_types');
|
|
8
9
|
|
|
9
10
|
const decodeNumber = encoded => decodeBigSize({encoded}).decoded;
|
|
10
11
|
const findRecord = (records, type) => records.find(n => n.type === type);
|
|
@@ -16,6 +17,7 @@ const sha256 = preimage => createHash('sha256').update(preimage).digest('hex');
|
|
|
16
17
|
const {typeClaimCoopPublicKeyHash} = publicTypes;
|
|
17
18
|
const {typeClaimSoloPublicKey} = publicTypes;
|
|
18
19
|
const {typeHash} = publicTypes;
|
|
20
|
+
const {typeInboundPeer} = publicTypes;
|
|
19
21
|
const {typePrivateRefundDetails} = publicTypes;
|
|
20
22
|
const {typeRefundCoopPrivateKeyHash} = publicTypes;
|
|
21
23
|
const {typeTimeout} = publicTypes;
|
|
@@ -60,8 +62,17 @@ module.exports = ({decrypt, recovery}) => {
|
|
|
60
62
|
|
|
61
63
|
const {records} = decodeTlvStream({encoded: recovery});
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
// Make sure the recovery records are for the known swap version
|
|
66
|
+
const versionRecord = findRecord(records, typeVersion);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
decodeNumber(versionRecord.value);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error('ExpectedValidVersionRecordForOffToOnRecovery');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Number(decodeNumber(versionRecord.value)) !== swapVersion) {
|
|
75
|
+
throw new Error('UnsupportedSwapVersionNumberForOffToOnRecovery');
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
const claimCoopPubKeyHash = findRecord(records, typeClaimCoopPublicKeyHash);
|
|
@@ -74,6 +85,12 @@ module.exports = ({decrypt, recovery}) => {
|
|
|
74
85
|
throw new Error('ExpectedValidClaimCoopPublicKeyHash');
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
const inboundRecord = findRecord(records, typeInboundPeer);
|
|
89
|
+
|
|
90
|
+
if (!!inboundRecord && !isPublicKey(inboundRecord.value)) {
|
|
91
|
+
throw new Error('ExpectectedPublicKeyForInboundPeerConstraintInRecovery');
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
const claimSoloPublicKeyRecord = findRecord(records, typeClaimSoloPublicKey);
|
|
78
95
|
|
|
79
96
|
if (!claimSoloPublicKeyRecord) {
|
|
@@ -5,6 +5,7 @@ const {parsePaymentRequest} = require('ln-service');
|
|
|
5
5
|
const encodeSwapSecrets = require('./encode_swap_secrets');
|
|
6
6
|
const {requestAsRequestRecords} = require('./../records');
|
|
7
7
|
const {publicTypes} = require('./swap_field_types');
|
|
8
|
+
const {swapVersion} = require('./swap_field_types');
|
|
8
9
|
|
|
9
10
|
const encode = records => encodeTlvStream({records}).encoded;
|
|
10
11
|
const encodeNumber = n => encodeBigSize({number: n.toString()}).encoded;
|
|
@@ -13,6 +14,7 @@ const {typeClaimCoopPublicKeyHash} = publicTypes;
|
|
|
13
14
|
const {typeClaimSoloPublicKey} = publicTypes;
|
|
14
15
|
const {typeDeposit} = publicTypes;
|
|
15
16
|
const {typeHash} = publicTypes;
|
|
17
|
+
const {typeInboundPeer} = publicTypes;
|
|
16
18
|
const {typePush} = publicTypes;
|
|
17
19
|
const {typePrivateRefundDetails} = publicTypes;
|
|
18
20
|
const {typeRefundCoopPrivateKeyHash} = publicTypes;
|
|
@@ -21,6 +23,7 @@ const {typeRefundSoloPublicKey} = publicTypes;
|
|
|
21
23
|
const {typeRequest} = publicTypes;
|
|
22
24
|
const {typeTimeout} = publicTypes;
|
|
23
25
|
const {typeTokens} = publicTypes;
|
|
26
|
+
const {typeVersion} = publicTypes;
|
|
24
27
|
|
|
25
28
|
/** Serialize off to on swap records
|
|
26
29
|
|
|
@@ -32,6 +35,7 @@ const {typeTokens} = publicTypes;
|
|
|
32
35
|
deposit: <Deposit BOLT 11 Request String>
|
|
33
36
|
encrypt: <Encrypt Secrets Base Encryption Key Hex String>
|
|
34
37
|
hash: <Swap Hash Hex String>
|
|
38
|
+
[incoming_peer]: <Constrained to Inbound Peer Public Key Id Hex String>
|
|
35
39
|
[key_index]: <Refund Unilateral Key Id Number>
|
|
36
40
|
push: <BOLT 11 Encoded Push Request String>
|
|
37
41
|
refund_public_key: <Refund Unilateral Public Key Hex String>
|
|
@@ -61,6 +65,10 @@ module.exports = args => {
|
|
|
61
65
|
type: typeDeposit,
|
|
62
66
|
value: deposit.payment + depositAmount,
|
|
63
67
|
},
|
|
68
|
+
{
|
|
69
|
+
type: typeInboundPeer,
|
|
70
|
+
value: args.incoming_peer,
|
|
71
|
+
},
|
|
64
72
|
{
|
|
65
73
|
type: typeRefundCoopPrivateKeyHash,
|
|
66
74
|
value: privateCoopKeyHash,
|
|
@@ -85,6 +93,10 @@ module.exports = args => {
|
|
|
85
93
|
type: typeTimeout,
|
|
86
94
|
value: timeout,
|
|
87
95
|
},
|
|
96
|
+
{
|
|
97
|
+
type: typeVersion,
|
|
98
|
+
value: encodeNumber(swapVersion),
|
|
99
|
+
},
|
|
88
100
|
];
|
|
89
101
|
|
|
90
102
|
const {encoded} = encodeSwapSecrets({
|
|
@@ -108,6 +120,10 @@ module.exports = args => {
|
|
|
108
120
|
type: typeHash,
|
|
109
121
|
value: swapHash,
|
|
110
122
|
},
|
|
123
|
+
{
|
|
124
|
+
type: typeInboundPeer,
|
|
125
|
+
value: args.incoming_peer,
|
|
126
|
+
},
|
|
111
127
|
{
|
|
112
128
|
type: typePrivateRefundDetails,
|
|
113
129
|
value: encoded,
|
|
@@ -124,10 +140,14 @@ module.exports = args => {
|
|
|
124
140
|
type: typeTokens,
|
|
125
141
|
value: encodeNumber(args.tokens),
|
|
126
142
|
},
|
|
143
|
+
{
|
|
144
|
+
type: typeVersion,
|
|
145
|
+
value: encodeNumber(swapVersion),
|
|
146
|
+
},
|
|
127
147
|
];
|
|
128
148
|
|
|
129
149
|
return {
|
|
130
|
-
recovery: encode(recoveryRecords),
|
|
131
|
-
response: encode(responseRecords),
|
|
150
|
+
recovery: encode(recoveryRecords.filter(n => !!n.value)),
|
|
151
|
+
response: encode(responseRecords.filter(n => !!n.value)),
|
|
132
152
|
};
|
|
133
153
|
};
|
package/swaps/manage_swap.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const {address} = require('bitcoinjs-lib');
|
|
1
2
|
const asyncAuto = require('async/auto');
|
|
2
3
|
const asyncMap = require('async/map');
|
|
3
4
|
const {findKey} = require('ln-sync');
|
|
@@ -6,6 +7,7 @@ const {getAllInvoices} = require('ln-sync');
|
|
|
6
7
|
const {getInvoice} = require('ln-service');
|
|
7
8
|
const {getNetwork} = require('ln-sync');
|
|
8
9
|
const {getNodeAlias} = require('ln-sync');
|
|
10
|
+
const {networks} = require('bitcoinjs-lib');
|
|
9
11
|
const {returnResult} = require('asyncjs-util');
|
|
10
12
|
|
|
11
13
|
const decodeOffToOnRequest = require('./decode_off_to_on_request');
|
|
@@ -25,6 +27,7 @@ const pushesReceivedAfter = () => new Date(Date.now() - 1000 * 60 * 60 * 24);
|
|
|
25
27
|
const recoverRespondAction = 'recover-response';
|
|
26
28
|
const requestAction = 'request';
|
|
27
29
|
const respondAction = 'respond';
|
|
30
|
+
const {toOutputScript} = address;
|
|
28
31
|
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
|
|
29
32
|
const typeKeySendTrade = '805805';
|
|
30
33
|
|
|
@@ -260,14 +263,57 @@ module.exports = (args, cbk) => {
|
|
|
260
263
|
}
|
|
261
264
|
}],
|
|
262
265
|
|
|
266
|
+
// Ask if the sweep address should be custom
|
|
267
|
+
askForSweepAddress: [
|
|
268
|
+
'askForExternal',
|
|
269
|
+
'getNetwork',
|
|
270
|
+
'selectAction',
|
|
271
|
+
({getNetwork, selectAction}, cbk) =>
|
|
272
|
+
{
|
|
273
|
+
switch (selectAction) {
|
|
274
|
+
case actionPushRequest:
|
|
275
|
+
case requestAction:
|
|
276
|
+
return args.ask({
|
|
277
|
+
message: 'Send on-chain funds to an external address? (Optional)',
|
|
278
|
+
name: 'sweep',
|
|
279
|
+
type: 'input',
|
|
280
|
+
validate: input => {
|
|
281
|
+
if (!input) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
return !!toOutputScript(input, networks[getNetwork.bitcoinjs]);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
return 'Unsupported on-chain address format';
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
({sweep}) => cbk(null, sweep));
|
|
293
|
+
|
|
294
|
+
default:
|
|
295
|
+
// Custom sweep address not supported
|
|
296
|
+
return cbk();
|
|
297
|
+
}
|
|
298
|
+
}],
|
|
299
|
+
|
|
263
300
|
// Make swap request
|
|
264
301
|
makeRequest: [
|
|
265
302
|
'askForExternal',
|
|
266
303
|
'askForRemote',
|
|
304
|
+
'askForSweepAddress',
|
|
267
305
|
'findKey',
|
|
268
306
|
'hasTr',
|
|
269
307
|
'selectAction',
|
|
270
|
-
({
|
|
308
|
+
({
|
|
309
|
+
askForExternal,
|
|
310
|
+
askForRemote,
|
|
311
|
+
askForSweepAddress,
|
|
312
|
+
findKey,
|
|
313
|
+
hasTr,
|
|
314
|
+
selectAction,
|
|
315
|
+
},
|
|
316
|
+
cbk) =>
|
|
271
317
|
{
|
|
272
318
|
switch (selectAction) {
|
|
273
319
|
case actionPushRequest:
|
|
@@ -291,6 +337,7 @@ module.exports = (args, cbk) => {
|
|
|
291
337
|
push_to: findKey.public_key,
|
|
292
338
|
min_confirmations: minConfirmations[getNetwork.network],
|
|
293
339
|
request: !hasTr ? args.request : undefined,
|
|
340
|
+
sweep_address: askForSweepAddress || undefined,
|
|
294
341
|
},
|
|
295
342
|
cbk);
|
|
296
343
|
}],
|
|
@@ -51,6 +51,7 @@ const typeSwapResponse = serviceTypes.serviceTypeSwapResponse;
|
|
|
51
51
|
[min_confirmations]: <Minimum Confirmations to Wait Number>
|
|
52
52
|
[push_to]: <Push Swap Request to Node with Identity Public Key Hex String>
|
|
53
53
|
[request]: <Request Function>
|
|
54
|
+
[sweep_address]: <Sweep Chain Address String>
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
@returns via cbk or Promise
|
|
@@ -329,6 +330,7 @@ module.exports = (args, cbk) => {
|
|
|
329
330
|
});
|
|
330
331
|
|
|
331
332
|
const deposit = mtokensAsTokens(response.deposit_mtokens);
|
|
333
|
+
const inbound = response.incoming_peer;
|
|
332
334
|
const timeout = `that times out at ${response.timeout}`;
|
|
333
335
|
const {tokens} = parsePaymentRequest({request: response.request});
|
|
334
336
|
|
|
@@ -340,9 +342,11 @@ module.exports = (args, cbk) => {
|
|
|
340
342
|
return cbk(null, true)
|
|
341
343
|
}
|
|
342
344
|
|
|
345
|
+
const inPeer = !!args.is_external_funding ? ` in via ${inbound}` : '';
|
|
346
|
+
|
|
343
347
|
return args.ask({
|
|
344
348
|
default: true,
|
|
345
|
-
message: `Start swap ${timeout}? ${pricing}?`,
|
|
349
|
+
message: `Start swap ${timeout}? ${pricing}${inPeer}?`,
|
|
346
350
|
name: 'ok',
|
|
347
351
|
type: 'confirm',
|
|
348
352
|
},
|
|
@@ -378,6 +382,7 @@ module.exports = (args, cbk) => {
|
|
|
378
382
|
recovery: makeRequest.recovery,
|
|
379
383
|
request: args.request,
|
|
380
384
|
response: getResponse,
|
|
385
|
+
sweep_address: args.sweep_address,
|
|
381
386
|
},
|
|
382
387
|
cbk);
|
|
383
388
|
}],
|
|
@@ -2,6 +2,7 @@ const {createHash} = require('crypto');
|
|
|
2
2
|
const EventEmitter = require('events');
|
|
3
3
|
|
|
4
4
|
const asyncAuto = require('async/auto');
|
|
5
|
+
const {findKey} = require('ln-sync');
|
|
5
6
|
const {getChainFeeRate} = require('ln-service');
|
|
6
7
|
const {returnResult} = require('asyncjs-util');
|
|
7
8
|
|
|
@@ -158,6 +159,24 @@ module.exports = ({ask, lnd, logger, request, swap, to}, cbk) => {
|
|
|
158
159
|
({target}) => cbk(null, Number(target)));
|
|
159
160
|
}],
|
|
160
161
|
|
|
162
|
+
// Ask for an incoming peer to constrain the swap to
|
|
163
|
+
askForIncoming: ['askForTarget', ({}, cbk) => {
|
|
164
|
+
return ask({
|
|
165
|
+
message: 'Require off-chain through a specific peer? (Optional)',
|
|
166
|
+
name: 'incoming',
|
|
167
|
+
},
|
|
168
|
+
({incoming}) => cbk(null, incoming));
|
|
169
|
+
}],
|
|
170
|
+
|
|
171
|
+
// Find the incoming identity key when specified
|
|
172
|
+
findIncoming: ['askForIncoming', ({askForIncoming}, cbk) => {
|
|
173
|
+
if (!askForIncoming) {
|
|
174
|
+
return cbk(null, {});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return findKey({lnd, query: askForIncoming}, cbk);
|
|
178
|
+
}],
|
|
179
|
+
|
|
161
180
|
// Get the chain fee rate
|
|
162
181
|
getRate: ['askForTarget', ({askForTarget}, cbk) => {
|
|
163
182
|
return getChainFeeRate({lnd, confirmation_target: askForTarget}, cbk);
|
|
@@ -167,15 +186,21 @@ module.exports = ({ask, lnd, logger, request, swap, to}, cbk) => {
|
|
|
167
186
|
makeResponse: [
|
|
168
187
|
'askForRate',
|
|
169
188
|
'askForRequest',
|
|
189
|
+
'findIncoming',
|
|
170
190
|
'getRate',
|
|
171
|
-
({askForRate, askForRequest, getRate}, cbk) =>
|
|
191
|
+
({askForRate, askForRequest, findIncoming, getRate}, cbk) =>
|
|
172
192
|
{
|
|
173
193
|
const {tokens} = decodeOffToOnRequest({request: askForRequest});
|
|
174
194
|
|
|
195
|
+
if (!!findIncoming.public_key) {
|
|
196
|
+
logger.info({incoming_peer_constraint: findIncoming.public_key});
|
|
197
|
+
}
|
|
198
|
+
|
|
175
199
|
return startOnToOffSwap({
|
|
176
200
|
lnd,
|
|
177
201
|
delta: defaultCltvDelta,
|
|
178
202
|
deposit: ceil(getRate.tokens_per_vbyte * estimatedVirtualSize),
|
|
203
|
+
incoming_peer: findIncoming.public_key || undefined,
|
|
179
204
|
is_external_solo_key: !!request,
|
|
180
205
|
price: floor(tokens * askForRate / rateDenominator),
|
|
181
206
|
request: askForRequest,
|
|
@@ -29,6 +29,7 @@ const sha256 = preimage => createHash('sha256').update(preimage).digest('hex');
|
|
|
29
29
|
{
|
|
30
30
|
delta: <Swap CLTV Delta Number>
|
|
31
31
|
deposit: <Unilateral Deposit Tokens Number>
|
|
32
|
+
[incoming_peer]: <Constrained to Inbound Peer Public Key Id Hex String>
|
|
32
33
|
[is_external_solo_key]: <Use External Unilateral Refund Key Bool>
|
|
33
34
|
lnd: <Authenticated LND API Object>
|
|
34
35
|
price: <Swap Price Tokens Number>
|
|
@@ -227,6 +228,7 @@ module.exports = (args, cbk) => {
|
|
|
227
228
|
deposit: createExecInvoice.request,
|
|
228
229
|
encrypt: getEncrypt.secret,
|
|
229
230
|
hash: requestDetails.hash,
|
|
231
|
+
incoming_peer: args.incoming_peer || undefined,
|
|
230
232
|
key_index: getRefundKey.index,
|
|
231
233
|
push: createPushInvoice.request,
|
|
232
234
|
refund_public_key: getRefundKey.public_key,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"typeSoloPrivateKey": "6"
|
|
9
9
|
},
|
|
10
10
|
"publicTypes": {
|
|
11
|
+
"typeVersion": "0",
|
|
11
12
|
"typeClaimCoopPublicKeyHash": "1",
|
|
12
13
|
"typeClaimSoloPublicKey": "2",
|
|
13
14
|
"typeDeposit": "3",
|
|
@@ -21,11 +22,12 @@
|
|
|
21
22
|
"typeRequest": "11",
|
|
22
23
|
"typeTimeout": "12",
|
|
23
24
|
"typeTokens": "13",
|
|
24
|
-
"
|
|
25
|
+
"typeInboundPeer": "14"
|
|
25
26
|
},
|
|
26
27
|
"pushTypes": {
|
|
27
28
|
"typeSwapId": "0",
|
|
28
29
|
"typeSwapResponse": "1"
|
|
29
30
|
},
|
|
31
|
+
"swapVersion": 1,
|
|
30
32
|
"typePayMetadata": "805001"
|
|
31
33
|
}
|
|
@@ -103,6 +103,10 @@ test(`Start offchain swap`, async ({end, equal, strictSame}) => {
|
|
|
103
103
|
return cbk({[args.name]: args.default});
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
if (args.name === 'incoming') {
|
|
107
|
+
return cbk({incoming: false});
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
if (args.name === 'req') {
|
|
107
111
|
return cbk({req: swapRequest.swap_request});
|
|
108
112
|
}
|
|
@@ -103,6 +103,10 @@ test(`Swap with claim path`, async ({end, equal, strictSame}) => {
|
|
|
103
103
|
return cbk({[args.name]: args.default});
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
if (args.name === 'incoming') {
|
|
107
|
+
return cbk({incoming: false});
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
if (args.name === 'req') {
|
|
107
111
|
return cbk({req: swapRequest.swap_request});
|
|
108
112
|
}
|
|
@@ -104,6 +104,10 @@ test(`Timeout a swap`, async ({end, equal, strictSame}) => {
|
|
|
104
104
|
return cbk({[args.name]: args.default});
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
if (args.name === 'incoming') {
|
|
108
|
+
return cbk({incoming: false});
|
|
109
|
+
}
|
|
110
|
+
|
|
107
111
|
if (args.name === 'req') {
|
|
108
112
|
return cbk({req: swapRequest.swap_request});
|
|
109
113
|
}
|
|
@@ -2,12 +2,15 @@ const {addPeer} = require('ln-service');
|
|
|
2
2
|
const asyncAuto = require('async/auto');
|
|
3
3
|
const asyncDetect = require('async/detect');
|
|
4
4
|
const asyncDetectSeries = require('async/detectSeries');
|
|
5
|
+
const asyncEach = require('async/each');
|
|
5
6
|
const asyncMap = require('async/map');
|
|
6
7
|
const asyncRetry = require('async/retry');
|
|
7
8
|
const {getChannel} = require('ln-service');
|
|
9
|
+
const {getChannels} = require('ln-service');
|
|
8
10
|
const {getIdentity} = require('ln-service');
|
|
9
11
|
const {getNode} = require('ln-service');
|
|
10
12
|
const {getPeers} = require('ln-service');
|
|
13
|
+
const {removePeer} = require('ln-service');
|
|
11
14
|
const {returnResult} = require('asyncjs-util');
|
|
12
15
|
|
|
13
16
|
const interval = 1000;
|
|
@@ -55,12 +58,14 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
|
|
|
55
58
|
return cbk();
|
|
56
59
|
},
|
|
57
60
|
|
|
61
|
+
// Get the channels to see if this is a channel peer
|
|
62
|
+
getChannels: ['validate', ({}, cbk) => {
|
|
63
|
+
return getChannels({lnd, is_active: true}, cbk);
|
|
64
|
+
}],
|
|
65
|
+
|
|
58
66
|
// Derive the self public key
|
|
59
67
|
getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
|
|
60
68
|
|
|
61
|
-
// Get the list of connected peers
|
|
62
|
-
getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
|
|
63
|
-
|
|
64
69
|
// Find node connect info to connect to
|
|
65
70
|
getNodes: ['getIdentity', ({getIdentity}, cbk) => {
|
|
66
71
|
return asyncMap(nodes, (connect, cbk) => {
|
|
@@ -135,8 +140,56 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
|
|
|
135
140
|
cbk);
|
|
136
141
|
}],
|
|
137
142
|
|
|
143
|
+
// Remove the peer if there is no channel peer to avoid stale peers
|
|
144
|
+
removePeer: [
|
|
145
|
+
'getChannels',
|
|
146
|
+
'getNodes',
|
|
147
|
+
({getChannels, getNodes}, cbk) =>
|
|
148
|
+
{
|
|
149
|
+
const ids = getChannels.channels.map(n => n.partner_public_key);
|
|
150
|
+
|
|
151
|
+
return asyncEach(getNodes, (node, cbk) => {
|
|
152
|
+
// Exit early when there is no node id
|
|
153
|
+
if (!node || !node.id || ids.includes(node.id)) {
|
|
154
|
+
return cbk();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return removePeer({lnd, public_key: node.id}, (err, res) => {
|
|
158
|
+
if (!!err) {
|
|
159
|
+
return cbk(err);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return asyncRetry({interval, times}, cbk => {
|
|
163
|
+
return getPeers({lnd}, (err, res) => {
|
|
164
|
+
if (!!err) {
|
|
165
|
+
return cbk(err);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const peers = res.peers.map(n => n.public_key);
|
|
169
|
+
|
|
170
|
+
if (!!peers.includes(node.id)) {
|
|
171
|
+
return cbk([503, 'FailedToDisconnectSellerPeer']);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return cbk();
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
cbk);
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
cbk);
|
|
181
|
+
}],
|
|
182
|
+
|
|
183
|
+
// Get the list of connected peers
|
|
184
|
+
getPeers: ['removePeer', ({}, cbk) => getPeers({lnd}, cbk)],
|
|
185
|
+
|
|
138
186
|
// Try and connect to a node in order to do p2p messaging
|
|
139
|
-
connect: [
|
|
187
|
+
connect: [
|
|
188
|
+
'getNodes',
|
|
189
|
+
'getPeers',
|
|
190
|
+
'removePeer',
|
|
191
|
+
({getNodes, getPeers}, cbk) =>
|
|
192
|
+
{
|
|
140
193
|
const connected = getPeers.peers.map(n => n.public_key);
|
|
141
194
|
|
|
142
195
|
// Look for a node that is already a connected peer
|