import '@nomiclabs/hardhat-waffle'; import { expect } from 'chai'; import { ethers } from 'ethers'; import hre from 'hardhat'; import sinon from 'sinon'; import { TestCcipReadIsm, TestCcipReadIsm__factory } from '@hyperlane-xyz/core'; import { EvmIsmReader, HyperlaneCore, HyperlaneIsmFactory, HyperlaneProxyFactoryDeployer, MultiProvider, OffchainLookupIsmConfig, TestCoreDeployer, TestRecipientDeployer, offchainLookupRequestMessageHash, } from '@hyperlane-xyz/sdk'; import { WithAddress } from '@hyperlane-xyz/utils'; import { BaseMetadataBuilder } from './builder.js'; import { MetadataContext, isMetadataBuildable } from './types.js'; const OFFCHAIN_LOOKUP_SERVER_URL = 'http://example.com/namespace'; describe('Offchain Lookup ISM Integration', () => { let core: HyperlaneCore; let multiProvider: MultiProvider; let testRecipient: any; let ccipReadIsm: TestCcipReadIsm; let metadataBuilder: BaseMetadataBuilder; let ismFactory: HyperlaneIsmFactory; let fetchStub: sinon.SinonStub; beforeEach(async () => { // Set up a local test multi-provider and Hyperlane core const signers = await hre.ethers.getSigners(); multiProvider = MultiProvider.createTestMultiProvider({ signer: signers[0], }); const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); const contractsMap = await ismFactoryDeployer.deploy( multiProvider.mapKnownChains(() => ({})), ); ismFactory = new HyperlaneIsmFactory(contractsMap, multiProvider); core = await new TestCoreDeployer(multiProvider, ismFactory).deployApp(); // Deploy a TestRecipient on test1 const deployments = await new TestRecipientDeployer(multiProvider).deploy({ test2: {}, }); testRecipient = (deployments.test2 as any).testRecipient; // Deploy the TestCcipReadIsm on test1 domain const domain = multiProvider.getDomainId('test1'); ccipReadIsm = await multiProvider.handleDeploy( domain, new TestCcipReadIsm__factory(), // Pass in desired offchain URLs for the ISM constructor: [[OFFCHAIN_LOOKUP_SERVER_URL]], ); // Configure the TestRecipient to use the CCIP-Read ISM await testRecipient.setInterchainSecurityModule(ccipReadIsm.address); // Prepare the metadata builder metadataBuilder = new BaseMetadataBuilder(core); fetchStub = sinon.stub(global, 'fetch').resolves({ ok: true, json: async () => ({ data: '0x0000000000000000000000000000000000000000000000000000000000000001', }), } as Response); }); it('should process a message protected by CCIP-Read ISM', async () => { // Send a message from test1 to test2 const { dispatchTx, message } = await core.sendMessage( 'test1', 'test2', testRecipient.address, '0x1234', ); // Derive the on-chain ISM config for CCIP-Read const derivedIsm = await new EvmIsmReader( multiProvider, 'test2', ).deriveOffchainLookupConfig(ccipReadIsm.address); // Build the metadata using the CCIP-Read builder const context = { ism: derivedIsm, message, dispatchTx, hook: {} as any, }; const result = await metadataBuilder.build(context); expect(isMetadataBuildable(result)).to.be.true; if (!isMetadataBuildable(result)) { throw new Error('Metadata should be buildable'); } // Finally, call mailbox.process on test2 with the metadata and message const mailbox = core.getContracts('test2').mailbox; await expect(mailbox.process(result.metadata, message.message)).to.not.be .reverted; }); it('sends signature field in request when calling fetch', async () => { const { dispatchTx, message } = await core.sendMessage( 'test1', 'test2', testRecipient.address, '0x1234', ); // Derive the on-chain ISM config for CCIP-Read const derivedIsm = (await new EvmIsmReader( multiProvider, 'test2', ).deriveIsmConfig( ccipReadIsm.address, )) as WithAddress; // Build the metadata using the CCIP-Read builder const context: MetadataContext> = { ism: derivedIsm, message, dispatchTx, hook: {} as any, }; await metadataBuilder.build(context); // Verify that fetch was called exactly once expect(fetchStub.calledOnce).to.be.true; const [url, options] = fetchStub.getCall(0).args; const payload = JSON.parse(options.body as string); expect(url).to.equal( OFFCHAIN_LOOKUP_SERVER_URL.replace('{data}', payload.data), ); // Should include sender, data, and signature expect(payload).to.include.keys('sender', 'data', 'signature'); expect(payload.sender).to.equal(ccipReadIsm.address); const recovered = ethers.utils.verifyMessage( ethers.utils.arrayify( offchainLookupRequestMessageHash( payload.sender, payload.data, OFFCHAIN_LOOKUP_SERVER_URL, ), ), payload.signature, ); expect(recovered).to.equal((await hre.ethers.getSigners())[0].address); }); afterEach(() => { fetchStub.restore(); }); });