1+ import { compareVersions } from 'compare-versions' ;
12import { ethers } from 'ethers' ;
23import { Logger } from 'pino' ;
34
@@ -31,6 +32,7 @@ import {
3132 TrustedRelayerIsm__factory ,
3233 ZKSyncArtifact ,
3334} from '@hyperlane-xyz/core' ;
35+
3436import {
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+
87115const 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