Skip to content

Commit 9e5dbfd

Browse files
authored
feat: warp send with --chains (#7198)
1 parent 6ef5f44 commit 9e5dbfd

7 files changed

Lines changed: 215 additions & 143 deletions

File tree

.changeset/curly-garlics-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperlane-xyz/cli": minor
3+
---
4+
5+
Update `hyperlane warp send` to include `--chains` parameter to input which chains to send to

typescript/cli/src/commands/warp.ts

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ import { stringify as yamlStringify } from 'yaml';
33
import { CommandModule } from 'yargs';
44

55
import {
6-
ChainName,
76
RawForkedChainConfigByChain,
87
RawForkedChainConfigByChainSchema,
98
expandVirtualWarpDeployConfig,
109
expandWarpDeployConfig,
1110
getRouterAddressesFromWarpCoreConfig,
1211
} from '@hyperlane-xyz/sdk';
13-
import { ProtocolType, assert, objFilter } from '@hyperlane-xyz/utils';
12+
import {
13+
ProtocolType,
14+
assert,
15+
difference,
16+
intersection,
17+
objFilter,
18+
} from '@hyperlane-xyz/utils';
1419

1520
import { runWarpRouteCheck } from '../check/warp.js';
1621
import { createWarpRouteDeployConfig } from '../config/warp.js';
@@ -33,7 +38,6 @@ import { getWarpRouteConfigsByCore, runWarpRouteRead } from '../read/warp.js';
3338
import { RebalancerRunner } from '../rebalancer/runner.js';
3439
import { sendTestTransfer } from '../send/transfer.js';
3540
import { ExtendedChainSubmissionStrategySchema } from '../submitters/types.js';
36-
import { runSingleChainSelectionStep } from '../utils/chains.js';
3741
import {
3842
indentYamlOrJson,
3943
readYamlOrJson,
@@ -258,6 +262,7 @@ const send: CommandModuleWithWriteContext<
258262
router?: string;
259263
amount: string;
260264
recipient?: string;
265+
chains?: string;
261266
}
262267
> = {
263268
command: 'send',
@@ -274,6 +279,12 @@ const send: CommandModuleWithWriteContext<
274279
type: 'string',
275280
description: 'Token recipient address (defaults to sender)',
276281
},
282+
chains: {
283+
type: 'string',
284+
description: 'Comma separated list of chains to send messages to',
285+
demandOption: false,
286+
conflicts: ['origin', 'destination'],
287+
},
277288
},
278289
handler: async ({
279290
context,
@@ -287,43 +298,41 @@ const send: CommandModuleWithWriteContext<
287298
amount,
288299
recipient,
289300
roundTrip,
301+
chains: chainsAsString,
290302
}) => {
291303
const warpCoreConfig = await getWarpCoreConfigOrExit({
292304
symbol,
293305
warp,
294306
context,
295307
});
308+
const chainsToSend = chainsAsString?.split(',').map((_) => _.trim());
309+
let chains = chainsToSend || [];
296310

297-
let chains: ChainName[] = warpCoreConfig.tokens.map((t) => t.chainName);
298-
if (roundTrip) {
299-
// Appends the reverse of the array, excluding the 1st (e.g. [1,2,3] becomes [1,2,3,2,1])
300-
const reversed = [...chains].reverse().slice(1, chains.length + 1); // We make a copy because .reverse() is mutating
301-
chains.push(...reversed);
302-
} else {
303-
// Assume we want to use use `--origin` and `--destination` params, prompt as needed.
304-
const chainMetadata = objFilter(
305-
context.chainMetadata,
306-
(key, _metadata): _metadata is any => chains.includes(key),
307-
);
311+
if (origin && destination) {
312+
chains.push(origin);
313+
chains.push(destination);
314+
}
308315

309-
if (!origin)
310-
origin = await runSingleChainSelectionStep(
311-
chainMetadata,
312-
'Select the origin chain:',
313-
);
316+
const supportedChains = new Set(
317+
warpCoreConfig.tokens.map((t) => t.chainName),
318+
);
314319

315-
if (!destination)
316-
destination = await runSingleChainSelectionStep(
317-
chainMetadata,
318-
'Select the destination chain:',
319-
);
320+
const unsupportedChains = difference(
321+
new Set([...(chainsToSend || []), origin, destination].filter(Boolean)),
322+
supportedChains,
323+
);
324+
assert(
325+
unsupportedChains.size === 0,
326+
`Chain(s) ${[...unsupportedChains].join(', ')} are not part of the warp route.`,
327+
);
320328

321-
chains = [origin, destination].filter((c) => chains.includes(c));
329+
chains = [...intersection(new Set(chains), supportedChains)];
330+
assert(chains.length > 1, `Not enough chains to send messages.`);
322331

323-
assert(
324-
chains.length === 2,
325-
`Origin (${origin}) or destination (${destination}) are not part of the warp route.`,
326-
);
332+
if (roundTrip) {
333+
// Appends the reverse of the array, excluding the 1st (e.g. [1,2,3] becomes [1,2,3,2,1])
334+
const reversed = [...chains].reverse().slice(1, chains.length + 1);
335+
chains = [...chains, ...reversed];
327336
}
328337

329338
logBlue(`🚀 Sending a message for chains: ${chains.join(' ➡️ ')}`);

typescript/cli/src/tests/ethereum/commands/warp.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -232,23 +232,35 @@ export function hyperlaneWarpCheck(
232232
});
233233
}
234234

235-
export function hyperlaneWarpSendRelay(
236-
origin: string,
237-
destination: string,
238-
warpCorePath: string,
235+
export function hyperlaneWarpSendRelay({
236+
origin,
237+
destination,
238+
warpCorePath,
239239
relay = true,
240-
value: number | string = 1,
241-
): ProcessPromise {
240+
value = 1,
241+
chains,
242+
roundTrip,
243+
}: {
244+
origin?: string;
245+
destination?: string;
246+
warpCorePath: string;
247+
relay?: boolean;
248+
value?: number | string;
249+
chains?: string;
250+
roundTrip?: boolean;
251+
}): ProcessPromise {
242252
return $`${localTestRunCmdPrefix()} hyperlane warp send \
243253
${relay ? '--relay' : []} \
244254
--registry ${REGISTRY_PATH} \
245-
--origin ${origin} \
246-
--destination ${destination} \
255+
${origin ? ['--origin', origin] : []} \
256+
${destination ? ['--destination', destination] : []} \
247257
--warp ${warpCorePath} \
248258
--key ${ANVIL_KEY} \
249259
--verbosity debug \
250260
--yes \
251-
--amount ${value}`;
261+
--amount ${value} \
262+
${chains ? ['--chains', chains] : []} \
263+
${roundTrip ? ['--round-trip'] : []} `;
252264
}
253265

254266
export function hyperlaneWarpRebalancer(
@@ -640,6 +652,14 @@ export async function sendWarpRouteMessageRoundTrip(
640652
chain2: string,
641653
warpCoreConfigPath: string,
642654
) {
643-
await hyperlaneWarpSendRelay(chain1, chain2, warpCoreConfigPath);
644-
return hyperlaneWarpSendRelay(chain2, chain1, warpCoreConfigPath);
655+
await hyperlaneWarpSendRelay({
656+
origin: chain1,
657+
destination: chain2,
658+
warpCorePath: warpCoreConfigPath,
659+
});
660+
return hyperlaneWarpSendRelay({
661+
origin: chain2,
662+
destination: chain1,
663+
warpCorePath: warpCoreConfigPath,
664+
});
645665
}

typescript/cli/src/tests/ethereum/relay.e2e-test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,18 @@ describe('hyperlane relayer e2e tests', async function () {
6464
WARP_DEPLOY_OUTPUT,
6565
);
6666

67-
await hyperlaneWarpSendRelay(
68-
CHAIN_NAME_2,
69-
CHAIN_NAME_3,
70-
WARP_DEPLOY_OUTPUT,
71-
false,
72-
);
73-
await hyperlaneWarpSendRelay(
74-
CHAIN_NAME_3,
75-
CHAIN_NAME_2,
76-
WARP_DEPLOY_OUTPUT,
77-
false,
78-
);
67+
await hyperlaneWarpSendRelay({
68+
origin: CHAIN_NAME_2,
69+
destination: CHAIN_NAME_3,
70+
warpCorePath: WARP_DEPLOY_OUTPUT,
71+
relay: false,
72+
});
73+
await hyperlaneWarpSendRelay({
74+
origin: CHAIN_NAME_3,
75+
destination: CHAIN_NAME_2,
76+
warpCorePath: WARP_DEPLOY_OUTPUT,
77+
relay: false,
78+
});
7979

8080
await process.kill('SIGINT');
8181
});

typescript/cli/src/tests/ethereum/warp/warp-deploy.e2e-test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -708,28 +708,28 @@ describe('hyperlane warp deploy e2e tests', async function () {
708708
await hyperlaneWarpDeploy(WARP_DEPLOY_OUTPUT_PATH);
709709

710710
// Try to send a transaction with the origin destination
711-
const { stdout: chain2Tochain3Stdout } = await hyperlaneWarpSendRelay(
712-
CHAIN_NAME_2,
713-
CHAIN_NAME_3,
714-
WARP_CORE_CONFIG_PATH_2_3,
715-
);
711+
const { stdout: chain2Tochain3Stdout } = await hyperlaneWarpSendRelay({
712+
origin: CHAIN_NAME_2,
713+
destination: CHAIN_NAME_3,
714+
warpCorePath: WARP_CORE_CONFIG_PATH_2_3,
715+
});
716716
expect(chain2Tochain3Stdout).to.include('anvil2 ➡️ anvil3');
717717

718718
// Send another message with swapped origin destination
719-
const { stdout: chain3Tochain2Stdout } = await hyperlaneWarpSendRelay(
720-
CHAIN_NAME_3,
721-
CHAIN_NAME_2,
722-
WARP_CORE_CONFIG_PATH_2_3,
723-
);
719+
const { stdout: chain3Tochain2Stdout } = await hyperlaneWarpSendRelay({
720+
origin: CHAIN_NAME_3,
721+
destination: CHAIN_NAME_2,
722+
warpCorePath: WARP_CORE_CONFIG_PATH_2_3,
723+
});
724724
expect(chain3Tochain2Stdout).to.include('anvil3 ➡️ anvil2');
725725

726726
// Should throw if invalid origin or destination
727-
await hyperlaneWarpSendRelay(
728-
'anvil1',
729-
CHAIN_NAME_3,
730-
WARP_CORE_CONFIG_PATH_2_3,
731-
).should.be.rejectedWith(
732-
'Error: Origin (anvil1) or destination (anvil3) are not part of the warp route.',
727+
await hyperlaneWarpSendRelay({
728+
origin: 'anvil1',
729+
destination: CHAIN_NAME_3,
730+
warpCorePath: WARP_CORE_CONFIG_PATH_2_3,
731+
}).should.be.rejectedWith(
732+
'Error: Chain(s) anvil1 are not part of the warp route.',
733733
);
734734
});
735735

typescript/cli/src/tests/ethereum/warp/warp-rebalancer.e2e-test.ts

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,21 @@ describe('hyperlane warp rebalancer e2e tests', async function () {
160160
console.log('Bridging tokens...');
161161

162162
await Promise.all([
163-
hyperlaneWarpSendRelay(
164-
CHAIN_NAME_2,
165-
CHAIN_NAME_4,
166-
warpCoreConfigPath,
167-
true,
168-
toWei(10),
169-
),
163+
hyperlaneWarpSendRelay({
164+
origin: CHAIN_NAME_2,
165+
destination: CHAIN_NAME_4,
166+
warpCorePath: warpCoreConfigPath,
167+
relay: true,
168+
value: toWei(10),
169+
}),
170170
sleep(2000).then(() =>
171-
hyperlaneWarpSendRelay(
172-
CHAIN_NAME_3,
173-
CHAIN_NAME_4,
174-
warpCoreConfigPath,
175-
true,
176-
toWei(10),
177-
),
171+
hyperlaneWarpSendRelay({
172+
origin: CHAIN_NAME_3,
173+
destination: CHAIN_NAME_4,
174+
warpCorePath: warpCoreConfigPath,
175+
relay: true,
176+
value: toWei(10),
177+
}),
178178
),
179179
]);
180180
});
@@ -1235,13 +1235,13 @@ describe('hyperlane warp rebalancer e2e tests', async function () {
12351235
// This process locks tokens on the destination chain and unlocks them on the origin,
12361236
// effectively increasing collateral on the destination while decreasing it on the origin,
12371237
// which achieves the desired rebalancing effect.
1238-
await hyperlaneWarpSendRelay(
1239-
destName,
1240-
originName,
1241-
warpCoreConfigPath,
1242-
true,
1243-
sentTransferRemote.amount.toString(),
1244-
);
1238+
await hyperlaneWarpSendRelay({
1239+
origin: destName,
1240+
destination: originName,
1241+
warpCorePath: warpCoreConfigPath,
1242+
relay: true,
1243+
value: sentTransferRemote.amount.toString(),
1244+
});
12451245

12461246
originBalance = await originTkn.balanceOf(originContractAddress);
12471247
destBalance = await destTkn.balanceOf(destContractAddress);
@@ -1701,13 +1701,13 @@ describe('hyperlane warp rebalancer e2e tests', async function () {
17011701
// This process locks tokens on the destination chain and unlocks them on the origin,
17021702
// effectively increasing collateral on the destination while decreasing it on the origin,
17031703
// which achieves the desired rebalancing effect.
1704-
await hyperlaneWarpSendRelay(
1705-
destName,
1706-
originName,
1707-
warpCoreConfigPath,
1708-
true,
1709-
sentTransferRemote.amount.toString(),
1710-
);
1704+
await hyperlaneWarpSendRelay({
1705+
origin: destName,
1706+
destination: originName,
1707+
warpCorePath: warpCoreConfigPath,
1708+
relay: true,
1709+
value: sentTransferRemote.amount.toString(),
1710+
});
17111711

17121712
originBalance = await originTkn.balanceOf(originContractAddress);
17131713
destBalance = await destTkn.balanceOf(destContractAddress);

0 commit comments

Comments
 (0)