ln-service 53.5.2 → 53.7.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 +8 -0
- package/README.md +51 -2
- package/index.js +4 -2
- package/package.json +16 -5
- package/test/integration/test_add_peer.js +25 -19
- package/test/integration/test_delete_pending_channel.js +18 -11
- package/test/integration/test_get_failed_payments.js +123 -119
- package/test/integration/test_get_node.js +48 -44
- package/test/integration/test_get_pending_force.js +8 -1
- package/test/integration/test_get_wallet_info.js +30 -15
- package/test/integration/test_open_channels.js +21 -14
- package/test/integration/test_propose_channel.js +438 -421
- package/test/integration/test_subscribe_to_channels.js +1 -1
- package/test/integration/test_subscribe_to_peer_messages.js +77 -71
- package/test/integration/test_subscribe_to_peers.js +23 -19
- package/test/routerrpc-integration/test_get_forwarding_reputations.js +64 -60
- package/test/routerrpc-integration/test_get_route_confidence.js +38 -34
- package/test/routerrpc-integration/test_get_route_through_hops.js +33 -14
- package/test/routerrpc-integration/test_pay_via_payment_details.js +118 -110
- package/test/routerrpc-integration/test_pay_via_routes.js +19 -1
- package/test/routerrpc-integration/test_probe_for_route.js +75 -71
- package/test/routerrpc-integration/test_subscribe_to_forward_requests.js +2 -2
- package/test/routerrpc-integration/test_subscribe_to_forwards.js +508 -504
- package/test/walletrpc-integration/test_fund_psbt.js +4 -1
- package/test/walletrpc-integration/test_partially_sign_psbt.js +216 -0
- package/test/walletrpc-integration/test_sign_psbt.js +4 -1
|
@@ -25,140 +25,148 @@ test(`Pay`, async ({end, equal, rejects, strictSame}) => {
|
|
|
25
25
|
|
|
26
26
|
const [{generate, lnd}, target, remote] = nodes;
|
|
27
27
|
|
|
28
|
-
const channel = await setupChannel({lnd, generate, to: target});
|
|
29
|
-
|
|
30
|
-
const remoteChan = await setupChannel({
|
|
31
|
-
generate: target.generate,
|
|
32
|
-
lnd: target.lnd,
|
|
33
|
-
to: remote,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
await addPeer({lnd, public_key: remote.id, socket: remote.socket});
|
|
37
|
-
|
|
38
|
-
const height = (await getHeight({lnd})).current_block_height;
|
|
39
|
-
const invoice = await createInvoice({tokens, lnd: remote.lnd});
|
|
40
|
-
|
|
41
|
-
const {features} = await decodePaymentRequest({
|
|
42
|
-
lnd,
|
|
43
|
-
request: invoice.request,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const expectedHops = [
|
|
47
|
-
{
|
|
48
|
-
channel: channel.id,
|
|
49
|
-
channel_capacity: 1000000,
|
|
50
|
-
fee: 1,
|
|
51
|
-
fee_mtokens: '1000',
|
|
52
|
-
forward: invoice.tokens,
|
|
53
|
-
forward_mtokens: (BigInt(invoice.tokens) * BigInt(1e3)).toString(),
|
|
54
|
-
public_key: target.id,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
channel: remoteChan.id,
|
|
58
|
-
channel_capacity: 1000000,
|
|
59
|
-
fee: 0,
|
|
60
|
-
fee_mtokens: '0',
|
|
61
|
-
forward: invoice.tokens,
|
|
62
|
-
forward_mtokens: '100000',
|
|
63
|
-
public_key: remote.id,
|
|
64
|
-
},
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
await waitForRoute({lnd, destination: remote.id, tokens: invoice.tokens});
|
|
68
|
-
|
|
69
28
|
try {
|
|
70
|
-
const
|
|
71
|
-
features,
|
|
72
|
-
lnd,
|
|
73
|
-
destination: remote.id,
|
|
74
|
-
payment: invoice.payment,
|
|
75
|
-
tokens: invoice.tokens,
|
|
76
|
-
});
|
|
77
|
-
} catch (err) {
|
|
78
|
-
strictSame(err, [503, 'PaymentRejectedByDestination']);
|
|
79
|
-
}
|
|
29
|
+
const channel = await setupChannel({lnd, generate, to: target});
|
|
80
30
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
destination: remote.id,
|
|
86
|
-
id: invoice.id,
|
|
87
|
-
max_timeout_height: height + 46,
|
|
88
|
-
payment: invoice.payment,
|
|
89
|
-
tokens: invoice.tokens,
|
|
31
|
+
const remoteChan = await setupChannel({
|
|
32
|
+
generate: target.generate,
|
|
33
|
+
lnd: target.lnd,
|
|
34
|
+
to: remote,
|
|
90
35
|
});
|
|
91
36
|
|
|
92
|
-
|
|
93
|
-
} catch (err) {
|
|
94
|
-
strictSame(err, [503, 'PaymentPathfindingFailedToFindPossibleRoute'], 'Fail');
|
|
95
|
-
}
|
|
37
|
+
await addPeer({lnd, public_key: remote.id, socket: remote.socket});
|
|
96
38
|
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
39
|
+
const height = (await getHeight({lnd})).current_block_height;
|
|
40
|
+
const invoice = await createInvoice({tokens, lnd: remote.lnd});
|
|
41
|
+
|
|
42
|
+
const {features} = await decodePaymentRequest({
|
|
100
43
|
lnd,
|
|
101
|
-
|
|
102
|
-
id: invoice.id,
|
|
103
|
-
max_timeout_height: height + 90,
|
|
104
|
-
messages: [{type: tlvType, value: tlvData}],
|
|
105
|
-
payment: invoice.payment,
|
|
106
|
-
tokens: invoice.tokens,
|
|
44
|
+
request: invoice.request,
|
|
107
45
|
});
|
|
108
46
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
47
|
+
const expectedHops = [
|
|
48
|
+
{
|
|
49
|
+
channel: channel.id,
|
|
50
|
+
channel_capacity: 1000000,
|
|
51
|
+
fee: 1,
|
|
52
|
+
fee_mtokens: '1000',
|
|
53
|
+
forward: invoice.tokens,
|
|
54
|
+
forward_mtokens: (BigInt(invoice.tokens) * BigInt(1e3)).toString(),
|
|
55
|
+
public_key: target.id,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
channel: remoteChan.id,
|
|
59
|
+
channel_capacity: 1000000,
|
|
60
|
+
fee: 0,
|
|
61
|
+
fee_mtokens: '0',
|
|
62
|
+
forward: invoice.tokens,
|
|
63
|
+
forward_mtokens: '100000',
|
|
64
|
+
public_key: remote.id,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
await waitForRoute({lnd, destination: remote.id, tokens: invoice.tokens});
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const paid = await payViaPaymentDetails({
|
|
72
|
+
features,
|
|
73
|
+
lnd,
|
|
74
|
+
destination: remote.id,
|
|
75
|
+
payment: invoice.payment,
|
|
76
|
+
tokens: invoice.tokens,
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
strictSame(err, [503, 'PaymentRejectedByDestination']);
|
|
80
|
+
}
|
|
120
81
|
|
|
121
|
-
|
|
122
|
-
|
|
82
|
+
try {
|
|
83
|
+
const tooSoonCltv = await payViaPaymentDetails({
|
|
84
|
+
features,
|
|
85
|
+
lnd,
|
|
86
|
+
destination: remote.id,
|
|
87
|
+
id: invoice.id,
|
|
88
|
+
max_timeout_height: height + 46,
|
|
89
|
+
payment: invoice.payment,
|
|
90
|
+
tokens: invoice.tokens,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
equal(tooSoonCltv, null, 'Should not be able to pay a too soon CLTV');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
strictSame(
|
|
96
|
+
err,
|
|
97
|
+
[503, 'PaymentPathfindingFailedToFindPossibleRoute'],
|
|
98
|
+
'Fail'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
123
101
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
102
|
+
try {
|
|
103
|
+
const paid = await payViaPaymentDetails({
|
|
104
|
+
features,
|
|
105
|
+
lnd,
|
|
106
|
+
destination: remote.id,
|
|
107
|
+
id: invoice.id,
|
|
108
|
+
max_timeout_height: height + 90,
|
|
109
|
+
messages: [{type: tlvType, value: tlvData}],
|
|
110
|
+
payment: invoice.payment,
|
|
111
|
+
tokens: invoice.tokens,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
equal(paid.confirmed_at > start, true, 'Confirm date was received');
|
|
115
|
+
equal(paid.fee, 1, 'Fee tokens paid');
|
|
116
|
+
equal(paid.fee_mtokens, '1000', 'Fee mtokens tokens paid');
|
|
117
|
+
equal(paid.id, invoice.id, 'Payment hash is equal on both sides');
|
|
118
|
+
equal(paid.mtokens, '101000', 'Paid mtokens');
|
|
119
|
+
equal(paid.secret, invoice.secret, 'Paid for invoice secret');
|
|
120
|
+
|
|
121
|
+
paid.hops.forEach(n => {
|
|
122
|
+
equal(n.timeout === height + 40 || n.timeout === height + 43, true);
|
|
123
|
+
|
|
124
|
+
delete n.timeout;
|
|
125
|
+
|
|
126
|
+
return;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
strictSame(paid.hops, expectedHops, 'Hops are returned');
|
|
130
|
+
} catch (err) {
|
|
131
|
+
equal(err, null, 'No error is thrown when payment is attempted');
|
|
132
|
+
}
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
{
|
|
135
|
+
const {payments} = await getInvoice({id: invoice.id, lnd: remote.lnd});
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
if (!!payments) {
|
|
138
|
+
const [payment] = payments;
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
if (!!payment && !!payment.messages.length) {
|
|
141
|
+
const [message] = payment.messages;
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
equal(message.type, tlvType, 'Got TLV type');
|
|
144
|
+
equal(message.value, tlvData, 'Got TLV value');
|
|
145
|
+
}
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
|
-
}
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
{
|
|
150
|
+
const {invoices} = await getInvoices({lnd: remote.lnd});
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
const [{payments}] = invoices;
|
|
148
153
|
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
if (!!payments.length) {
|
|
155
|
+
const [payment] = payments;
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
if (!!payment && !!payment.messages.length) {
|
|
158
|
+
const [message] = payment.messages;
|
|
154
159
|
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
equal(message.type, tlvType, 'Got TLV type');
|
|
161
|
+
equal(message.value, tlvData, 'Got TLV value');
|
|
162
|
+
}
|
|
157
163
|
}
|
|
158
164
|
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
equal(err, null, 'Expected no error');
|
|
167
|
+
} finally {
|
|
168
|
+
await kill({});
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
await kill({});
|
|
162
|
-
|
|
163
171
|
return end();
|
|
164
172
|
});
|
|
@@ -18,6 +18,7 @@ const {openChannel} = require('./../../');
|
|
|
18
18
|
const {payViaRoutes} = require('./../../');
|
|
19
19
|
const {routeFromChannels} = require('./../../');
|
|
20
20
|
const {setupChannel} = require('./../macros');
|
|
21
|
+
const {subscribeToForwardRequests} = require('./../../');
|
|
21
22
|
const {waitForChannel} = require('./../macros');
|
|
22
23
|
const {waitForPendingChannel} = require('./../macros');
|
|
23
24
|
const {waitForRoute} = require('./../macros');
|
|
@@ -27,6 +28,7 @@ const confirmationCount = 6;
|
|
|
27
28
|
const defaultFee = 1e3;
|
|
28
29
|
const defaultVout = 0;
|
|
29
30
|
const interval = 10;
|
|
31
|
+
const intermediateRecord = {type: '65536', value: '5678'};
|
|
30
32
|
const mtokPadding = '000';
|
|
31
33
|
const regtestChain = '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206';
|
|
32
34
|
const reserveRatio = 0.99;
|
|
@@ -39,7 +41,7 @@ const tokens = 100;
|
|
|
39
41
|
const txIdHexLength = 32 * 2;
|
|
40
42
|
|
|
41
43
|
// Paying via routes should successfully pay via routes
|
|
42
|
-
test(`Pay via routes`, async ({end, equal}) => {
|
|
44
|
+
test(`Pay via routes`, async ({end, equal, strictSame}) => {
|
|
43
45
|
const {kill, nodes} = await spawnLightningCluster({size});
|
|
44
46
|
|
|
45
47
|
const [{generate, lnd}, target, remote] = nodes;
|
|
@@ -170,10 +172,26 @@ test(`Pay via routes`, async ({end, equal}) => {
|
|
|
170
172
|
equal(lowFeeErrMessage, 'FeeInsufficient', 'Low fee returns low fee msg');
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
const [hopToTarget] = route.hops;
|
|
176
|
+
|
|
177
|
+
hopToTarget.messages = [intermediateRecord];
|
|
178
|
+
|
|
173
179
|
route.messages = [{type: tlvType, value: tlvValue}];
|
|
174
180
|
|
|
181
|
+
const sub = subscribeToForwardRequests({lnd: target.lnd});
|
|
182
|
+
|
|
183
|
+
const forwardMessages = [];
|
|
184
|
+
|
|
185
|
+
sub.on('forward_request', request => {
|
|
186
|
+
request.messages.forEach(message => forwardMessages.push(message));
|
|
187
|
+
|
|
188
|
+
return request.accept();
|
|
189
|
+
});
|
|
190
|
+
|
|
175
191
|
const payment = await payViaRoutes({id, lnd, routes: [route]});
|
|
176
192
|
|
|
193
|
+
strictSame(forwardMessages, [intermediateRecord], 'Got intermediate record');
|
|
194
|
+
|
|
177
195
|
equal(payment.confirmed_at > start, true, 'Paid has confirm date');
|
|
178
196
|
|
|
179
197
|
const paidInvoice = await getInvoice({id, lnd: remote.lnd});
|
|
@@ -34,96 +34,100 @@ test('Probe for route', async ({end, equal, strictSame}) => {
|
|
|
34
34
|
// Send coins to remote so that it can accept a channel
|
|
35
35
|
await remote.generate({count});
|
|
36
36
|
|
|
37
|
-
await setupChannel({
|
|
38
|
-
generate,
|
|
39
|
-
lnd,
|
|
40
|
-
capacity: channelCapacityTokens + channelCapacityTokens,
|
|
41
|
-
to: target,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
await setupChannel({
|
|
45
|
-
capacity: channelCapacityTokens,
|
|
46
|
-
lnd: target.lnd,
|
|
47
|
-
generate: target.generate,
|
|
48
|
-
give: Math.round(channelCapacityTokens / 2),
|
|
49
|
-
to: remote,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await addPeer({lnd, public_key: remote.id, socket: remote.socket});
|
|
53
|
-
|
|
54
|
-
const invoice = await createInvoice({tokens, lnd: remote.lnd});
|
|
55
|
-
|
|
56
|
-
await delay(1000);
|
|
57
|
-
|
|
58
37
|
try {
|
|
59
|
-
await
|
|
38
|
+
await setupChannel({
|
|
39
|
+
generate,
|
|
60
40
|
lnd,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
tokens: invoice.tokens,
|
|
41
|
+
capacity: channelCapacityTokens + channelCapacityTokens,
|
|
42
|
+
to: target,
|
|
64
43
|
});
|
|
65
|
-
} catch (err) {
|
|
66
|
-
const [code, message, {failure}] = err;
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
45
|
+
await setupChannel({
|
|
46
|
+
capacity: channelCapacityTokens,
|
|
47
|
+
lnd: target.lnd,
|
|
48
|
+
generate: target.generate,
|
|
49
|
+
give: Math.round(channelCapacityTokens / 2),
|
|
50
|
+
to: remote,
|
|
51
|
+
});
|
|
72
52
|
|
|
73
|
-
|
|
53
|
+
await addPeer({lnd, public_key: remote.id, socket: remote.socket});
|
|
74
54
|
|
|
75
|
-
|
|
55
|
+
const invoice = await createInvoice({tokens, lnd: remote.lnd});
|
|
76
56
|
|
|
77
|
-
|
|
78
|
-
const {payments} = await getFailedPayments({lnd});
|
|
57
|
+
await delay(1000);
|
|
79
58
|
|
|
80
|
-
|
|
81
|
-
|
|
59
|
+
try {
|
|
60
|
+
await probeForRoute({
|
|
61
|
+
lnd,
|
|
62
|
+
destination: remote.id,
|
|
63
|
+
is_ignoring_past_failures: true,
|
|
64
|
+
tokens: invoice.tokens,
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const [code, message, {failure}] = err;
|
|
82
68
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
generate: target.generate,
|
|
88
|
-
to: remote,
|
|
89
|
-
});
|
|
69
|
+
equal(code, 503, 'Failed to find route');
|
|
70
|
+
equal(message, 'RoutingFailure', 'Hit a routing failure');
|
|
71
|
+
equal(failure.reason, 'TemporaryChannelFailure', 'Temporary failure');
|
|
72
|
+
}
|
|
90
73
|
|
|
91
|
-
|
|
74
|
+
const {version} = await getWalletVersion({lnd});
|
|
92
75
|
|
|
93
|
-
|
|
76
|
+
const [, minor] = (version || '').split('.');
|
|
94
77
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
lnd,
|
|
98
|
-
destination: remote.id,
|
|
99
|
-
payment: invoice.payment,
|
|
100
|
-
tokens: invoice.tokens,
|
|
101
|
-
total_mtokens: !!invoice.payment ? invoice.mtokens : undefined,
|
|
102
|
-
});
|
|
78
|
+
if (!version || parseInt(minor) > 13) {
|
|
79
|
+
const {payments} = await getFailedPayments({lnd});
|
|
103
80
|
|
|
104
|
-
|
|
105
|
-
throw new Error('ExpectedRouteFromProbe');
|
|
81
|
+
strictSame(payments, [], 'Probes do not leave a failed state behind');
|
|
106
82
|
}
|
|
107
83
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const {secret} = await payViaRoutes({
|
|
116
|
-
lnd,
|
|
117
|
-
id: invoice.id,
|
|
118
|
-
routes: [route],
|
|
84
|
+
// Create a new channel to increase total edge liquidity
|
|
85
|
+
await setupChannel({
|
|
86
|
+
capacity: channelCapacityTokens,
|
|
87
|
+
lnd: target.lnd,
|
|
88
|
+
generate: target.generate,
|
|
89
|
+
to: remote,
|
|
119
90
|
});
|
|
120
91
|
|
|
121
|
-
|
|
92
|
+
await deleteForwardingReputations({lnd});
|
|
93
|
+
|
|
94
|
+
await waitForRoute({lnd, destination: remote.id, tokens: invoice.tokens});
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const {route} = await probeForRoute({
|
|
98
|
+
lnd,
|
|
99
|
+
destination: remote.id,
|
|
100
|
+
payment: invoice.payment,
|
|
101
|
+
tokens: invoice.tokens,
|
|
102
|
+
total_mtokens: !!invoice.payment ? invoice.mtokens : undefined,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!route) {
|
|
106
|
+
throw new Error('ExpectedRouteFromProbe');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
equal(route.fee, 1, 'Found route fee');
|
|
110
|
+
equal(route.fee_mtokens, '1500', 'Found route fee mtokens');
|
|
111
|
+
strictSame(route.hops.length, 2, 'Found route hops returned');
|
|
112
|
+
equal(route.mtokens, '500001500', 'Found route mtokens');
|
|
113
|
+
equal(route.timeout >= 400, true, 'Found route timeout');
|
|
114
|
+
equal(route.tokens, 500001, 'Found route tokens');
|
|
115
|
+
|
|
116
|
+
const {secret} = await payViaRoutes({
|
|
117
|
+
lnd,
|
|
118
|
+
id: invoice.id,
|
|
119
|
+
routes: [route],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
equal(secret, invoice.secret, 'Route works');
|
|
123
|
+
} catch (err) {
|
|
124
|
+
equal(err, null, 'No error when probing for route');
|
|
125
|
+
}
|
|
122
126
|
} catch (err) {
|
|
123
|
-
equal(err, null, '
|
|
127
|
+
equal(err, null, 'Expected no error');
|
|
128
|
+
} finally {
|
|
129
|
+
await kill({});
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
await kill({});
|
|
127
|
-
|
|
128
132
|
return end();
|
|
129
133
|
});
|
|
@@ -20,8 +20,8 @@ const {waitForRoute} = require('./../macros');
|
|
|
20
20
|
const size = 3;
|
|
21
21
|
const tokens = 100;
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
test(`
|
|
23
|
+
// Subscribing to forward requests should intercept forwards
|
|
24
|
+
test(`Subscribe to requests`, async ({end, equal, rejects, strictSame}) => {
|
|
25
25
|
const {kill, nodes} = await spawnLightningCluster({size});
|
|
26
26
|
|
|
27
27
|
const [{generate, lnd}, target, remote] = nodes;
|