Skip to content

Commit e93a4c8

Browse files
paulbalajiclaude
andauthored
feat(cli): add multi-VM support to warp send command (#7819)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5caac66 commit e93a4c8

29 files changed

Lines changed: 1310 additions & 213 deletions

File tree

.changeset/multi-vm-warp-send.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+
Added multi-VM support to `hyperlane warp send` command. The command now supports transfers across all WarpCore-supported VMs including EVM, Sealevel (Solana), Cosmos, CosmosNative, Starknet, and Radix. Non-EVM destinations use Explorer GraphQL polling for delivery verification with automatic fallback to on-chain polling. Self-relay is only supported for EVM destinations and will warn/skip otherwise.

.changeset/warm-lemons-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/sdk': patch
3+
---
4+
5+
Fixed Tron EthersV5 provider to use TronJsonRpcProvider (which appends `/jsonrpc` to the RPC URL) instead of HyperlaneSmartProvider, preventing 302 redirect failures on Tron nodes.

.github/workflows/test-cli-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ jobs:
301301
test:
302302
- warp-apply
303303
- warp-deploy
304+
- warp-send
304305
steps:
305306
- uses: actions/checkout@v6
306307
with:
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Cross-Chain Core Configs
2+
3+
These core configs are specifically designed for **cross-chain e2e tests** that involve multiple VM types (EVM, CosmosNative, Radix, Sealevel).
4+
5+
## Why separate configs?
6+
7+
The standard example configs (in `../cosmosnative/`, `../radix/`, etc.) are minimal and protocol-specific. Cross-VM warp route deployments require:
8+
9+
1. **IGP destination gas configs** for all remote chains - without these, `MsgEnrollRemoteRouter` fails on AltVMs because the remote domain isn't registered as "supported"
10+
2. **Consistent hook types** across VMs for interoperability testing
11+
12+
These configs pre-register all test chains (`hyp1-3`, `anvil1-4`, `radix1-2`, `sealevel1`) in their IGP `oracleConfig` and `overhead` settings.
13+
14+
## Supported test chains
15+
16+
| Chain | Protocol | Domain ID | Native Token Decimals |
17+
| --------- | ------------ | ---------- | --------------------- |
18+
| hyp1 | CosmosNative | 758986691 | 6 |
19+
| hyp2 | CosmosNative | 758986692 | 6 |
20+
| hyp3 | CosmosNative | 758986693 | 6 |
21+
| anvil1 | Ethereum | 31337 | 18 |
22+
| anvil2 | Ethereum | 31338 | 18 |
23+
| anvil3 | Ethereum | 31347 | 18 |
24+
| anvil4 | Ethereum | 31348 | 18 |
25+
| radix1 | Radix | 1421493353 | 18 |
26+
| radix2 | Radix | 1421493354 | 18 |
27+
| sealevel1 | Sealevel | 1399811149 | 9 |
28+
29+
## Usage
30+
31+
Cross-chain tests reference these configs via `CROSS_CHAIN_CORE_CONFIG_PATH_BY_PROTOCOL` in `src/tests/constants.ts`. To add a new cross-VM test, use these configs instead of the standard examples.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
defaultHook:
2+
beneficiary: hyp1jq304cthpx0lwhpqzrdjrcza559ukyy3sc4dw5
3+
oracleConfig:
4+
# Cosmos chains (6 decimals)
5+
hyp1:
6+
gasPrice: '1'
7+
tokenDecimals: 6
8+
tokenExchangeRate: '10000000000'
9+
hyp2:
10+
gasPrice: '1'
11+
tokenDecimals: 6
12+
tokenExchangeRate: '10000000000'
13+
hyp3:
14+
gasPrice: '1'
15+
tokenDecimals: 6
16+
tokenExchangeRate: '10000000000'
17+
# EVM chains (18 decimals)
18+
anvil1:
19+
gasPrice: '1'
20+
tokenDecimals: 18
21+
tokenExchangeRate: '10000000000'
22+
anvil2:
23+
gasPrice: '1'
24+
tokenDecimals: 18
25+
tokenExchangeRate: '10000000000'
26+
anvil3:
27+
gasPrice: '1'
28+
tokenDecimals: 18
29+
tokenExchangeRate: '10000000000'
30+
anvil4:
31+
gasPrice: '1'
32+
tokenDecimals: 18
33+
tokenExchangeRate: '10000000000'
34+
# Radix chains (18 decimals)
35+
radix1:
36+
gasPrice: '1'
37+
tokenDecimals: 18
38+
tokenExchangeRate: '10000000000'
39+
radix2:
40+
gasPrice: '1'
41+
tokenDecimals: 18
42+
tokenExchangeRate: '10000000000'
43+
# Sealevel chains (9 decimals)
44+
sealevel1:
45+
gasPrice: '1'
46+
tokenDecimals: 9
47+
tokenExchangeRate: '10000000000'
48+
oracleKey: hyp1jq304cthpx0lwhpqzrdjrcza559ukyy3sc4dw5
49+
overhead:
50+
hyp1: 200000
51+
hyp2: 200000
52+
hyp3: 200000
53+
anvil1: 200000
54+
anvil2: 200000
55+
anvil3: 200000
56+
anvil4: 200000
57+
radix1: 200000
58+
radix2: 200000
59+
sealevel1: 200000
60+
owner: hyp1jq304cthpx0lwhpqzrdjrcza559ukyy3sc4dw5
61+
type: interchainGasPaymaster
62+
defaultIsm:
63+
threshold: 1
64+
type: merkleRootMultisigIsm
65+
validators:
66+
- '0x0c60e7eCd06429052223C78452F791AAb5C5CAc6'
67+
owner: hyp1jq304cthpx0lwhpqzrdjrcza559ukyy3sc4dw5
68+
requiredHook:
69+
type: merkleTreeHook
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# A config to define the core contract deployments (crosschain tests)
2+
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
3+
defaultIsm:
4+
type: 'testIsm'
5+
threshold: 1 # Number: Signatures required to approve a message
6+
validators: # Array: List of validator addresses
7+
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
8+
defaultHook:
9+
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
10+
oracleConfig:
11+
# Cosmos chains (6 decimals)
12+
hyp1:
13+
gasPrice: '1'
14+
tokenDecimals: 6
15+
tokenExchangeRate: '10000000000'
16+
hyp2:
17+
gasPrice: '1'
18+
tokenDecimals: 6
19+
tokenExchangeRate: '10000000000'
20+
hyp3:
21+
gasPrice: '1'
22+
tokenDecimals: 6
23+
tokenExchangeRate: '10000000000'
24+
# EVM chains (18 decimals)
25+
anvil1:
26+
gasPrice: '1'
27+
tokenDecimals: 18
28+
tokenExchangeRate: '10000000000'
29+
anvil2:
30+
gasPrice: '1'
31+
tokenDecimals: 18
32+
tokenExchangeRate: '10000000000'
33+
anvil3:
34+
gasPrice: '1'
35+
tokenDecimals: 18
36+
tokenExchangeRate: '10000000000'
37+
anvil4:
38+
gasPrice: '1'
39+
tokenDecimals: 18
40+
tokenExchangeRate: '10000000000'
41+
# Radix chains (18 decimals)
42+
radix1:
43+
gasPrice: '1'
44+
tokenDecimals: 18
45+
tokenExchangeRate: '10000000000'
46+
radix2:
47+
gasPrice: '1'
48+
tokenDecimals: 18
49+
tokenExchangeRate: '10000000000'
50+
# Sealevel chains (9 decimals)
51+
sealevel1:
52+
gasPrice: '1'
53+
tokenDecimals: 9
54+
tokenExchangeRate: '10000000000'
55+
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
56+
overhead:
57+
hyp1: 200000
58+
hyp2: 200000
59+
hyp3: 200000
60+
anvil1: 200000
61+
anvil2: 200000
62+
anvil3: 200000
63+
anvil4: 200000
64+
radix1: 200000
65+
radix2: 200000
66+
sealevel1: 200000
67+
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
68+
type: interchainGasPaymaster
69+
requiredHook:
70+
type: merkleTreeHook
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
defaultHook:
2+
beneficiary: account_loc12ytsy99ajzkwy7ce0444fs8avat7jy3fkj5mk64yz2z3yml6s7y7x3
3+
oracleConfig:
4+
# Radix chains (18 decimals)
5+
radix1:
6+
tokenExchangeRate: '347026904130352406214'
7+
gasPrice: '201383436'
8+
tokenDecimals: 18
9+
radix2:
10+
tokenExchangeRate: '347026904130352406214'
11+
gasPrice: '201383436'
12+
tokenDecimals: 18
13+
# Cosmos chains (6 decimals)
14+
hyp1:
15+
tokenExchangeRate: '10000000000'
16+
gasPrice: '1'
17+
tokenDecimals: 6
18+
hyp2:
19+
tokenExchangeRate: '10000000000'
20+
gasPrice: '1'
21+
tokenDecimals: 6
22+
hyp3:
23+
tokenExchangeRate: '10000000000'
24+
gasPrice: '1'
25+
tokenDecimals: 6
26+
# EVM chains (18 decimals)
27+
anvil1:
28+
tokenExchangeRate: '10000000000'
29+
gasPrice: '1'
30+
tokenDecimals: 18
31+
anvil2:
32+
tokenExchangeRate: '10000000000'
33+
gasPrice: '1'
34+
tokenDecimals: 18
35+
anvil3:
36+
tokenExchangeRate: '10000000000'
37+
gasPrice: '1'
38+
tokenDecimals: 18
39+
anvil4:
40+
tokenExchangeRate: '10000000000'
41+
gasPrice: '1'
42+
tokenDecimals: 18
43+
# Sealevel chains (9 decimals)
44+
sealevel1:
45+
tokenExchangeRate: '10000000000'
46+
gasPrice: '1'
47+
tokenDecimals: 9
48+
oracleKey: account_loc12ytsy99ajzkwy7ce0444fs8avat7jy3fkj5mk64yz2z3yml6s7y7x3
49+
overhead:
50+
radix1: 100
51+
radix2: 100
52+
hyp1: 200000
53+
hyp2: 200000
54+
hyp3: 200000
55+
anvil1: 200000
56+
anvil2: 200000
57+
anvil3: 200000
58+
anvil4: 200000
59+
sealevel1: 200000
60+
owner: account_loc12ytsy99ajzkwy7ce0444fs8avat7jy3fkj5mk64yz2z3yml6s7y7x3
61+
type: interchainGasPaymaster
62+
defaultIsm:
63+
domains:
64+
radix1:
65+
domains:
66+
radix2:
67+
type: testIsm
68+
owner: account_loc12ytsy99ajzkwy7ce0444fs8avat7jy3fkj5mk64yz2z3yml6s7y7x3
69+
type: domainRoutingIsm
70+
71+
radix2:
72+
validators: ['0x10E0271ec47d55511a047516f2a7301801d55eaB']
73+
threshold: 1
74+
type: messageIdMultisigIsm
75+
76+
hyp1:
77+
type: testIsm
78+
hyp2:
79+
type: testIsm
80+
hyp3:
81+
type: testIsm
82+
anvil1:
83+
type: testIsm
84+
anvil2:
85+
type: testIsm
86+
anvil3:
87+
type: testIsm
88+
anvil4:
89+
type: testIsm
90+
sealevel1:
91+
type: testIsm
92+
owner: account_loc12ytsy99ajzkwy7ce0444fs8avat7jy3fkj5mk64yz2z3yml6s7y7x3
93+
type: domainRoutingIsm
94+
owner: account_loc12ytsy99ajzkwy7ce0444fs8avat7jy3fkj5mk64yz2z3yml6s7y7x3
95+
requiredHook:
96+
type: merkleTreeHook

typescript/cli/src/commands/warp.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { RebalancerConfig, RebalancerService } from '@hyperlane-xyz/rebalancer';
66
import {
77
type RawForkedChainConfigByChain,
88
RawForkedChainConfigByChainSchema,
9-
type WarpCoreConfig,
109
expandVirtualWarpDeployConfig,
1110
expandWarpDeployConfig,
1211
getRouterAddressesFromWarpCoreConfig,
@@ -52,6 +51,7 @@ import {
5251
removeTrailingSlash,
5352
writeYamlOrJson,
5453
} from '../utils/files.js';
54+
import { getOrderedWarpSendChains } from '../utils/warp-send.js';
5555
import {
5656
filterWarpConfigsToMatchingChains,
5757
getWarpConfigs,
@@ -354,7 +354,6 @@ const send: CommandModuleWithWriteContext<
354354
skipValidation?: boolean;
355355
sourceToken?: string;
356356
destinationToken?: string;
357-
preResolvedWarpCoreConfig?: WarpCoreConfig;
358357
}
359358
> = {
360359
command: 'send',
@@ -408,14 +407,13 @@ const send: CommandModuleWithWriteContext<
408407
skipValidation,
409408
sourceToken,
410409
destinationToken,
411-
preResolvedWarpCoreConfig,
412410
}) => {
413411
const filterChains = [origin, destination, ...(chainsArg || [])]
414412
.filter((v): v is string => Boolean(v))
415413
.filter((v, i, a) => a.indexOf(v) === i);
416414

417415
const warpCoreConfig =
418-
preResolvedWarpCoreConfig ??
416+
context.warpCoreConfig ??
419417
(await getWarpCoreConfigOrExit({
420418
warpRouteId,
421419
context,
@@ -448,12 +446,10 @@ const send: CommandModuleWithWriteContext<
448446
if (origin && destination) {
449447
chains = [origin, destination];
450448
} else {
451-
// Order EVM chains first so non-EVM chains are final destinations
452-
const orderedDefaultChains = [...supportedChains].sort((a, b) => {
453-
const aEvm = isEVMLike(context.multiProvider.getProtocol(a)) ? 0 : 1;
454-
const bEvm = isEVMLike(context.multiProvider.getProtocol(b)) ? 0 : 1;
455-
return aEvm - bEvm || a.localeCompare(b);
456-
});
449+
const orderedDefaultChains = getOrderedWarpSendChains(
450+
supportedChains,
451+
context.multiProvider,
452+
);
457453

458454
chains =
459455
chains.length === 0

typescript/cli/src/config/chain.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ export async function createChainConfig({
138138
sortMapEntries: true,
139139
});
140140
log(indentYamlOrJson(metadataYaml, 4));
141-
await context.registry.updateChain({ chainName: metadata.name, metadata });
141+
await context.registry.updateChain({
142+
chainName: metadata.name,
143+
metadata,
144+
});
142145
} else {
143146
errorRed(
144147
`Chain config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,
@@ -165,7 +168,9 @@ async function addBlockExplorerConfig(metadata: ChainMetadata): Promise<void> {
165168
});
166169
const family = (await select({
167170
message: 'Select the type (family) of block explorer:',
168-
choices: Object.entries(ExplorerFamily).map(([_, value]) => ({ value })),
171+
choices: Object.entries(ExplorerFamily).map(([_, value]) => ({
172+
value,
173+
})),
169174
pageSize: 10,
170175
})) as ExplorerFamily;
171176
const apiKey =

typescript/cli/src/context/context.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,13 @@ export async function signerMiddleware(argv: Record<string, any>) {
9696
const protocol = multiProvider.getProtocol(chain);
9797
const metadata = multiProvider.getChainMetadata(chain);
9898

99-
if (hasProtocol(protocol))
100-
altVmProviders[chain] =
99+
if (hasProtocol(protocol)) {
100+
const provider =
101101
await getProtocolProvider(protocol).createProvider(metadata);
102+
altVmProviders[chain] = provider;
103+
// multiProtocolProvider keeps its own typed providers from metadata/rpcUrls.
104+
// Avoid injecting AltVM.IProvider here because it requires unsafe casting.
105+
}
102106
}),
103107
);
104108

0 commit comments

Comments
 (0)