paid-services 3.19.2 → 3.20.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 +12 -0
- package/groups/ask_for_group_details.js +2 -2
- package/groups/ask_to_confirm_group_join.js +1 -1
- package/groups/assemble/assemble_unsigned_psbt.js +3 -2
- package/groups/assemble/coordinate_group.js +6 -1
- package/groups/assemble_channel_group.js +12 -1
- package/groups/funding/confirm_incoming_channel.js +6 -5
- package/groups/funding/propose_group_channel.js +54 -11
- package/groups/funding/sign_and_fund_group_channel.js +11 -5
- package/groups/join_channel_group.js +12 -1
- package/groups/members/partners_from_members.js +7 -0
- package/groups/messages/decode_group_details.js +21 -2
- package/groups/messages/encode_group_details.js +3 -0
- package/groups/messages/encode_partners_records.js +1 -3
- package/groups/p2p/find_group_partners.js +21 -1
- package/groups/p2p/get_group_details.js +1 -0
- package/groups/p2p/register_pending_open.js +6 -5
- package/package.json +1 -1
- package/test/integration/test_group.js +1 -1
- package/test/integration/test_group_pair.js +145 -0
- package/trades/connect_to_seller.js +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## Version 3.20.2
|
|
4
|
+
|
|
5
|
+
- `manageGroupJoin`: Allow opening a pair channel when funds are below capacity
|
|
6
|
+
|
|
7
|
+
## Version 3.20.1
|
|
8
|
+
|
|
9
|
+
- `manageTrades`: Fix seller connection when not peered
|
|
10
|
+
|
|
11
|
+
## Version 3.20.0
|
|
12
|
+
|
|
13
|
+
- `manageGroupJoin`: Add support for 2 person groups
|
|
14
|
+
|
|
3
15
|
## Version 3.19.2
|
|
4
16
|
|
|
5
17
|
- `manageSwap`: Add support for inbound peer constraint
|
|
@@ -11,7 +11,7 @@ const isOdd = n => !!(n % 2);
|
|
|
11
11
|
const maxChannelSize = 21e14;
|
|
12
12
|
const minChannelSize = 2e4;
|
|
13
13
|
const maxGroupSize = 420;
|
|
14
|
-
const minGroupSize =
|
|
14
|
+
const minGroupSize = 2;
|
|
15
15
|
const {round} = Math;
|
|
16
16
|
|
|
17
17
|
/** Ask for new group details to create a group
|
|
@@ -150,7 +150,7 @@ module.exports = ({ask, lnd}, cbk) => {
|
|
|
150
150
|
{
|
|
151
151
|
return cbk(null, {
|
|
152
152
|
capacity: askForCapacity,
|
|
153
|
-
count: askForCount,
|
|
153
|
+
count: Number(askForCount),
|
|
154
154
|
rate: askForFeeRate,
|
|
155
155
|
});
|
|
156
156
|
}],
|
|
@@ -105,7 +105,7 @@ module.exports = ({ask, lnd}, cbk) => {
|
|
|
105
105
|
({getAlias, getBalance, getDetails}, cbk) =>
|
|
106
106
|
{
|
|
107
107
|
// Check to make sure that there are on chain funds for this group
|
|
108
|
-
if (getBalance.chain_balance < getDetails.
|
|
108
|
+
if (getBalance.chain_balance < getDetails.funding) {
|
|
109
109
|
return cbk([
|
|
110
110
|
400,
|
|
111
111
|
'InsufficientChainFundsAvailableToJoinGroup',
|
|
@@ -6,6 +6,7 @@ const tinysecp = require('tiny-secp256k1');
|
|
|
6
6
|
const {Transaction} = require('bitcoinjs-lib');
|
|
7
7
|
|
|
8
8
|
const {ceil} = Math;
|
|
9
|
+
const committed = (capacity, m) => m.length === 2 ? capacity / 2 : capacity;
|
|
9
10
|
const dummyEcdsaSignature = Buffer.alloc(74);
|
|
10
11
|
const dummyPublicKey = Buffer.alloc(33);
|
|
11
12
|
const dummySchnorrSignature = Buffer.alloc(64);
|
|
@@ -24,7 +25,7 @@ const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
|
|
|
24
25
|
capacity: <Channel Capacity Tokens Number>
|
|
25
26
|
proposed: [{
|
|
26
27
|
[change]: <Change Output Hex String>
|
|
27
|
-
funding: <Funding Output Hex String>
|
|
28
|
+
[funding]: <Funding Output Hex String>
|
|
28
29
|
utxos: [{
|
|
29
30
|
[non_witness_utxo]: <Spending Transaction Hex String>
|
|
30
31
|
transaction_id: <Transaction Id Hex String>
|
|
@@ -120,7 +121,7 @@ module.exports = ({capacity, proposed, rate}, cbk) => {
|
|
|
120
121
|
},
|
|
121
122
|
{
|
|
122
123
|
script: member.change,
|
|
123
|
-
tokens: funded - capacity - (vbytes * rate),
|
|
124
|
+
tokens: funded - committed(capacity, proposed) - (vbytes * rate),
|
|
124
125
|
},
|
|
125
126
|
];
|
|
126
127
|
});
|
|
@@ -21,7 +21,7 @@ const {serviceTypeRegisterSignedOpen} = require('./../../service_types');
|
|
|
21
21
|
const findRecord = (records, type) => records.find(n => n.type === type);
|
|
22
22
|
const {isArray} = Array;
|
|
23
23
|
const makeGroupId = () => randomBytes(16).toString('hex');
|
|
24
|
-
const minGroupCount =
|
|
24
|
+
const minGroupCount = 2;
|
|
25
25
|
const now = () => new Date().toISOString();
|
|
26
26
|
const staleDate = () => new Date(Date.now() - (1000 * 60 * 10)).toISOString();
|
|
27
27
|
const typeGroupId = '1';
|
|
@@ -186,6 +186,11 @@ module.exports = ({capacity, count, identity, lnd, rate}) => {
|
|
|
186
186
|
// Emit event that everyone has joined
|
|
187
187
|
group.emitter.emit('joined', {ids: group.ids});
|
|
188
188
|
|
|
189
|
+
// Exit early when this is a pair group
|
|
190
|
+
if (count === minGroupCount) {
|
|
191
|
+
return res.success({});
|
|
192
|
+
}
|
|
193
|
+
|
|
189
194
|
// Derive position in members list
|
|
190
195
|
const {inbound, outbound} = partnersFromMembers({group, id: req.from});
|
|
191
196
|
|
|
@@ -16,6 +16,7 @@ const {signAndFundGroupChannel} = require('./funding');
|
|
|
16
16
|
|
|
17
17
|
const {fromHex} = Transaction;
|
|
18
18
|
const interval = 500;
|
|
19
|
+
const minGroupCount = 2;
|
|
19
20
|
const times = 2 * 60 * 10;
|
|
20
21
|
|
|
21
22
|
/** Assemble a channel group
|
|
@@ -68,6 +69,10 @@ const times = 2 * 60 * 10;
|
|
|
68
69
|
{}
|
|
69
70
|
*/
|
|
70
71
|
module.exports = ({capacity, count, ecp, identity, lnd, rate}) => {
|
|
72
|
+
if (count < minGroupCount) {
|
|
73
|
+
throw new Error('ExpectedHigherGroupCountToAssembleChannelGroup');
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
const coordinator = coordinateGroup({capacity, count, identity, lnd, rate});
|
|
72
77
|
const emitter = new EventEmitter();
|
|
73
78
|
const pending = {};
|
|
@@ -85,6 +90,11 @@ module.exports = ({capacity, count, ecp, identity, lnd, rate}) => {
|
|
|
85
90
|
coordinator.events.once('joined', async ({ids}) => {
|
|
86
91
|
emitter.emit('filled', {ids});
|
|
87
92
|
|
|
93
|
+
// Exit early when this is a pair group
|
|
94
|
+
if (count === minGroupCount) {
|
|
95
|
+
return coordinator.connected();
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
const {inbound, outbound} = coordinator.partners(identity);
|
|
89
99
|
|
|
90
100
|
// Connect to the inbound and outbound partners
|
|
@@ -106,6 +116,7 @@ module.exports = ({capacity, count, ecp, identity, lnd, rate}) => {
|
|
|
106
116
|
// Fund and propose the pending channel to the outbound partner
|
|
107
117
|
const {change, funding, id, utxos} = await proposeGroupChannel({
|
|
108
118
|
capacity,
|
|
119
|
+
count,
|
|
109
120
|
lnd,
|
|
110
121
|
rate,
|
|
111
122
|
to: coordinator.partners(identity).outbound,
|
|
@@ -142,7 +153,7 @@ module.exports = ({capacity, count, ecp, identity, lnd, rate}) => {
|
|
|
142
153
|
lnd,
|
|
143
154
|
from: coordinator.partners(identity).inbound,
|
|
144
155
|
id: fromHex(basePsbt.unsigned_transaction).getId(),
|
|
145
|
-
to: coordinator.partners(identity).outbound,
|
|
156
|
+
to: coordinator.partners(identity).outbound || undefined,
|
|
146
157
|
});
|
|
147
158
|
});
|
|
148
159
|
|
|
@@ -11,7 +11,7 @@ const half = n => n / 2;
|
|
|
11
11
|
from: <Look for Incoming Channel From Identity Public Key Hex String>
|
|
12
12
|
id: <Channel Transaction id Hex String>
|
|
13
13
|
lnd: <Authenticated LND API Object>
|
|
14
|
-
to: <Look for Outgoing Channel To Identity Public Key Hex String>
|
|
14
|
+
[to]: <Look for Outgoing Channel To Identity Public Key Hex String>
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
@returns via cbk or Promise
|
|
@@ -37,10 +37,6 @@ module.exports = ({capacity, from, id, lnd, to}, cbk) => {
|
|
|
37
37
|
return cbk([400, 'ExpectedAuthenticatedLndToConfirmChannel']);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
if (!to) {
|
|
41
|
-
return cbk([400, "ExpectedToPublicKeyToConfirmIncomingChannel"]);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
40
|
return cbk();
|
|
45
41
|
},
|
|
46
42
|
|
|
@@ -78,6 +74,11 @@ module.exports = ({capacity, from, id, lnd, to}, cbk) => {
|
|
|
78
74
|
|
|
79
75
|
// Double check that there is also a twin outgoing channel
|
|
80
76
|
outgoing: ['getPending', ({getPending}, cbk) => {
|
|
77
|
+
// Exit early when there is no outgoing channel
|
|
78
|
+
if (!to) {
|
|
79
|
+
return cbk();
|
|
80
|
+
}
|
|
81
|
+
|
|
81
82
|
const pending = getPending.pending_channels.find(chan => {
|
|
82
83
|
return !chan.is_partner_initiated && chan.transaction_id === id;
|
|
83
84
|
});
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
2
|
const asyncEach = require('async/each');
|
|
3
3
|
const {fundPsbtDisallowingInputs} = require('ln-sync');
|
|
4
|
+
const {getNetwork} = require('ln-sync');
|
|
4
5
|
const {getUtxos} = require('ln-service');
|
|
5
6
|
const {openChannels} = require('ln-service');
|
|
7
|
+
const {networks} = require('bitcoinjs-lib');
|
|
8
|
+
const {payments} = require('bitcoinjs-lib');
|
|
6
9
|
const {returnResult} = require('asyncjs-util');
|
|
7
10
|
const {unlockUtxo} = require('ln-service');
|
|
8
11
|
|
|
12
|
+
const dummyKeys = () => ([Buffer.alloc(33, 2), Buffer.alloc(33, 3)]);
|
|
9
13
|
const fuzzSize = 1;
|
|
10
14
|
const halfOf = n => n / 2;
|
|
11
15
|
const isEven = n => !(n % 2);
|
|
16
|
+
const minGroupCount = 2;
|
|
12
17
|
const nestedSegWitAddressFormat = 'np2wpkh';
|
|
13
18
|
const nestedSegWitPath = "m/49'/";
|
|
14
19
|
|
|
@@ -18,7 +23,7 @@ const nestedSegWitPath = "m/49'/";
|
|
|
18
23
|
capacity: <Channel Capacity Tokens Number>
|
|
19
24
|
lnd: <Authenticated LND API Object>
|
|
20
25
|
rate: <Fee Rate Number>
|
|
21
|
-
to: <Peer Id Public Key Hex String>
|
|
26
|
+
[to]: <Peer Id Public Key Hex String>
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
@returns via cbk or Promise
|
|
@@ -45,7 +50,7 @@ const nestedSegWitPath = "m/49'/";
|
|
|
45
50
|
}]
|
|
46
51
|
}
|
|
47
52
|
*/
|
|
48
|
-
module.exports = ({capacity, lnd, rate, to}, cbk) => {
|
|
53
|
+
module.exports = ({capacity, count, lnd, rate, to}, cbk) => {
|
|
49
54
|
return new Promise((resolve, reject) => {
|
|
50
55
|
return asyncAuto({
|
|
51
56
|
// Check arguments
|
|
@@ -54,6 +59,10 @@ module.exports = ({capacity, lnd, rate, to}, cbk) => {
|
|
|
54
59
|
return cbk([400, 'ExpectedCapacityToProposeGroupChannel']);
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
if (!count) {
|
|
63
|
+
return cbk([400, 'ExpectedGroupCountToProposeGroupChannel']);
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
if (!isEven(capacity)) {
|
|
58
67
|
return cbk([400, 'ExpectedEventCapacityToProposeGroupChannel']);
|
|
59
68
|
}
|
|
@@ -66,28 +75,59 @@ module.exports = ({capacity, lnd, rate, to}, cbk) => {
|
|
|
66
75
|
return cbk([400, 'ExpectedChainFeeRateToProposeGroupChannel']);
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
if (!to) {
|
|
70
|
-
return cbk([400, 'ExpectedChannelPartnerPublicKeyToProposeChannel']);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
78
|
return cbk();
|
|
74
79
|
},
|
|
75
80
|
|
|
76
81
|
// Get inputs to figure out which cannot be used for a group funding
|
|
77
82
|
getInputs: ['validate', ({}, cbk) => getUtxos({lnd}, cbk)],
|
|
78
83
|
|
|
84
|
+
// Get the bitcoinjs network name for dummy output derivation
|
|
85
|
+
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
|
|
86
|
+
|
|
79
87
|
// Propose the channel to get an address to fund
|
|
80
|
-
propose: ['
|
|
88
|
+
propose: ['getNetwork', ({getNetwork}, cbk) => {
|
|
89
|
+
const tokens = halfOf(capacity);
|
|
90
|
+
|
|
91
|
+
// Exit early when there is a shared proposal due to a pair group
|
|
92
|
+
if (!to) {
|
|
93
|
+
const {address} = payments.p2wsh({
|
|
94
|
+
network: networks[getNetwork.bitcoinjs],
|
|
95
|
+
redeem: payments.p2ms({
|
|
96
|
+
m: dummyKeys().length,
|
|
97
|
+
network: networks[getNetwork.bitcoinjs],
|
|
98
|
+
pubkeys: dummyKeys(),
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Pretend we are opening a channel when there is no outbound target
|
|
103
|
+
return cbk(null, {pending: [{address, tokens}]});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Propose a channel
|
|
81
107
|
return openChannels({
|
|
82
108
|
lnd,
|
|
83
109
|
channels: [{
|
|
84
110
|
capacity,
|
|
85
|
-
give_tokens:
|
|
111
|
+
give_tokens: tokens,
|
|
86
112
|
partner_public_key: to,
|
|
87
113
|
}],
|
|
88
114
|
is_avoiding_broadcast: true,
|
|
89
115
|
},
|
|
90
|
-
|
|
116
|
+
(err, res) => {
|
|
117
|
+
if (!!err) {
|
|
118
|
+
return cbk(err);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Exit early with the regular pending when it's a normal group size
|
|
122
|
+
if (count > minGroupCount) {
|
|
123
|
+
return cbk(null, res);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// In a pair group size, remap the funding so that it's only half
|
|
127
|
+
const [{address, id}] = res.pending;
|
|
128
|
+
|
|
129
|
+
return cbk(null, {pending: [{address, id, tokens}]});
|
|
130
|
+
});
|
|
91
131
|
}],
|
|
92
132
|
|
|
93
133
|
// Fund the address to populate UTXOs that can be used
|
|
@@ -145,10 +185,13 @@ module.exports = ({capacity, lnd, rate, to}, cbk) => {
|
|
|
145
185
|
// Find the change output
|
|
146
186
|
const change = fund.outputs.find(n => n.is_change);
|
|
147
187
|
|
|
148
|
-
//
|
|
188
|
+
// Find the funding output
|
|
189
|
+
const funding = fund.outputs.find(n => !n.is_change);
|
|
190
|
+
|
|
191
|
+
// UTXOs have been selected
|
|
149
192
|
return cbk(null, {
|
|
150
193
|
change: !!change ? change.output_script : undefined,
|
|
151
|
-
funding:
|
|
194
|
+
funding: !!to ? funding.output_script : undefined,
|
|
152
195
|
id: proposal.id,
|
|
153
196
|
overflow: !!change ? change.tokens : undefined,
|
|
154
197
|
utxos: fund.inputs.map(input => ({
|
|
@@ -40,7 +40,7 @@ const times = 500;
|
|
|
40
40
|
/** Sign and fund group channel
|
|
41
41
|
|
|
42
42
|
{
|
|
43
|
-
id: <Pending Channel Id Hex String>
|
|
43
|
+
[id]: <Pending Channel Id Hex String>
|
|
44
44
|
lnd: <Authenticated LND API Object>
|
|
45
45
|
psbt: <Base Funding PSBT Hex String>
|
|
46
46
|
utxos: [{
|
|
@@ -74,10 +74,6 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
|
|
|
74
74
|
|
|
75
75
|
// Check arguments
|
|
76
76
|
validate: cbk => {
|
|
77
|
-
if (!id) {
|
|
78
|
-
return cbk([400, 'ExpectedPendingChannelIdToSignAndFundGroupChan']);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
77
|
if (!lnd) {
|
|
82
78
|
return cbk([400, 'ExpectedAuthenticatedLndToSignAndFundGroupChan']);
|
|
83
79
|
}
|
|
@@ -254,6 +250,11 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
|
|
|
254
250
|
|
|
255
251
|
// Fund the pending channel with the finalized PSBT
|
|
256
252
|
fundChannel: ['conflict', 'finalizePsbt', ({finalizePsbt}, cbk) => {
|
|
253
|
+
// Exit early when this is a pair channel and there is no proposal
|
|
254
|
+
if (!id) {
|
|
255
|
+
return cbk();
|
|
256
|
+
}
|
|
257
|
+
|
|
257
258
|
return fundPendingChannels({
|
|
258
259
|
lnd,
|
|
259
260
|
channels: [id],
|
|
@@ -264,6 +265,11 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
|
|
|
264
265
|
|
|
265
266
|
// Confirm that the outgoing pending channel is present
|
|
266
267
|
confirmOutPending: ['ecp', 'fundChannel', ({ecp}, cbk) => {
|
|
268
|
+
// Exit early when this is a pair channel and there is no proposal
|
|
269
|
+
if (!id) {
|
|
270
|
+
return cbk();
|
|
271
|
+
}
|
|
272
|
+
|
|
267
273
|
const tx = fromHex(decodePsbt({ecp, psbt}).unsigned_transaction);
|
|
268
274
|
|
|
269
275
|
// Wait for the outgoing pending channel to be present
|
|
@@ -90,11 +90,16 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
|
|
|
90
90
|
|
|
91
91
|
// Find partners in the group
|
|
92
92
|
partners: ['validate', ({}, cbk) => {
|
|
93
|
-
return findGroupPartners({coordinator, id, lnd}, cbk);
|
|
93
|
+
return findGroupPartners({coordinator, count, id, lnd}, cbk);
|
|
94
94
|
}],
|
|
95
95
|
|
|
96
96
|
// Peer with the group partners
|
|
97
97
|
peer: ['partners', ({partners}, cbk) => {
|
|
98
|
+
// Exit early when there is no inbound partner to connect with
|
|
99
|
+
if (!partners.inbound) {
|
|
100
|
+
return cbk();
|
|
101
|
+
}
|
|
102
|
+
|
|
98
103
|
// Let listeners know that peering will be happening
|
|
99
104
|
emitter.emit('peering', {
|
|
100
105
|
inbound: partners.inbound,
|
|
@@ -118,6 +123,7 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
|
|
|
118
123
|
propose: ['connected', 'partners', ({partners}, cbk) => {
|
|
119
124
|
return proposeGroupChannel({
|
|
120
125
|
capacity,
|
|
126
|
+
count,
|
|
121
127
|
lnd,
|
|
122
128
|
rate,
|
|
123
129
|
to: partners.outbound,
|
|
@@ -168,6 +174,11 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
|
|
|
168
174
|
'transaction',
|
|
169
175
|
asyncReflect(({ecp, partners, register, transaction}, cbk) =>
|
|
170
176
|
{
|
|
177
|
+
// Exit early when there is no inbound partner
|
|
178
|
+
if (!partners.inbound) {
|
|
179
|
+
return cbk();
|
|
180
|
+
}
|
|
181
|
+
|
|
171
182
|
// Make sure that there is an inbound channel
|
|
172
183
|
return asyncRetry({interval, times}, cbk => {
|
|
173
184
|
return confirmIncomingChannel({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const minGroupCount = 2;
|
|
2
|
+
|
|
1
3
|
/** Derive inbound and outbound partners from members list
|
|
2
4
|
|
|
3
5
|
{
|
|
@@ -14,6 +16,11 @@
|
|
|
14
16
|
}
|
|
15
17
|
*/
|
|
16
18
|
module.exports = ({group, id}) => {
|
|
19
|
+
// Exit early when the group only has a pair and there is only one partner
|
|
20
|
+
if (group.ids.length === minGroupCount) {
|
|
21
|
+
return {inbound: group.ids.find(n => n !== id)};
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
const [first] = group.ids;
|
|
18
25
|
const reversed = group.ids.slice().reverse();
|
|
19
26
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
const {decodeBigSize} = require('bolt01');
|
|
2
2
|
|
|
3
3
|
const findRecord = (records, type) => records.find(n => n.type === type);
|
|
4
|
+
const funding = (capacity, count) => count === 2 ? capacity / 2 : capacity;
|
|
4
5
|
const {isArray} = Array;
|
|
5
6
|
const isOdd = n => !!(n % 2);
|
|
6
7
|
const maxCapacityTokens = BigInt(7e14);
|
|
7
8
|
const maxMembersCount = BigInt(650);
|
|
8
9
|
const maxFeeRate = BigInt(1e5);
|
|
10
|
+
const minMembersCount = BigInt(2);
|
|
9
11
|
const typeCapacity = '1';
|
|
10
12
|
const typeCount = '2';
|
|
11
13
|
const typeRate = '3';
|
|
12
14
|
const typeVersion = '0';
|
|
15
|
+
const version = '1';
|
|
13
16
|
|
|
14
17
|
/** Decode group details
|
|
15
18
|
|
|
@@ -27,6 +30,7 @@ const typeVersion = '0';
|
|
|
27
30
|
{
|
|
28
31
|
capacity: <Channel Capacity Tokens Number>
|
|
29
32
|
count: <Target Members Count Number>
|
|
33
|
+
funding: <Amount Of Funding Required Tokens Number>
|
|
30
34
|
rate: <Chain Fee Rate Number>
|
|
31
35
|
}
|
|
32
36
|
*/
|
|
@@ -41,8 +45,18 @@ module.exports = ({records}) => {
|
|
|
41
45
|
|
|
42
46
|
const versionRecord = findRecord(records, typeVersion);
|
|
43
47
|
|
|
44
|
-
if (
|
|
45
|
-
throw new Error('
|
|
48
|
+
if (!versionRecord) {
|
|
49
|
+
throw new Error('ExpectedVersionOfGroupDetailsRecords');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
decodeBigSize({encoded: versionRecord.value});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error('ExpectedValidVersionNumberInGroupRecords');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (decodeBigSize({encoded: versionRecord.value}).decoded !== version) {
|
|
59
|
+
throw new Error('UnsupportedGroupVersion');
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
const capacityRecord = findRecord(records, typeCapacity);
|
|
@@ -77,6 +91,10 @@ module.exports = ({records}) => {
|
|
|
77
91
|
throw new Error('UnexpectedValueForCountInGroupRecords');
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
if (BigInt(count) < minMembersCount) {
|
|
95
|
+
throw new Error('ExpectedHigherMembersCountInGroupDetails');
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
const rateRecord = findRecord(records, typeRate);
|
|
81
99
|
|
|
82
100
|
try {
|
|
@@ -94,6 +112,7 @@ module.exports = ({records}) => {
|
|
|
94
112
|
return {
|
|
95
113
|
capacity: Number(capacity),
|
|
96
114
|
count: Number(count),
|
|
115
|
+
funding: funding(Number(capacity), Number(count)),
|
|
97
116
|
rate: Number(rate),
|
|
98
117
|
};
|
|
99
118
|
};
|
|
@@ -4,6 +4,8 @@ const encodeNumber = number => encodeBigSize({number}).encoded;
|
|
|
4
4
|
const typeCapacity = '1';
|
|
5
5
|
const typeCount = '2';
|
|
6
6
|
const typeRate = '3';
|
|
7
|
+
const typeVersion = '0';
|
|
8
|
+
const version = '1';
|
|
7
9
|
|
|
8
10
|
/** Encode group details records
|
|
9
11
|
|
|
@@ -27,6 +29,7 @@ module.exports = ({capacity, count, rate}) => {
|
|
|
27
29
|
{type: typeCapacity, value: encodeNumber(capacity)},
|
|
28
30
|
{type: typeCount, value: encodeNumber(count)},
|
|
29
31
|
{type: typeRate, value: encodeNumber(rate)},
|
|
32
|
+
{type: typeVersion, value: encodeNumber(version)},
|
|
30
33
|
],
|
|
31
34
|
};
|
|
32
35
|
};
|
|
@@ -16,7 +16,5 @@ const typePartnersRecord = '1';
|
|
|
16
16
|
}
|
|
17
17
|
*/
|
|
18
18
|
module.exports = ({inbound, outbound}) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return {records};
|
|
19
|
+
return {records: [{type: typePartnersRecord, value: inbound + outbound}]};
|
|
22
20
|
};
|
|
@@ -13,6 +13,7 @@ const defaultConnectPollTimes = 2 * 60 * 5;
|
|
|
13
13
|
const defaultGroupPartnersIntervalMs = 500;
|
|
14
14
|
const defaultGroupPartnersPollTimes = 2 * 60 * 60 * 24 * 3;
|
|
15
15
|
const defaultRequestTimeoutMs = 1000 * 60;
|
|
16
|
+
const minGroupCount = 2;
|
|
16
17
|
const missingGroupPartners = 'NoGroupPartnersFound';
|
|
17
18
|
const typeGroupChannelId = '1';
|
|
18
19
|
|
|
@@ -30,7 +31,7 @@ const typeGroupChannelId = '1';
|
|
|
30
31
|
outbound: <Outbound Peer Public Key Identity Hex String>
|
|
31
32
|
}
|
|
32
33
|
*/
|
|
33
|
-
module.exports = ({coordinator, id, lnd}, cbk) => {
|
|
34
|
+
module.exports = ({coordinator, count, id, lnd}, cbk) => {
|
|
34
35
|
return new Promise((resolve, reject) => {
|
|
35
36
|
return asyncAuto({
|
|
36
37
|
// Check arguments
|
|
@@ -39,6 +40,10 @@ module.exports = ({coordinator, id, lnd}, cbk) => {
|
|
|
39
40
|
return cbk([400, 'ExpectedCoordinatorToFindGroupPartners']);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
if (!count) {
|
|
44
|
+
return cbk([400, 'ExpectedGroupMemberCountToFindGroupPartners']);
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
if (!id) {
|
|
43
48
|
return cbk([400, 'ExpectedGroupIdToFindGroupPartners']);
|
|
44
49
|
}
|
|
@@ -84,6 +89,11 @@ module.exports = ({coordinator, id, lnd}, cbk) => {
|
|
|
84
89
|
return cbk(err);
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
// Exit early when the group is a pair
|
|
93
|
+
if (count === minGroupCount) {
|
|
94
|
+
return cbk();
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
// Exit with error when there are no group partners
|
|
88
98
|
if (!res.records || !res.records.length) {
|
|
89
99
|
return cbk([503, missingGroupPartners]);
|
|
@@ -97,6 +107,11 @@ module.exports = ({coordinator, id, lnd}, cbk) => {
|
|
|
97
107
|
|
|
98
108
|
// Parse the group partners response
|
|
99
109
|
partners: ['request', ({request}, cbk) => {
|
|
110
|
+
// Exit early when there are no records
|
|
111
|
+
if (!request) {
|
|
112
|
+
return cbk(null, {outbound: coordinator});
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
try {
|
|
101
116
|
return cbk(null, decodePartnersRecords({records: request}));
|
|
102
117
|
} catch (err) {
|
|
@@ -106,6 +121,11 @@ module.exports = ({coordinator, id, lnd}, cbk) => {
|
|
|
106
121
|
|
|
107
122
|
// Attempt to connect to the partners
|
|
108
123
|
peer: ['partners', ({partners}, cbk) => {
|
|
124
|
+
// Exit early when there are no partners
|
|
125
|
+
if (!partners.inbound) {
|
|
126
|
+
return cbk();
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
return asyncEach([partners.inbound, partners.outbound], (id, cbk) => {
|
|
110
130
|
return asyncRetry({
|
|
111
131
|
interval: defaultConnectIntervalMs,
|
|
@@ -34,7 +34,7 @@ const typeGroupChannelId = '1';
|
|
|
34
34
|
group: <Group Identifier Hex String>
|
|
35
35
|
lnd: <Authenticated LND API Object>
|
|
36
36
|
overflow: <Expected Minimum Change Amount Tokens Number>
|
|
37
|
-
pending: <Pending Channel Id Hex String>
|
|
37
|
+
[pending]: <Pending Channel Id Hex String>
|
|
38
38
|
utxos: [{
|
|
39
39
|
[non_witness_utxo]: <Non Witness Transaction Hex String>
|
|
40
40
|
transaction_id: <Transaction Id Hex String>
|
|
@@ -80,10 +80,6 @@ module.exports = (args, cbk) => {
|
|
|
80
80
|
return cbk([400, 'ExpectedAuthenticatedLndToRegisterPendingOpen']);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
if (!args.pending) {
|
|
84
|
-
return cbk([400, 'ExpectedPendingChannelIdToRegisterPendingOpen']);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
83
|
if (!isArray(args.utxos)) {
|
|
88
84
|
return cbk([400, 'ExpectedArrayOfUtxosToRegisterPendingOpen']);
|
|
89
85
|
}
|
|
@@ -157,6 +153,11 @@ module.exports = (args, cbk) => {
|
|
|
157
153
|
return cbk();
|
|
158
154
|
}
|
|
159
155
|
|
|
156
|
+
// Exit early if there is no pending id
|
|
157
|
+
if (!args.pending) {
|
|
158
|
+
return cbk();
|
|
159
|
+
}
|
|
160
|
+
|
|
160
161
|
return cancelPendingChannel({id: args.pending, lnd: args.lnd}, err => {
|
|
161
162
|
if (!!err) {
|
|
162
163
|
return cbk([503, 'UnexpectedErrorCleaningPendingChannel', {err}]);
|
package/package.json
CHANGED
|
@@ -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.20.2"
|
|
51
51
|
}
|
|
@@ -57,7 +57,7 @@ test(`Setup joint channel group`, async ({end, equal, strictSame}) => {
|
|
|
57
57
|
lnd: remote.lnd,
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
// Send coins to
|
|
60
|
+
// Send coins to remote
|
|
61
61
|
await sendToChainAddress({lnd, tokens, address: remoteAddress.address});
|
|
62
62
|
|
|
63
63
|
// Wait for funds to arrive
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const {once} = require('events');
|
|
2
|
+
|
|
3
|
+
const {addPeer} = require('ln-service');
|
|
4
|
+
const asyncMap = require('async/map');
|
|
5
|
+
const asyncRetry = require('async/retry');
|
|
6
|
+
const {createChainAddress} = require('ln-service');
|
|
7
|
+
const {getChainTransactions} = require('ln-service');
|
|
8
|
+
const {getChannels} = require('ln-service');
|
|
9
|
+
const {getNetwork} = require('ln-sync');
|
|
10
|
+
const {getUtxos} = require('ln-service');
|
|
11
|
+
const {networks} = require('bitcoinjs-lib');
|
|
12
|
+
const {sendToChainAddress} = require('ln-service');
|
|
13
|
+
const {spawnLightningCluster} = require('ln-docker-daemons');
|
|
14
|
+
const {test} = require('@alexbosworth/tap');
|
|
15
|
+
const tinysecp = require('tiny-secp256k1');
|
|
16
|
+
|
|
17
|
+
const assembleChannelGroup = require('./../../groups/assemble_channel_group');
|
|
18
|
+
const {confirmIncomingChannel} = require('./../../groups/funding');
|
|
19
|
+
const {getGroupDetails} = require('./../../groups/p2p');
|
|
20
|
+
const joinChannelGroup = require('./../../groups/join_channel_group');
|
|
21
|
+
|
|
22
|
+
const capacity = 1e5;
|
|
23
|
+
const count = 101;
|
|
24
|
+
const feeRate = 1;
|
|
25
|
+
const interval = 10;
|
|
26
|
+
const size = 2;
|
|
27
|
+
const tokens = 1e6;
|
|
28
|
+
const times = 2000;
|
|
29
|
+
|
|
30
|
+
// Make a joint transaction channel group
|
|
31
|
+
test(`Setup joint channel group`, async ({end, equal, strictSame}) => {
|
|
32
|
+
const ecp = (await import('ecpair')).ECPairFactory(tinysecp);
|
|
33
|
+
const {kill, nodes} = await spawnLightningCluster({size});
|
|
34
|
+
|
|
35
|
+
const [control, target] = nodes;
|
|
36
|
+
|
|
37
|
+
const {generate, lnd} = control;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Setup the cluster of nodes to have funds and be connected
|
|
41
|
+
|
|
42
|
+
// Make some funds for control
|
|
43
|
+
await generate({count});
|
|
44
|
+
|
|
45
|
+
// Get the bitcoinjs network
|
|
46
|
+
const network = networks[(await getNetwork({lnd})).bitcoinjs];
|
|
47
|
+
|
|
48
|
+
// Create a target chain address
|
|
49
|
+
const targetAddress = await createChainAddress({lnd: target.lnd});
|
|
50
|
+
|
|
51
|
+
// Send coins to target
|
|
52
|
+
await sendToChainAddress({lnd, tokens, address: targetAddress.address});
|
|
53
|
+
|
|
54
|
+
// Wait for funds to arrive
|
|
55
|
+
await asyncRetry({interval, times}, async () => {
|
|
56
|
+
await generate({});
|
|
57
|
+
|
|
58
|
+
const {transactions} = await getChainTransactions({lnd});
|
|
59
|
+
|
|
60
|
+
if (!!transactions.filter(n => !n.is_confirmed).length) {
|
|
61
|
+
throw new Error('TransactionsAreUnconfirmed');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Wait for UTXOs to be confirmed
|
|
66
|
+
await asyncRetry({interval, times}, async () => {
|
|
67
|
+
const targetUtxos = await getUtxos({lnd: target.lnd});
|
|
68
|
+
|
|
69
|
+
if (!targetUtxos.utxos.filter(n => !!n.confirmation_count).length) {
|
|
70
|
+
throw new Error('ExpectedConfirmedUtxoOnTarget');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Connect control to target
|
|
75
|
+
await addPeer({lnd, public_key: target.id, socket: target.socket});
|
|
76
|
+
|
|
77
|
+
// Start Group Coordination
|
|
78
|
+
|
|
79
|
+
// Start the coordination
|
|
80
|
+
const assemble = assembleChannelGroup({
|
|
81
|
+
capacity,
|
|
82
|
+
ecp,
|
|
83
|
+
count: nodes.length,
|
|
84
|
+
identity: control.id,
|
|
85
|
+
lnd: control.lnd,
|
|
86
|
+
rate: feeRate,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const events = {};
|
|
90
|
+
|
|
91
|
+
assemble.events.once('broadcast', n => events.broadcast = n);
|
|
92
|
+
assemble.events.once('filled', n => events.filled = n);
|
|
93
|
+
|
|
94
|
+
// Target join the group
|
|
95
|
+
const joins = await asyncMap([target.lnd], async lnd => {
|
|
96
|
+
const group = await getGroupDetails({
|
|
97
|
+
lnd,
|
|
98
|
+
coordinator: control.id,
|
|
99
|
+
id: assemble.id,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const join = joinChannelGroup({
|
|
103
|
+
lnd,
|
|
104
|
+
capacity: group.capacity,
|
|
105
|
+
coordinator: control.id,
|
|
106
|
+
count: group.count,
|
|
107
|
+
id: assemble.id,
|
|
108
|
+
rate: group.rate,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const [tx] = await once(join, 'end');
|
|
112
|
+
|
|
113
|
+
return tx;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Transaction ids of the open should be returned
|
|
117
|
+
const ids = joins.map(n => n.id);
|
|
118
|
+
|
|
119
|
+
// Finished, wait for the channels to activate
|
|
120
|
+
await generate({count});
|
|
121
|
+
|
|
122
|
+
const {getPendingChannels} = require('ln-service');
|
|
123
|
+
|
|
124
|
+
await asyncRetry({interval, times}, async () => {
|
|
125
|
+
await generate({});
|
|
126
|
+
|
|
127
|
+
const {channels} = await getChannels({lnd, is_active: true});
|
|
128
|
+
|
|
129
|
+
if (!channels.length) {
|
|
130
|
+
throw new Error('ExpectedChannelActivation');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
strictSame(ids, [events.broadcast.id], 'Got tx ids');
|
|
135
|
+
strictSame(events.broadcast.id.length, 64, 'Got broadcast tx id');
|
|
136
|
+
strictSame(!!events.broadcast.transaction, true, 'Got broadcast tx');
|
|
137
|
+
strictSame(events.filled.ids.length, nodes.length, 'Got filled event');
|
|
138
|
+
} catch (err) {
|
|
139
|
+
strictSame(err, null, 'Expected no failure');
|
|
140
|
+
} finally {
|
|
141
|
+
await kill({});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return end();
|
|
145
|
+
});
|
|
@@ -66,6 +66,9 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
|
|
|
66
66
|
// Derive the self public key
|
|
67
67
|
getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
|
|
68
68
|
|
|
69
|
+
// Get the actively connected peers
|
|
70
|
+
getPeered: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
|
|
71
|
+
|
|
69
72
|
// Find node connect info to connect to
|
|
70
73
|
getNodes: ['getIdentity', ({getIdentity}, cbk) => {
|
|
71
74
|
return asyncMap(nodes, (connect, cbk) => {
|
|
@@ -144,7 +147,8 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
|
|
|
144
147
|
removePeer: [
|
|
145
148
|
'getChannels',
|
|
146
149
|
'getNodes',
|
|
147
|
-
|
|
150
|
+
'getPeered',
|
|
151
|
+
({getChannels, getNodes, getPeered}, cbk) =>
|
|
148
152
|
{
|
|
149
153
|
const ids = getChannels.channels.map(n => n.partner_public_key);
|
|
150
154
|
|
|
@@ -154,6 +158,11 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
|
|
|
154
158
|
return cbk();
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
// Exit early when already not peered
|
|
162
|
+
if (!getPeered.peers.map(n => n.public_key).includes(node.id)) {
|
|
163
|
+
return cbk();
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
return removePeer({lnd, public_key: node.id}, (err, res) => {
|
|
158
167
|
if (!!err) {
|
|
159
168
|
return cbk(err);
|
|
@@ -181,7 +190,7 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
|
|
|
181
190
|
}],
|
|
182
191
|
|
|
183
192
|
// Get the list of connected peers
|
|
184
|
-
getPeers: ['
|
|
193
|
+
getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
|
|
185
194
|
|
|
186
195
|
// Try and connect to a node in order to do p2p messaging
|
|
187
196
|
connect: [
|