@hyperlane-xyz/sdk 41.3.1 → 42.0.0

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.
Files changed (166) hide show
  1. package/dist/ccip/consts.d.ts.map +1 -1
  2. package/dist/ccip/consts.js +20 -0
  3. package/dist/ccip/consts.js.map +1 -1
  4. package/dist/consts/multisigIsm.d.ts.map +1 -1
  5. package/dist/consts/multisigIsm.js +20 -0
  6. package/dist/consts/multisigIsm.js.map +1 -1
  7. package/dist/contracts/contracts.d.ts.map +1 -1
  8. package/dist/contracts/contracts.js +15 -0
  9. package/dist/contracts/contracts.js.map +1 -1
  10. package/dist/contracts/contracts.test.d.ts +2 -0
  11. package/dist/contracts/contracts.test.d.ts.map +1 -0
  12. package/dist/contracts/contracts.test.js +88 -0
  13. package/dist/contracts/contracts.test.js.map +1 -0
  14. package/dist/core/types.d.ts +80 -2602
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/core/types.js +19 -1
  17. package/dist/core/types.js.map +1 -1
  18. package/dist/core/types.test.js +58 -0
  19. package/dist/core/types.test.js.map +1 -1
  20. package/dist/deploy/warp.d.ts +119 -3
  21. package/dist/deploy/warp.d.ts.map +1 -1
  22. package/dist/deploy/warp.js +696 -33
  23. package/dist/deploy/warp.js.map +1 -1
  24. package/dist/deploy/warp.test.d.ts +2 -0
  25. package/dist/deploy/warp.test.d.ts.map +1 -0
  26. package/dist/deploy/warp.test.js +358 -0
  27. package/dist/deploy/warp.test.js.map +1 -0
  28. package/dist/deploy/warpHybridPlan.d.ts +104 -0
  29. package/dist/deploy/warpHybridPlan.d.ts.map +1 -0
  30. package/dist/deploy/warpHybridPlan.js +178 -0
  31. package/dist/deploy/warpHybridPlan.js.map +1 -0
  32. package/dist/deploy/warpHybridPlan.test.d.ts +2 -0
  33. package/dist/deploy/warpHybridPlan.test.d.ts.map +1 -0
  34. package/dist/deploy/warpHybridPlan.test.js +141 -0
  35. package/dist/deploy/warpHybridPlan.test.js.map +1 -0
  36. package/dist/hook/EvmHookModule.d.ts +1 -1
  37. package/dist/hook/EvmHookModule.d.ts.map +1 -1
  38. package/dist/hook/EvmHookModule.js +16 -3
  39. package/dist/hook/EvmHookModule.js.map +1 -1
  40. package/dist/hook/EvmHookReader.d.ts +10 -0
  41. package/dist/hook/EvmHookReader.d.ts.map +1 -1
  42. package/dist/hook/EvmHookReader.js +92 -11
  43. package/dist/hook/EvmHookReader.js.map +1 -1
  44. package/dist/hook/EvmHookReader.test.js +155 -1
  45. package/dist/hook/EvmHookReader.test.js.map +1 -1
  46. package/dist/hook/types.d.ts +107 -1983
  47. package/dist/hook/types.d.ts.map +1 -1
  48. package/dist/hook/types.js +49 -0
  49. package/dist/hook/types.js.map +1 -1
  50. package/dist/hook/types.test.js +21 -1
  51. package/dist/hook/types.test.js.map +1 -1
  52. package/dist/hook/updates.d.ts +1 -0
  53. package/dist/hook/updates.d.ts.map +1 -1
  54. package/dist/hook/updates.js +2 -2
  55. package/dist/hook/updates.js.map +1 -1
  56. package/dist/hook/utils.d.ts +28 -1
  57. package/dist/hook/utils.d.ts.map +1 -1
  58. package/dist/hook/utils.js +89 -0
  59. package/dist/hook/utils.js.map +1 -1
  60. package/dist/ica/types.d.ts +4 -674
  61. package/dist/ica/types.d.ts.map +1 -1
  62. package/dist/index.d.ts +6 -5
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +6 -5
  65. package/dist/index.js.map +1 -1
  66. package/dist/ism/EvmIsmModule.d.ts +28 -2
  67. package/dist/ism/EvmIsmModule.d.ts.map +1 -1
  68. package/dist/ism/EvmIsmModule.hardhat-test.js +478 -3
  69. package/dist/ism/EvmIsmModule.hardhat-test.js.map +1 -1
  70. package/dist/ism/EvmIsmModule.js +224 -10
  71. package/dist/ism/EvmIsmModule.js.map +1 -1
  72. package/dist/ism/EvmIsmReader.d.ts.map +1 -1
  73. package/dist/ism/EvmIsmReader.js +77 -1
  74. package/dist/ism/EvmIsmReader.js.map +1 -1
  75. package/dist/ism/EvmIsmReader.test.js +245 -4
  76. package/dist/ism/EvmIsmReader.test.js.map +1 -1
  77. package/dist/ism/HyperlaneIsmFactory.d.ts +14 -4
  78. package/dist/ism/HyperlaneIsmFactory.d.ts.map +1 -1
  79. package/dist/ism/HyperlaneIsmFactory.hardhat-test.js +372 -3
  80. package/dist/ism/HyperlaneIsmFactory.hardhat-test.js.map +1 -1
  81. package/dist/ism/HyperlaneIsmFactory.js +80 -6
  82. package/dist/ism/HyperlaneIsmFactory.js.map +1 -1
  83. package/dist/ism/delayedFlow.d.ts +31 -0
  84. package/dist/ism/delayedFlow.d.ts.map +1 -0
  85. package/dist/ism/delayedFlow.js +33 -0
  86. package/dist/ism/delayedFlow.js.map +1 -0
  87. package/dist/ism/types.d.ts +135 -4
  88. package/dist/ism/types.d.ts.map +1 -1
  89. package/dist/ism/types.js +263 -11
  90. package/dist/ism/types.js.map +1 -1
  91. package/dist/ism/types.test.js +433 -1
  92. package/dist/ism/types.test.js.map +1 -1
  93. package/dist/ism/utils.d.ts.map +1 -1
  94. package/dist/ism/utils.js +113 -3
  95. package/dist/ism/utils.js.map +1 -1
  96. package/dist/ism/utils.test.js +44 -0
  97. package/dist/ism/utils.test.js.map +1 -1
  98. package/dist/providers/MultiProviderAdapter.d.ts +3 -3
  99. package/dist/providers/MultiProviderAdapter.d.ts.map +1 -1
  100. package/dist/providers/MultiProviderAdapter.js +2 -1
  101. package/dist/providers/MultiProviderAdapter.js.map +1 -1
  102. package/dist/providers/SmartProvider/HyperlaneJsonRpcProvider.d.ts.map +1 -1
  103. package/dist/providers/SmartProvider/HyperlaneJsonRpcProvider.js +4 -0
  104. package/dist/providers/SmartProvider/HyperlaneJsonRpcProvider.js.map +1 -1
  105. package/dist/providers/transactionFeeEstimators.d.ts +16 -9
  106. package/dist/providers/transactionFeeEstimators.d.ts.map +1 -1
  107. package/dist/providers/transactionFeeEstimators.js +66 -17
  108. package/dist/providers/transactionFeeEstimators.js.map +1 -1
  109. package/dist/providers/transactionFeeEstimators.test.js +153 -1
  110. package/dist/providers/transactionFeeEstimators.test.js.map +1 -1
  111. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.d.ts +14 -1
  112. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.d.ts.map +1 -1
  113. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.js +46 -5
  114. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.js.map +1 -1
  115. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.d.ts +2 -0
  116. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.d.ts.map +1 -0
  117. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.js +125 -0
  118. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.js.map +1 -0
  119. package/dist/router/HyperlaneRouterDeployer.d.ts +3 -1
  120. package/dist/router/HyperlaneRouterDeployer.d.ts.map +1 -1
  121. package/dist/router/HyperlaneRouterDeployer.js +4 -2
  122. package/dist/router/HyperlaneRouterDeployer.js.map +1 -1
  123. package/dist/router/types.d.ts +6 -1011
  124. package/dist/router/types.d.ts.map +1 -1
  125. package/dist/test/testUtils.d.ts +1 -1
  126. package/dist/test/testUtils.d.ts.map +1 -1
  127. package/dist/test/testUtils.js +17 -0
  128. package/dist/test/testUtils.js.map +1 -1
  129. package/dist/test/testUtils.test.d.ts +2 -0
  130. package/dist/test/testUtils.test.d.ts.map +1 -0
  131. package/dist/test/testUtils.test.js +28 -0
  132. package/dist/test/testUtils.test.js.map +1 -0
  133. package/dist/token/EvmWarpModule.d.ts +44 -8
  134. package/dist/token/EvmWarpModule.d.ts.map +1 -1
  135. package/dist/token/EvmWarpModule.hardhat-test.js +574 -4
  136. package/dist/token/EvmWarpModule.hardhat-test.js.map +1 -1
  137. package/dist/token/EvmWarpModule.js +237 -25
  138. package/dist/token/EvmWarpModule.js.map +1 -1
  139. package/dist/token/configUtils.d.ts +17 -7
  140. package/dist/token/configUtils.d.ts.map +1 -1
  141. package/dist/token/configUtils.js +108 -0
  142. package/dist/token/configUtils.js.map +1 -1
  143. package/dist/token/configUtils.test.js +155 -1
  144. package/dist/token/configUtils.test.js.map +1 -1
  145. package/dist/token/deploy.d.ts +3 -1
  146. package/dist/token/deploy.d.ts.map +1 -1
  147. package/dist/token/deploy.hardhat-test.js +454 -3
  148. package/dist/token/deploy.hardhat-test.js.map +1 -1
  149. package/dist/token/deploy.js +25 -6
  150. package/dist/token/deploy.js.map +1 -1
  151. package/dist/token/types.d.ts +169 -2931
  152. package/dist/token/types.d.ts.map +1 -1
  153. package/dist/token/types.test.js +48 -0
  154. package/dist/token/types.test.js.map +1 -1
  155. package/dist/token/warpCheck.d.ts.map +1 -1
  156. package/dist/token/warpCheck.js +36 -1
  157. package/dist/token/warpCheck.js.map +1 -1
  158. package/dist/utils/ism.d.ts +167 -1
  159. package/dist/utils/ism.d.ts.map +1 -1
  160. package/dist/utils/ism.js +412 -2
  161. package/dist/utils/ism.js.map +1 -1
  162. package/dist/utils/ism.test.d.ts +2 -0
  163. package/dist/utils/ism.test.d.ts.map +1 -0
  164. package/dist/utils/ism.test.js +372 -0
  165. package/dist/utils/ism.test.js.map +1 -0
  166. package/package.json +10 -10
@@ -3,16 +3,18 @@ import chai, { expect } from 'chai';
3
3
  import chaiAsPromised from 'chai-as-promised';
4
4
  import hre from 'hardhat';
5
5
  import sinon from 'sinon';
6
- import { BlacklistIsm__factory, RateLimitedIsm__factory, StaticAggregationIsm__factory, TestLegacyBlacklistIsm__factory, } from '@hyperlane-xyz/core';
7
- import { eqAddress, rootLogger } from '@hyperlane-xyz/utils';
6
+ import { BlacklistIsm__factory, DefaultIsm__factory, DelayedFlowRouterHookIsm__factory, HypERC20__factory, NetFlowRateLimitedHookIsm__factory, RateLimitedIsm__factory, StaticAggregationIsm__factory, TestLegacyBlacklistIsm__factory, } from '@hyperlane-xyz/core';
7
+ import { addressToBytes32, eqAddress, rootLogger, } from '@hyperlane-xyz/utils';
8
8
  import { TestChainName, testChains } from '../consts/testChains.js';
9
9
  import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
10
10
  import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
11
+ import { buildDelayedFlowEnrollmentTxs, } from '../deploy/warp.js';
11
12
  import { MultiProvider } from '../providers/MultiProvider.js';
12
13
  import { EvmEventLogsReader } from '../rpc/evm/EvmEventLogsReader.js';
13
14
  import { randomAddress, randomIsmConfig, randomMultisigIsmConfig, } from '../test/testUtils.js';
14
- import { normalizeConfig } from '../utils/ism.js';
15
+ import { collectHybridIsmNodes, normalizeConfig } from '../utils/ism.js';
15
16
  import { EvmIsmModule } from './EvmIsmModule.js';
17
+ import { EvmIsmReader } from './EvmIsmReader.js';
16
18
  import { HyperlaneIsmFactory } from './HyperlaneIsmFactory.js';
17
19
  import { IsmType, ModuleType, } from './types.js';
18
20
  chai.use(chaiAsPromised);
@@ -31,6 +33,8 @@ describe('EvmIsmModule', async () => {
31
33
  const legacyDeploymentBlocks = new Map();
32
34
  let legacyExplorerMetadataStub;
33
35
  let legacyDeploymentBlockStub;
36
+ // paired TokenRouter required by the warp-route hybrid hook/ISM constructors
37
+ let warpRouterAddress;
34
38
  before(async () => {
35
39
  const [signer, funder] = await hre.ethers.getSigners();
36
40
  fundingAccount = funder;
@@ -47,6 +51,7 @@ describe('EvmIsmModule', async () => {
47
51
  const legacyIsmFactory = new HyperlaneIsmFactory(contractsMap, multiProvider);
48
52
  // mailbox
49
53
  mailboxAddress = (await new TestCoreDeployer(multiProvider, legacyIsmFactory).deployApp()).getContracts(chain).mailbox.address;
54
+ warpRouterAddress = (await new HypERC20__factory(signer).deploy(18, 1, 1, mailboxAddress)).address;
50
55
  });
51
56
  beforeEach(async () => {
52
57
  // Reset the MultiProvider for each test
@@ -153,6 +158,29 @@ describe('EvmIsmModule', async () => {
153
158
  },
154
159
  });
155
160
  }
161
+ /**
162
+ * Hybrid hook/ISMs are only valid inside a mandatory aggregation beside an
163
+ * authenticating ISM, which is also how a warp route always installs them.
164
+ * The wrapper holds `hybrid` by reference, so tests can keep mutating the
165
+ * node in place between create and update.
166
+ */
167
+ function hybridAgg(hybrid) {
168
+ return {
169
+ type: IsmType.AGGREGATION,
170
+ threshold: 2,
171
+ modules: [
172
+ { type: IsmType.TRUSTED_RELAYER, relayer: mailboxAddress },
173
+ hybrid,
174
+ ],
175
+ };
176
+ }
177
+ /** Address of the hybrid leaf inside the module's installed tree. */
178
+ async function hybridLeaf(ism) {
179
+ const derived = await new EvmIsmReader(multiProvider, chain).deriveIsmConfig(ism.serialize().deployedIsm);
180
+ const [leaf] = collectHybridIsmNodes(derived);
181
+ assert(leaf && 'address' in leaf && typeof leaf.address === 'string', 'expected a deployed hybrid leaf');
182
+ return leaf.address;
183
+ }
156
184
  describe('create', async () => {
157
185
  it('deploys a simple ism', async () => {
158
186
  const config = randomMultisigIsmConfig(3, 5);
@@ -241,6 +269,45 @@ describe('EvmIsmModule', async () => {
241
269
  multiProvider,
242
270
  })).to.be.rejectedWith(BLACKLIST_COMPOSITION_ERROR);
243
271
  });
272
+ it('deploys a mailbox default ism', async () => {
273
+ const config = {
274
+ type: IsmType.MAILBOX_DEFAULT,
275
+ };
276
+ await createIsm(config);
277
+ });
278
+ it('deploys a net flow rate limited hook ism and transfers ownership to non-deployer', async () => {
279
+ const owner = randomAddress();
280
+ const config = {
281
+ type: IsmType.NET_FLOW_RATE_LIMITED,
282
+ warpRouter: warpRouterAddress,
283
+ thresholdBps: 500,
284
+ duration: 86400n,
285
+ owner,
286
+ };
287
+ const { ism } = await createIsm(hybridAgg(config));
288
+ const netFlowIsm = NetFlowRateLimitedHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
289
+ expect((await netFlowIsm.owner()).toLowerCase()).to.equal(owner.toLowerCase());
290
+ expect((await netFlowIsm.warpRouter()).toLowerCase()).to.equal(warpRouterAddress.toLowerCase());
291
+ });
292
+ it('deploys a delayed flow router hook ism, enrolling routers before transferring ownership', async () => {
293
+ // owner is a non-deployer EOA: the deploy only succeeds if the
294
+ // owner-gated enrollment happens BEFORE the ownership transfer
295
+ const owner = randomAddress();
296
+ const remoteRouter = addressToBytes32(randomAddress()).toLowerCase();
297
+ const config = {
298
+ type: IsmType.DELAYED_FLOW_ROUTER,
299
+ warpRouter: warpRouterAddress,
300
+ thresholdBps: 10000,
301
+ maxDelay: 3600,
302
+ duration: 86400n,
303
+ owner,
304
+ remoteIsms: { [TestChainName.test2]: remoteRouter },
305
+ };
306
+ const { ism } = await createIsm(hybridAgg(config));
307
+ const delayedIsm = DelayedFlowRouterHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
308
+ expect(await delayedIsm.routers(multiProvider.getDomainId(TestChainName.test2))).to.equal(remoteRouter);
309
+ expect((await delayedIsm.owner()).toLowerCase()).to.equal(owner.toLowerCase());
310
+ });
244
311
  for (let i = 0; i < 16; i++) {
245
312
  it(`deploys a random ism config #${i}`, async () => {
246
313
  const config = randomIsmConfig();
@@ -765,6 +832,414 @@ describe('EvmIsmModule', async () => {
765
832
  expect(blacklistFound).to.be.true;
766
833
  });
767
834
  });
835
+ it('no changes to an existing mailbox default ism means no redeployment or updates', async () => {
836
+ const config = {
837
+ type: IsmType.MAILBOX_DEFAULT,
838
+ };
839
+ const { ism, initialIsmAddress } = await createIsm(config);
840
+ await expectTxsAndUpdate(ism, config, 0);
841
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
842
+ .true;
843
+ });
844
+ it('redeploys a mailbox default ism that defers to a different mailbox', async () => {
845
+ const config = {
846
+ type: IsmType.MAILBOX_DEFAULT,
847
+ };
848
+ const { initialIsmAddress } = await createIsm(config);
849
+ // A DefaultIsm's mailbox is an immutable constructor arg the reader
850
+ // omits, so this module — pointed at the same instance but at a
851
+ // different mailbox — sees an identical `{ type }` config while the
852
+ // instance routes through the wrong mailbox's default ISM.
853
+ const otherMailbox = warpRouterAddress;
854
+ const reboundModule = new EvmIsmModule(multiProvider, {
855
+ chain,
856
+ config,
857
+ addresses: {
858
+ ...factoryAddresses,
859
+ mailbox: otherMailbox,
860
+ deployedIsm: initialIsmAddress,
861
+ },
862
+ });
863
+ // redeploys internally (MAILBOX_DEFAULT is immutable), emitting no txs
864
+ expect(await reboundModule.update(config)).to.deep.equal([]);
865
+ const redeployedIsm = reboundModule.serialize().deployedIsm;
866
+ expect(eqAddress(redeployedIsm, initialIsmAddress)).to.be.false;
867
+ expect(eqAddress(await DefaultIsm__factory.connect(redeployedIsm, multiProvider.getProvider(chain)).mailbox(), otherMailbox)).to.be.true;
868
+ });
869
+ it('redeploys when changing a mailbox default ism to another type', async () => {
870
+ const { ism, initialIsmAddress } = await createIsm({
871
+ type: IsmType.MAILBOX_DEFAULT,
872
+ });
873
+ const trustedRelayerConfig = {
874
+ type: IsmType.TRUSTED_RELAYER,
875
+ relayer: randomAddress(),
876
+ };
877
+ // keep testConfig in sync for the afterEach read-back assertion
878
+ testConfig = trustedRelayerConfig;
879
+ // update() redeploys internally and emits no txs
880
+ await expectTxsAndUpdate(ism, trustedRelayerConfig, 0);
881
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
882
+ .false;
883
+ });
884
+ it('transfers netFlowRateLimitedHookIsm ownership in-place on owner change', async () => {
885
+ const signerAddress = await multiProvider.getSignerAddress(chain);
886
+ const netFlowConfig = {
887
+ type: IsmType.NET_FLOW_RATE_LIMITED,
888
+ warpRouter: warpRouterAddress,
889
+ thresholdBps: 500,
890
+ duration: 86400n,
891
+ owner: signerAddress,
892
+ };
893
+ const { ism, initialIsmAddress } = await createIsm(hybridAgg(netFlowConfig));
894
+ const newOwner = randomAddress();
895
+ // mutate in-place so testConfig (same reference) stays in sync for afterEach
896
+ netFlowConfig.owner = newOwner;
897
+ // NET_FLOW_RATE_LIMITED is mutable — update() transfers ownership in-place (1 tx)
898
+ await expectTxsAndUpdate(ism, hybridAgg(netFlowConfig), 1);
899
+ // same contract address — no redeploy
900
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
901
+ .true;
902
+ const netFlowIsm = NetFlowRateLimitedHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
903
+ expect((await netFlowIsm.owner()).toLowerCase()).to.equal(newOwner.toLowerCase());
904
+ });
905
+ it('redeploys a new netFlowRateLimitedHookIsm on thresholdBps change (immutable)', async () => {
906
+ const signerAddress = await multiProvider.getSignerAddress(chain);
907
+ const netFlowConfig = {
908
+ type: IsmType.NET_FLOW_RATE_LIMITED,
909
+ warpRouter: warpRouterAddress,
910
+ thresholdBps: 500,
911
+ duration: 86400n,
912
+ owner: signerAddress,
913
+ };
914
+ const { ism, initialIsmAddress } = await createIsm(hybridAgg(netFlowConfig));
915
+ // thresholdBps is immutable on-chain; changing it must redeploy
916
+ netFlowConfig.thresholdBps = 750;
917
+ // update() redeploys internally and emits no txs
918
+ await expectTxsAndUpdate(ism, hybridAgg(netFlowConfig), 0);
919
+ // different contract address — redeployed
920
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
921
+ .false;
922
+ const netFlowIsm = NetFlowRateLimitedHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
923
+ expect((await netFlowIsm.thresholdBps()).toString()).to.equal('750');
924
+ });
925
+ it('updates netFlowRateLimitedHookIsm owner in-place inside an aggregation ism without redeploying the container', async () => {
926
+ // the contract mandates composing NetFlowRateLimitedHookIsm under an
927
+ // authenticating ISM, so aggregation-wrapped is the primary shape
928
+ const signerAddress = await multiProvider.getSignerAddress(chain);
929
+ const netFlowConfig = {
930
+ type: IsmType.NET_FLOW_RATE_LIMITED,
931
+ warpRouter: warpRouterAddress,
932
+ thresholdBps: 500,
933
+ duration: 86400n,
934
+ owner: signerAddress,
935
+ };
936
+ const aggregationConfig = {
937
+ type: IsmType.AGGREGATION,
938
+ modules: [netFlowConfig, randomMultisigIsmConfig(3, 5)],
939
+ threshold: 2,
940
+ };
941
+ const { ism, initialIsmAddress } = await createIsm(aggregationConfig);
942
+ // mutate in-place so testConfig (same reference) stays in sync for afterEach
943
+ const newOwner = randomAddress();
944
+ netFlowConfig.owner = newOwner;
945
+ // owner is the netFlow's only mutable field: expect a single in-place
946
+ // transferOwnership tx, with the aggregation container untouched
947
+ await expectTxsAndUpdate(ism, aggregationConfig, 1);
948
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
949
+ .true;
950
+ });
951
+ it('no changes to an existing delayedFlowRouterHookIsm means no redeployment or updates', async () => {
952
+ const signerAddress = await multiProvider.getSignerAddress(chain);
953
+ const delayedConfig = {
954
+ type: IsmType.DELAYED_FLOW_ROUTER,
955
+ warpRouter: warpRouterAddress,
956
+ thresholdBps: 10000,
957
+ maxDelay: 3600,
958
+ duration: 86400n,
959
+ owner: signerAddress,
960
+ remoteIsms: {
961
+ [TestChainName.test2]: addressToBytes32(randomAddress()).toLowerCase(),
962
+ },
963
+ };
964
+ const { ism, initialIsmAddress } = await createIsm(hybridAgg(delayedConfig));
965
+ await expectTxsAndUpdate(ism, hybridAgg(delayedConfig), 0);
966
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
967
+ .true;
968
+ });
969
+ it('does not redeploy a delayedFlowRouterHookIsm when the target omits warpRouter', async () => {
970
+ const signerAddress = await multiProvider.getSignerAddress(chain);
971
+ const delayedConfig = {
972
+ type: IsmType.DELAYED_FLOW_ROUTER,
973
+ warpRouter: warpRouterAddress,
974
+ thresholdBps: 10000,
975
+ maxDelay: 3600,
976
+ duration: 86400n,
977
+ owner: signerAddress,
978
+ remoteIsms: {},
979
+ };
980
+ const { ism, initialIsmAddress } = await createIsm(hybridAgg(delayedConfig));
981
+ // warp-route-context configs omit warpRouter (implicitly the token);
982
+ // the update must treat it as unchanged, not as an immutable-field change
983
+ const targetWithoutWarpRouter = {
984
+ type: IsmType.DELAYED_FLOW_ROUTER,
985
+ thresholdBps: 10000,
986
+ maxDelay: 3600,
987
+ duration: 86400n,
988
+ owner: signerAddress,
989
+ remoteIsms: {},
990
+ };
991
+ await expectTxsAndUpdate(ism, hybridAgg(targetWithoutWarpRouter), 0);
992
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
993
+ .true;
994
+ });
995
+ it('enrolls and unenrolls remote routers on an existing delayedFlowRouterHookIsm', async () => {
996
+ const signerAddress = await multiProvider.getSignerAddress(chain);
997
+ const test2Router = addressToBytes32(randomAddress()).toLowerCase();
998
+ const test3Router = addressToBytes32(randomAddress()).toLowerCase();
999
+ const delayedConfig = {
1000
+ type: IsmType.DELAYED_FLOW_ROUTER,
1001
+ warpRouter: warpRouterAddress,
1002
+ thresholdBps: 10000,
1003
+ maxDelay: 3600,
1004
+ duration: 86400n,
1005
+ owner: signerAddress,
1006
+ remoteIsms: { [TestChainName.test2]: test2Router },
1007
+ };
1008
+ const { ism, initialIsmAddress } = await createIsm(hybridAgg(delayedConfig));
1009
+ // replace test2 with test3: 1 enroll tx + 1 unenroll tx
1010
+ delayedConfig.remoteIsms = { [TestChainName.test3]: test3Router };
1011
+ await expectTxsAndUpdate(ism, hybridAgg(delayedConfig), 2);
1012
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
1013
+ .true;
1014
+ const delayedIsm = DelayedFlowRouterHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
1015
+ const test3Domain = multiProvider.getDomainId(TestChainName.test3);
1016
+ expect(await delayedIsm.routers(test3Domain)).to.equal(test3Router);
1017
+ const enrolledDomains = await delayedIsm.domains();
1018
+ expect(enrolledDomains.map((domain) => Number(domain))).to.deep.equal([
1019
+ test3Domain,
1020
+ ]);
1021
+ });
1022
+ it('unenrolls a delayed flow domain missing from provider metadata', async () => {
1023
+ const signerAddress = await multiProvider.getSignerAddress(chain);
1024
+ const delayedConfig = {
1025
+ type: IsmType.DELAYED_FLOW_ROUTER,
1026
+ warpRouter: warpRouterAddress,
1027
+ thresholdBps: 10000,
1028
+ maxDelay: 3600,
1029
+ duration: 86400n,
1030
+ owner: signerAddress,
1031
+ remoteIsms: {},
1032
+ };
1033
+ const { ism } = await createIsm(hybridAgg(delayedConfig));
1034
+ const leafAddress = await hybridLeaf(ism);
1035
+ const delayedIsm = DelayedFlowRouterHookIsm__factory.connect(leafAddress, multiProvider.getSigner(chain));
1036
+ const unknownDomain = 987654;
1037
+ expect(multiProvider.tryGetChainName(unknownDomain)).to.be.null;
1038
+ await delayedIsm.enrollRemoteRouters([unknownDomain], [addressToBytes32(randomAddress())]);
1039
+ const leafModule = new EvmIsmModule(multiProvider, {
1040
+ chain,
1041
+ config: delayedConfig,
1042
+ addresses: {
1043
+ ...factoryAddresses,
1044
+ mailbox: mailboxAddress,
1045
+ deployedIsm: leafAddress,
1046
+ },
1047
+ });
1048
+ const txs = await leafModule.updateDeployedInstance(delayedConfig);
1049
+ expect(txs).to.have.length(1);
1050
+ for (const tx of txs) {
1051
+ await multiProvider.sendTransaction(chain, tx);
1052
+ }
1053
+ expect(await delayedIsm.domains()).to.deep.equal([]);
1054
+ });
1055
+ it('updateDeployedInstance reconciles the hybrid leaf but rejects a composed tree', async () => {
1056
+ const signerAddress = await multiProvider.getSignerAddress(chain);
1057
+ const delayedConfig = {
1058
+ type: IsmType.DELAYED_FLOW_ROUTER,
1059
+ warpRouter: warpRouterAddress,
1060
+ thresholdBps: 10000,
1061
+ maxDelay: 3600,
1062
+ duration: 86400n,
1063
+ owner: signerAddress,
1064
+ remoteIsms: {},
1065
+ };
1066
+ const { ism } = await createIsm(hybridAgg(delayedConfig));
1067
+ // Same module shape buildDelayedFlowEnrollmentTxs builds: pointed at the
1068
+ // deployed hybrid leaf, not at the aggregation above it.
1069
+ const leafModule = new EvmIsmModule(multiProvider, {
1070
+ chain,
1071
+ config: delayedConfig,
1072
+ addresses: {
1073
+ ...factoryAddresses,
1074
+ mailbox: mailboxAddress,
1075
+ deployedIsm: await hybridLeaf(ism),
1076
+ },
1077
+ });
1078
+ // The escape hatch skips the composition rules, so it must not accept a
1079
+ // config that composes anything.
1080
+ await expect(leafModule.updateDeployedInstance(hybridAgg(delayedConfig))).to.be.rejectedWith('composed');
1081
+ // ...while the leaf node it exists for still reconciles.
1082
+ delayedConfig.remoteIsms = {
1083
+ [TestChainName.test2]: addressToBytes32(randomAddress()).toLowerCase(),
1084
+ };
1085
+ const txs = await leafModule.updateDeployedInstance(delayedConfig);
1086
+ expect(txs.length).to.equal(1);
1087
+ for (const tx of txs) {
1088
+ await multiProvider.sendTransaction(chain, tx);
1089
+ }
1090
+ });
1091
+ it('transfers delayedFlowRouterHookIsm ownership after enrollment updates', async () => {
1092
+ const signerAddress = await multiProvider.getSignerAddress(chain);
1093
+ const delayedConfig = {
1094
+ type: IsmType.DELAYED_FLOW_ROUTER,
1095
+ warpRouter: warpRouterAddress,
1096
+ thresholdBps: 10000,
1097
+ maxDelay: 3600,
1098
+ duration: 86400n,
1099
+ owner: signerAddress,
1100
+ remoteIsms: {},
1101
+ };
1102
+ const { ism, initialIsmAddress } = await createIsm(hybridAgg(delayedConfig));
1103
+ // enroll a router AND hand over ownership in the same update: the txs
1104
+ // only execute successfully if enrollment (owner-gated) precedes the
1105
+ // ownership transfer
1106
+ const newOwner = randomAddress();
1107
+ delayedConfig.remoteIsms = {
1108
+ [TestChainName.test2]: addressToBytes32(randomAddress()).toLowerCase(),
1109
+ };
1110
+ delayedConfig.owner = newOwner;
1111
+ await expectTxsAndUpdate(ism, hybridAgg(delayedConfig), 2);
1112
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
1113
+ .true;
1114
+ const delayedIsm = DelayedFlowRouterHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
1115
+ expect((await delayedIsm.owner()).toLowerCase()).to.equal(newOwner.toLowerCase());
1116
+ });
1117
+ it('redeploys a new delayedFlowRouterHookIsm on maxDelay change (immutable)', async () => {
1118
+ const signerAddress = await multiProvider.getSignerAddress(chain);
1119
+ const delayedConfig = {
1120
+ type: IsmType.DELAYED_FLOW_ROUTER,
1121
+ warpRouter: warpRouterAddress,
1122
+ thresholdBps: 10000,
1123
+ maxDelay: 3600,
1124
+ duration: 86400n,
1125
+ owner: signerAddress,
1126
+ remoteIsms: {},
1127
+ };
1128
+ const { ism, initialIsmAddress } = await createIsm(hybridAgg(delayedConfig));
1129
+ // maxDelay is immutable on-chain; changing it must redeploy
1130
+ delayedConfig.maxDelay = 7200;
1131
+ // update() redeploys internally and emits no txs
1132
+ await expectTxsAndUpdate(ism, hybridAgg(delayedConfig), 0);
1133
+ // different contract address — redeployed
1134
+ expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
1135
+ .false;
1136
+ const delayedIsm = DelayedFlowRouterHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
1137
+ expect(await delayedIsm.maxDelay()).to.equal(7200);
1138
+ });
1139
+ });
1140
+ describe('delayed flow router cross-chain enrollment', async () => {
1141
+ const remoteChain = TestChainName.test2;
1142
+ const manualChain = TestChainName.test3;
1143
+ function registryAddresses() {
1144
+ return { [chain]: { ...factoryAddresses, mailbox: mailboxAddress } };
1145
+ }
1146
+ async function enrollmentTxs(allTargets) {
1147
+ return buildDelayedFlowEnrollmentTxs({
1148
+ chain,
1149
+ multiProvider,
1150
+ registryAddresses: registryAddresses(),
1151
+ warpRouter: warpRouterAddress,
1152
+ target: allTargets[chain],
1153
+ allTargets,
1154
+ });
1155
+ }
1156
+ async function applyTxs(txs) {
1157
+ for (const tx of txs) {
1158
+ await multiProvider.sendTransaction(chain, tx);
1159
+ }
1160
+ }
1161
+ it('enrolls the pairing derived from the route and converges on re-run', async () => {
1162
+ const signerAddress = await multiProvider.getSignerAddress(chain);
1163
+ const delayedConfig = {
1164
+ type: IsmType.DELAYED_FLOW_ROUTER,
1165
+ warpRouter: warpRouterAddress,
1166
+ thresholdBps: 10000,
1167
+ maxDelay: 3600,
1168
+ duration: 86400n,
1169
+ owner: signerAddress,
1170
+ };
1171
+ const { ism } = await createIsm(hybridAgg(delayedConfig));
1172
+ const remoteIsmAddress = randomAddress();
1173
+ const allTargets = {
1174
+ [chain]: {
1175
+ ismAddress: await hybridLeaf(ism),
1176
+ userNode: delayedConfig,
1177
+ },
1178
+ [remoteChain]: {
1179
+ ismAddress: remoteIsmAddress,
1180
+ userNode: delayedConfig,
1181
+ },
1182
+ };
1183
+ const txs = await enrollmentTxs(allTargets);
1184
+ expect(txs.length).to.equal(1);
1185
+ await applyTxs(txs);
1186
+ expect((await enrollmentTxs(allTargets)).length).to.equal(0);
1187
+ const delayedIsm = DelayedFlowRouterHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
1188
+ expect(await delayedIsm.domains()).to.deep.equal([
1189
+ multiProvider.getDomainId(remoteChain),
1190
+ ]);
1191
+ // keep testConfig (same reference) in sync for the afterEach read-back
1192
+ delayedConfig.remoteIsms = {
1193
+ [remoteChain]: addressToBytes32(remoteIsmAddress).toLowerCase(),
1194
+ };
1195
+ });
1196
+ it('enrolls a configured external remoteIsms alongside the derived pairing and converges on re-run', async () => {
1197
+ const signerAddress = await multiProvider.getSignerAddress(chain);
1198
+ const delayedConfig = {
1199
+ type: IsmType.DELAYED_FLOW_ROUTER,
1200
+ warpRouter: warpRouterAddress,
1201
+ thresholdBps: 10000,
1202
+ maxDelay: 3600,
1203
+ duration: 86400n,
1204
+ owner: signerAddress,
1205
+ remoteIsms: {},
1206
+ };
1207
+ const { ism } = await createIsm(hybridAgg(delayedConfig));
1208
+ // An operator-authored pairing names a chain the route never derives.
1209
+ // It is retained alongside the route-managed counterpart.
1210
+ const manualRouter = addressToBytes32(randomAddress()).toLowerCase();
1211
+ delayedConfig.remoteIsms = { [manualChain]: manualRouter };
1212
+ const derivedRouterAddress = randomAddress();
1213
+ const derivedRouter = addressToBytes32(derivedRouterAddress).toLowerCase();
1214
+ const allTargets = {
1215
+ [chain]: {
1216
+ ismAddress: await hybridLeaf(ism),
1217
+ userNode: delayedConfig,
1218
+ },
1219
+ [remoteChain]: {
1220
+ ismAddress: derivedRouterAddress,
1221
+ userNode: delayedConfig,
1222
+ },
1223
+ };
1224
+ const txs = await enrollmentTxs(allTargets);
1225
+ expect(txs.length).to.equal(1);
1226
+ await applyTxs(txs);
1227
+ expect((await enrollmentTxs(allTargets)).length).to.equal(0);
1228
+ const delayedIsm = DelayedFlowRouterHookIsm__factory.connect(await hybridLeaf(ism), multiProvider.getProvider(chain));
1229
+ const manualDomain = multiProvider.getDomainId(manualChain);
1230
+ const derivedDomain = multiProvider.getDomainId(remoteChain);
1231
+ expect(await delayedIsm.domains()).to.have.members([
1232
+ manualDomain,
1233
+ derivedDomain,
1234
+ ]);
1235
+ expect(await delayedIsm.routers(manualDomain)).to.equal(manualRouter);
1236
+ expect(await delayedIsm.routers(derivedDomain)).to.equal(derivedRouter);
1237
+ // keep testConfig (same reference) in sync for the afterEach read-back
1238
+ delayedConfig.remoteIsms = {
1239
+ [manualChain]: manualRouter,
1240
+ [remoteChain]: derivedRouter,
1241
+ };
1242
+ });
768
1243
  });
769
1244
  });
770
1245
  //# sourceMappingURL=EvmIsmModule.hardhat-test.js.map