Skip to content

Commit d98449d

Browse files
committed
feat(sdk): use routing ISM setBatch/removeBatch when available
Adds a PACKAGE_VERSION gate on the target routing ISM. When it reports >= 11.4.0 (where setBatch/removeBatch were introduced), consolidate enrollments and unenrollments into chunked setBatch/removeBatch txs sized by domainRoutingInitializationSize(destination). Older ISMs fall back to per-domain set()/remove() loops. Applies to three paths: - Reconfigure-existing-ISM enrollments and unenrollments - Post-initializer follow-on enrollments on new DomainRoutingIsm / IncrementalDomainRoutingIsm deploys - DefaultFallbackRoutingIsm new deploys (initialize with initial batch + setBatch remainder, matching the sharding already done on the proxy-factory path)
1 parent 38d81ab commit d98449d

2 files changed

Lines changed: 248 additions & 39 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/sdk': minor
3+
---
4+
5+
Wired `HyperlaneIsmFactory` to use the routing ISM `setBatch` and `removeBatch` functions added in `@hyperlane-xyz/core` 11.4.0. Enrollments and unenrollments are now consolidated into chunked batched transactions sized by the per-chain `domainRoutingInitializationSize`, avoiding one tx per domain on low-capacity chains (citrea, shibarium, tempo, etc.). Version-gated via `PACKAGE_VERSION()` on the target routing ISM, with a per-domain fallback for older deployed ISMs.

typescript/sdk/src/ism/HyperlaneIsmFactory.ts

Lines changed: 243 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { compareVersions } from 'compare-versions';
12
import { ethers } from 'ethers';
23
import { Logger } from 'pino';
34

@@ -31,6 +32,7 @@ import {
3132
TrustedRelayerIsm__factory,
3233
ZKSyncArtifact,
3334
} from '@hyperlane-xyz/core';
35+
3436
import {
3537
Address,
3638
Domain,
@@ -84,6 +86,32 @@ const ismFactories = {
8486
[IsmType.CCIP]: new CCIPIsm__factory(),
8587
};
8688

89+
type RoutingIsmContract =
90+
| DomainRoutingIsm
91+
| IncrementalDomainRoutingIsm
92+
| DefaultFallbackRoutingIsm;
93+
94+
// First @hyperlane-xyz/core version with setBatch/removeBatch on
95+
// DomainRoutingIsm. Routing ISMs at or above this version let the SDK
96+
// consolidate per-domain txs into chunked setBatch/removeBatch calls;
97+
// older ISMs fall back to the per-domain loop.
98+
const ROUTING_ISM_BATCH_MIN_VERSION = '11.4.0';
99+
100+
const routingIsmSupportsBatch = async (
101+
address: Address,
102+
provider: ethers.providers.Provider,
103+
): Promise<boolean> => {
104+
try {
105+
const version = await DomainRoutingIsm__factory.connect(
106+
address,
107+
provider,
108+
).PACKAGE_VERSION();
109+
return compareVersions(version, ROUTING_ISM_BATCH_MIN_VERSION) >= 0;
110+
} catch {
111+
return false;
112+
}
113+
};
114+
87115
const domainRoutingInitializationSize = (destination: ChainName) => {
88116
if (destination === 'tempo') {
89117
return 30;
@@ -541,7 +569,10 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
541569
existingIsmAddress,
542570
this.multiProvider.getSigner(destination),
543571
);
572+
const batchSize = domainRoutingInitializationSize(destination);
573+
544574
// deploying all the ISMs which have to be updated
575+
const enrollAddresses: Address[] = [];
545576
for (const originDomain of delta.domainsToEnroll) {
546577
const origin = this.multiProvider.getChainName(originDomain); // already filtered to only include domains in the multiprovider
547578
logger.debug(
@@ -554,21 +585,25 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
554585
mailbox,
555586
});
556587
isms[originDomain] = ism.address;
557-
const tx = await routingIsm.set(
558-
originDomain,
559-
isms[originDomain],
560-
overrides,
561-
);
562-
await this.multiProvider.handleTx(destination, tx);
563-
}
564-
// unenrolling domains if needed
565-
for (const originDomain of delta.domainsToUnenroll) {
566-
logger.debug(
567-
`Unenrolling originDomain ${originDomain} from preexisting routing ISM at ${existingIsmAddress}...`,
568-
);
569-
const tx = await routingIsm.remove(originDomain, overrides);
570-
await this.multiProvider.handleTx(destination, tx);
588+
enrollAddresses.push(ism.address);
571589
}
590+
await this.enrollDomains({
591+
routingIsm,
592+
domains: delta.domainsToEnroll,
593+
addresses: enrollAddresses,
594+
batchSize,
595+
overrides,
596+
destination,
597+
logger,
598+
});
599+
await this.unenrollDomains({
600+
routingIsm,
601+
domains: delta.domainsToUnenroll,
602+
batchSize,
603+
overrides,
604+
destination,
605+
logger,
606+
});
572607
// transfer ownership if needed
573608
if (delta.owner) {
574609
logger.debug(`Transferring ownership of routing ISM...`);
@@ -603,16 +638,31 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
603638
await getZKSyncArtifactByContractName(config.type),
604639
);
605640
// TODO: Should verify contract here
606-
logger.debug('Initialising fallback routing ISM ...');
641+
const batchSize = domainRoutingInitializationSize(destination);
642+
const initialBatchSize = Math.min(batchSize, safeConfigDomains.length);
643+
const initialDomains = safeConfigDomains.slice(0, initialBatchSize);
644+
const initialAddresses = submoduleAddresses.slice(0, initialBatchSize);
645+
logger.debug(
646+
`Initialising fallback routing ISM with ${initialBatchSize} domains on ${destination}`,
647+
);
607648
receipt = await this.multiProvider.handleTx(
608649
destination,
609650
routingIsm['initialize(address,uint32[],address[])'](
610651
config.owner,
611-
safeConfigDomains,
612-
submoduleAddresses,
652+
initialDomains,
653+
initialAddresses,
613654
overrides,
614655
),
615656
);
657+
await this.enrollDomains({
658+
routingIsm,
659+
domains: safeConfigDomains.slice(initialBatchSize),
660+
addresses: submoduleAddresses.slice(initialBatchSize),
661+
batchSize,
662+
overrides,
663+
destination,
664+
logger,
665+
});
616666
} else {
617667
// deploying new domain routing ISM
618668
const owner = config.owner;
@@ -704,28 +754,15 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
704754

705755
// Enroll remaining domains and addresses
706756
// If all domains are enrolled already, this is a no-op
707-
for (let i = initialBatchSize; i < safeConfigDomains.length; i++) {
708-
const estimatedGas = await routingIsm.estimateGas.set(
709-
safeConfigDomains[i],
710-
submoduleAddresses[i],
711-
overrides,
712-
);
713-
const chainName = this.multiProvider.getChainName(
714-
safeConfigDomains[i],
715-
);
716-
this.logger.debug(
717-
`Enrolling ${chainName} (${safeConfigDomains[i]}) ISM at ${submoduleAddresses[i]} on Domain Routing ISM ${moduleAddress}`,
718-
);
719-
const enrollTx = await routingIsm.set(
720-
safeConfigDomains[i],
721-
submoduleAddresses[i],
722-
{
723-
gasLimit: addBufferToGasLimit(estimatedGas, 15),
724-
...overrides,
725-
},
726-
);
727-
await this.multiProvider.handleTx(destination, enrollTx);
728-
}
757+
await this.enrollDomains({
758+
routingIsm,
759+
domains: safeConfigDomains.slice(initialBatchSize),
760+
addresses: submoduleAddresses.slice(initialBatchSize),
761+
batchSize,
762+
overrides,
763+
destination,
764+
logger,
765+
});
729766

730767
// Transfer ownership after all enrollments are complete, unless the
731768
// signer is already the target owner (common for self-owned deploys).
@@ -746,6 +783,173 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
746783
return routingIsm;
747784
}
748785

786+
/**
787+
* Enroll a set of (domain, ISM address) pairs on a routing ISM. Uses
788+
* `setBatch` chunked by `batchSize` when the deployed ISM is at a
789+
* `@hyperlane-xyz/core` version that supports it; otherwise falls back
790+
* to a per-domain `set` loop.
791+
*/
792+
private async enrollDomains(params: {
793+
routingIsm: RoutingIsmContract;
794+
domains: number[];
795+
addresses: Address[];
796+
batchSize: number;
797+
overrides: ethers.Overrides;
798+
destination: ChainName;
799+
logger: Logger;
800+
}): Promise<void> {
801+
if (params.domains.length === 0) return;
802+
const provider = this.multiProvider.getProvider(params.destination);
803+
const supportsBatch = await routingIsmSupportsBatch(
804+
params.routingIsm.address,
805+
provider,
806+
);
807+
return supportsBatch
808+
? this.setDomainsBatched(params)
809+
: this.setDomainsPerDomain(params);
810+
}
811+
812+
/**
813+
* Unenroll a set of domains from a routing ISM. Uses `removeBatch`
814+
* chunked by `batchSize` when supported; otherwise falls back to a
815+
* per-domain `remove` loop.
816+
*/
817+
private async unenrollDomains(params: {
818+
routingIsm: RoutingIsmContract;
819+
domains: number[];
820+
batchSize: number;
821+
overrides: ethers.Overrides;
822+
destination: ChainName;
823+
logger: Logger;
824+
}): Promise<void> {
825+
if (params.domains.length === 0) return;
826+
const provider = this.multiProvider.getProvider(params.destination);
827+
const supportsBatch = await routingIsmSupportsBatch(
828+
params.routingIsm.address,
829+
provider,
830+
);
831+
return supportsBatch
832+
? this.removeDomainsBatched(params)
833+
: this.removeDomainsPerDomain(params);
834+
}
835+
836+
private async setDomainsBatched(params: {
837+
routingIsm: RoutingIsmContract;
838+
domains: number[];
839+
addresses: Address[];
840+
batchSize: number;
841+
overrides: ethers.Overrides;
842+
destination: ChainName;
843+
logger: Logger;
844+
}): Promise<void> {
845+
const {
846+
routingIsm,
847+
domains,
848+
addresses,
849+
batchSize,
850+
overrides,
851+
destination,
852+
logger,
853+
} = params;
854+
for (let i = 0; i < domains.length; i += batchSize) {
855+
const chunk = domains.slice(i, i + batchSize).map((domain, j) => ({
856+
domain,
857+
module: addresses[i + j],
858+
}));
859+
logger.debug(
860+
`setBatch enrolling ${chunk.length} domains on routing ISM ${routingIsm.address} (${destination})`,
861+
);
862+
const estimatedGas = await routingIsm.estimateGas.setBatch(
863+
chunk,
864+
overrides,
865+
);
866+
const tx = await routingIsm.setBatch(chunk, {
867+
gasLimit: addBufferToGasLimit(estimatedGas, 15),
868+
...overrides,
869+
});
870+
await this.multiProvider.handleTx(destination, tx);
871+
}
872+
}
873+
874+
private async setDomainsPerDomain(params: {
875+
routingIsm: RoutingIsmContract;
876+
domains: number[];
877+
addresses: Address[];
878+
overrides: ethers.Overrides;
879+
destination: ChainName;
880+
logger: Logger;
881+
}): Promise<void> {
882+
const { routingIsm, domains, addresses, overrides, destination, logger } =
883+
params;
884+
for (let i = 0; i < domains.length; i++) {
885+
const chainName = this.multiProvider.getChainName(domains[i]);
886+
logger.debug(
887+
`Enrolling ${chainName} (${domains[i]}) ISM at ${addresses[i]} on routing ISM ${routingIsm.address}`,
888+
);
889+
const estimatedGas = await routingIsm.estimateGas.set(
890+
domains[i],
891+
addresses[i],
892+
overrides,
893+
);
894+
const tx = await routingIsm.set(domains[i], addresses[i], {
895+
gasLimit: addBufferToGasLimit(estimatedGas, 15),
896+
...overrides,
897+
});
898+
await this.multiProvider.handleTx(destination, tx);
899+
}
900+
}
901+
902+
private async removeDomainsBatched(params: {
903+
routingIsm: RoutingIsmContract;
904+
domains: number[];
905+
batchSize: number;
906+
overrides: ethers.Overrides;
907+
destination: ChainName;
908+
logger: Logger;
909+
}): Promise<void> {
910+
const { routingIsm, domains, batchSize, overrides, destination, logger } =
911+
params;
912+
for (let i = 0; i < domains.length; i += batchSize) {
913+
const chunk = domains.slice(i, i + batchSize);
914+
logger.debug(
915+
`removeBatch unenrolling ${chunk.length} domains on routing ISM ${routingIsm.address} (${destination})`,
916+
);
917+
const estimatedGas = await routingIsm.estimateGas.removeBatch(
918+
chunk,
919+
overrides,
920+
);
921+
const tx = await routingIsm.removeBatch(chunk, {
922+
gasLimit: addBufferToGasLimit(estimatedGas, 15),
923+
...overrides,
924+
});
925+
await this.multiProvider.handleTx(destination, tx);
926+
}
927+
}
928+
929+
private async removeDomainsPerDomain(params: {
930+
routingIsm: RoutingIsmContract;
931+
domains: number[];
932+
overrides: ethers.Overrides;
933+
destination: ChainName;
934+
logger: Logger;
935+
}): Promise<void> {
936+
const { routingIsm, domains, overrides, destination, logger } = params;
937+
for (const domain of domains) {
938+
logger.debug(
939+
`Unenrolling domain ${domain} from routing ISM ${routingIsm.address}`,
940+
);
941+
const estimatedGas = await routingIsm.estimateGas.remove(
942+
domain,
943+
overrides,
944+
);
945+
const tx = await routingIsm.remove(domain, {
946+
gasLimit: addBufferToGasLimit(estimatedGas, 15),
947+
...overrides,
948+
});
949+
await this.multiProvider.handleTx(destination, tx);
950+
}
951+
}
952+
749953
protected async deployAggregationIsm(params: {
750954
destination: ChainName;
751955
config: AggregationIsmConfig;

0 commit comments

Comments
 (0)