diff --git a/packages/backend/src/peripherals/database/OAppRemoteRepository.test.ts b/packages/backend/src/peripherals/database/OAppRemoteRepository.test.ts new file mode 100644 index 0000000..b2ecafe --- /dev/null +++ b/packages/backend/src/peripherals/database/OAppRemoteRepository.test.ts @@ -0,0 +1,40 @@ +import { Logger } from '@l2beat/backend-tools' +import { ChainId } from '@lz/libs' +import { expect } from 'earl' + +import { setupDatabaseTestSuite } from '../../test/database' +import { OAppRemoteRecord, OAppRemoteRepository } from './OAppRemoteRepository' + +describe(OAppRemoteRepository.name, () => { + const { database } = setupDatabaseTestSuite() + const repository = new OAppRemoteRepository(database, Logger.SILENT) + + before(async () => await repository.deleteAll()) + afterEach(async () => await repository.deleteAll()) + + describe(OAppRemoteRepository.prototype.addMany.name, () => { + it('merges rows on insert', async () => { + const record1 = mockRecord({ oAppId: 1, targetChainId: ChainId.ETHEREUM }) + const record2 = mockRecord({ oAppId: 2, targetChainId: ChainId.OPTIMISM }) + + await repository.addMany([record1, record2]) + + const recordsBeforeMerge = await repository.findAll() + + await repository.addMany([record1, record2]) + + const recordsAfterMerge = await repository.findAll() + + expect(recordsBeforeMerge.length).toEqual(2) + expect(recordsAfterMerge.length).toEqual(2) + }) + }) +}) + +function mockRecord(overrides?: Partial): OAppRemoteRecord { + return { + oAppId: 1, + targetChainId: ChainId.ETHEREUM, + ...overrides, + } +} diff --git a/packages/backend/src/peripherals/database/OAppRemoteRepository.ts b/packages/backend/src/peripherals/database/OAppRemoteRepository.ts index 17e93bc..e302d29 100644 --- a/packages/backend/src/peripherals/database/OAppRemoteRepository.ts +++ b/packages/backend/src/peripherals/database/OAppRemoteRepository.ts @@ -35,15 +35,6 @@ export class OAppRemoteRepository extends BaseRepository { return rows.map(toRecord) } - public async findByOAppIds(oAppIds: number[]): Promise { - const knex = await this.knex() - - const rows = await knex('oapp_remote') - .select('*') - .whereIn('oapp_id', oAppIds) - - return rows.map(toRecord) - } async deleteAll(): Promise { const knex = await this.knex() diff --git a/packages/backend/src/tracking/TrackingModule.ts b/packages/backend/src/tracking/TrackingModule.ts index 0eb0960..690ae0f 100644 --- a/packages/backend/src/tracking/TrackingModule.ts +++ b/packages/backend/src/tracking/TrackingModule.ts @@ -26,6 +26,8 @@ import { OAppConfigurationIndexer } from './domain/indexers/OAppConfigurationInd import { OAppListIndexer } from './domain/indexers/OAppListIndexer' import { OAppRemoteIndexer } from './domain/indexers/OAppRemotesIndexer' import { DiscoveryDefaultConfigurationsProvider } from './domain/providers/DefaultConfigurationsProvider' +import { OFTInterfaceResolver } from './domain/providers/interface-resolvers/OFTInterfaceResolver' +import { StargateInterfaceResolver } from './domain/providers/interface-resolvers/StargateResolver' import { BlockchainOAppConfigurationProvider } from './domain/providers/OAppConfigurationProvider' import { BlockchainOAppRemotesProvider } from './domain/providers/OAppRemotesProvider' import { HttpOAppListProvider } from './domain/providers/OAppsListProvider' @@ -144,6 +146,9 @@ function createTrackingSubmodule( const httpClient = new HttpClient() + const OFTResolver = new OFTInterfaceResolver(multicall) + const stargateResolver = new StargateInterfaceResolver(multicall) + const oAppListProvider = new HttpOAppListProvider( logger, httpClient, @@ -165,10 +170,15 @@ function createTrackingSubmodule( logger, ) + const supportedChains = ChainId.getAll() + const resolvers = [OFTResolver, stargateResolver] + const oAppRemotesProvider = new BlockchainOAppRemotesProvider( provider, multicall, chainId, + supportedChains, + resolvers, logger, ) diff --git a/packages/backend/src/tracking/domain/indexers/DefaultConfigurationIndexer.ts b/packages/backend/src/tracking/domain/indexers/DefaultConfigurationIndexer.ts index 4a277c6..82f4cf8 100644 --- a/packages/backend/src/tracking/domain/indexers/DefaultConfigurationIndexer.ts +++ b/packages/backend/src/tracking/domain/indexers/DefaultConfigurationIndexer.ts @@ -1,5 +1,5 @@ import { Logger } from '@l2beat/backend-tools' -import { ChildIndexer, Indexer } from '@l2beat/uif' +import { Indexer } from '@l2beat/uif' import { ChainId } from '@lz/libs' import { @@ -9,9 +9,9 @@ import { import { OAppConfigurations } from '../configuration' import { ProtocolVersion } from '../const' import { DefaultConfigurationsProvider } from '../providers/DefaultConfigurationsProvider' +import { InMemoryIndexer } from './InMemoryIndexer' -export class DefaultConfigurationIndexer extends ChildIndexer { - protected height = 0 +export class DefaultConfigurationIndexer extends InMemoryIndexer { constructor( logger: Logger, private readonly chainId: ChainId, @@ -40,19 +40,6 @@ export class DefaultConfigurationIndexer extends ChildIndexer { return to } - - public override getSafeHeight(): Promise { - return Promise.resolve(this.height) - } - - protected override setSafeHeight(height: number): Promise { - this.height = height - return Promise.resolve() - } - - protected override invalidate(targetHeight: number): Promise { - return Promise.resolve(targetHeight) - } } function configToRecords( diff --git a/packages/backend/src/tracking/domain/indexers/InMemoryIndexer.ts b/packages/backend/src/tracking/domain/indexers/InMemoryIndexer.ts new file mode 100644 index 0000000..7a71e70 --- /dev/null +++ b/packages/backend/src/tracking/domain/indexers/InMemoryIndexer.ts @@ -0,0 +1,19 @@ +import { ChildIndexer } from '@l2beat/uif' + +export { InMemoryIndexer } + +abstract class InMemoryIndexer extends ChildIndexer { + protected height = 0 + public override getSafeHeight(): Promise { + return Promise.resolve(this.height) + } + + protected override setSafeHeight(height: number): Promise { + this.height = height + return Promise.resolve() + } + + protected override invalidate(targetHeight: number): Promise { + return Promise.resolve(targetHeight) + } +} diff --git a/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts b/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts index b618aa3..b6cbb2f 100644 --- a/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts +++ b/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts @@ -1,5 +1,5 @@ import { Logger } from '@l2beat/backend-tools' -import { ChildIndexer, Indexer } from '@l2beat/uif' +import { Indexer } from '@l2beat/uif' import { ChainId } from '@lz/libs' import { @@ -10,9 +10,9 @@ import { OAppRemoteRepository } from '../../../peripherals/database/OAppRemoteRe import { OAppRepository } from '../../../peripherals/database/OAppRepository' import { OAppConfigurations } from '../configuration' import { OAppConfigurationProvider } from '../providers/OAppConfigurationProvider' +import { InMemoryIndexer } from './InMemoryIndexer' -export class OAppConfigurationIndexer extends ChildIndexer { - protected height = 0 +export class OAppConfigurationIndexer extends InMemoryIndexer { constructor( logger: Logger, private readonly chainId: ChainId, @@ -26,8 +26,10 @@ export class OAppConfigurationIndexer extends ChildIndexer { } protected override async update(_from: number, to: number): Promise { - const oApps = await this.oAppRepo.getBySourceChain(this.chainId) - const oAppsRemotes = await this.oAppRemoteRepo.findAll() + const [oApps, oAppsRemotes] = await Promise.all([ + this.oAppRepo.getBySourceChain(this.chainId), + this.oAppRemoteRepo.findAll(), + ]) const configurationRecords = await Promise.all( oApps.map(async (oApp) => { @@ -49,19 +51,6 @@ export class OAppConfigurationIndexer extends ChildIndexer { return to } - - public override getSafeHeight(): Promise { - return Promise.resolve(this.height) - } - - protected override setSafeHeight(height: number): Promise { - this.height = height - return Promise.resolve() - } - - protected override invalidate(targetHeight: number): Promise { - return Promise.resolve(targetHeight) - } } function configToRecord( diff --git a/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts b/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts index 57419dc..e847299 100644 --- a/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts +++ b/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts @@ -1,13 +1,13 @@ import { Logger } from '@l2beat/backend-tools' -import { ChildIndexer, Indexer } from '@l2beat/uif' +import { Indexer } from '@l2beat/uif' import { ChainId } from '@lz/libs' import { OAppRepository } from '../../../peripherals/database/OAppRepository' import { ProtocolVersion } from '../const' import { OAppListProvider } from '../providers/OAppsListProvider' +import { InMemoryIndexer } from './InMemoryIndexer' -export class OAppListIndexer extends ChildIndexer { - protected height = 0 +export class OAppListIndexer extends InMemoryIndexer { constructor( logger: Logger, private readonly chainId: ChainId, @@ -38,17 +38,4 @@ export class OAppListIndexer extends ChildIndexer { return to } - - public override getSafeHeight(): Promise { - return Promise.resolve(this.height) - } - - protected override setSafeHeight(height: number): Promise { - this.height = height - return Promise.resolve() - } - - protected override invalidate(targetHeight: number): Promise { - return Promise.resolve(targetHeight) - } } diff --git a/packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts b/packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts index 0b98588..c069c1e 100644 --- a/packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts +++ b/packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts @@ -1,5 +1,5 @@ import { Logger } from '@l2beat/backend-tools' -import { ChildIndexer, Indexer } from '@l2beat/uif' +import { Indexer } from '@l2beat/uif' import { ChainId } from '@lz/libs' import { @@ -8,9 +8,9 @@ import { } from '../../../peripherals/database/OAppRemoteRepository' import { OAppRepository } from '../../../peripherals/database/OAppRepository' import { OAppRemotesProvider } from '../providers/OAppRemotesProvider' +import { InMemoryIndexer } from './InMemoryIndexer' -export class OAppRemoteIndexer extends ChildIndexer { - protected height = 0 +export class OAppRemoteIndexer extends InMemoryIndexer { constructor( logger: Logger, private readonly chainId: ChainId, @@ -42,17 +42,4 @@ export class OAppRemoteIndexer extends ChildIndexer { return to } - - public override getSafeHeight(): Promise { - return Promise.resolve(this.height) - } - - protected override setSafeHeight(height: number): Promise { - this.height = height - return Promise.resolve() - } - - protected override invalidate(targetHeight: number): Promise { - return Promise.resolve(targetHeight) - } } diff --git a/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.test.ts b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.test.ts new file mode 100644 index 0000000..c78bc99 --- /dev/null +++ b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.test.ts @@ -0,0 +1,58 @@ +import { Logger } from '@l2beat/backend-tools' +import { MulticallClient } from '@l2beat/discovery' +// eslint-disable-next-line import/no-internal-modules +import { MulticallResponse } from '@l2beat/discovery/dist/discovery/provider/multicall/types' +// eslint-disable-next-line import/no-internal-modules +import { Bytes } from '@l2beat/discovery/dist/utils/Bytes' +import { ChainId, EthereumAddress } from '@lz/libs' +import { expect, mockFn, mockObject } from 'earl' +import { providers } from 'ethers' + +import { OAppInterfaceResolver } from './interface-resolvers/resolver' +import { BlockchainOAppRemotesProvider } from './OAppRemotesProvider' + +describe(BlockchainOAppRemotesProvider.name, () => { + it('resolves available remotes for given OApp', async () => { + const blockNumber = 123 + const oAppAddress = EthereumAddress.random() + const rpcProvider = mockObject({ + getBlockNumber: mockFn().resolvesTo(blockNumber), + }) + + const mcResponse: MulticallResponse[] = ChainId.getAll().map(() => ({ + success: true, + data: Bytes.fromHex('0x0'), + })) + + const multicall = mockObject({ + multicall: mockFn().resolvesTo(mcResponse), + }) + + const supportedChains = [ChainId.ETHEREUM, ChainId.ARBITRUM] + + const resolverA: OAppInterfaceResolver = { + isSupported: async () => false, + encode: () => ({ address: oAppAddress, data: Bytes.fromHex('0x0') }), + decode: () => true, + } + + const resolverB: OAppInterfaceResolver = { + isSupported: async () => true, + encode: () => ({ address: oAppAddress, data: Bytes.fromHex('0x0') }), + decode: () => true, + } + + const provider = new BlockchainOAppRemotesProvider( + rpcProvider, + multicall, + ChainId.ETHEREUM, + supportedChains, + [resolverA, resolverB], + Logger.SILENT, + ) + + const result = await provider.getSupportedRemotes(oAppAddress) + + expect(result).toEqual(supportedChains) + }) +}) diff --git a/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts index bb669eb..e7033a8 100644 --- a/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts +++ b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts @@ -1,14 +1,10 @@ import { assert, Logger } from '@l2beat/backend-tools' import { MulticallClient } from '@l2beat/discovery' -import { - MulticallRequest, - MulticallResponse, - // eslint-disable-next-line import/no-internal-modules -} from '@l2beat/discovery/dist/discovery/provider/multicall/types' // eslint-disable-next-line import/no-internal-modules -import { Bytes } from '@l2beat/discovery/dist/utils/Bytes' import { ChainId, EndpointID, EthereumAddress } from '@lz/libs' -import { providers, utils } from 'ethers' +import { providers } from 'ethers' + +import { OAppInterfaceResolver } from './interface-resolvers/resolver' export { BlockchainOAppRemotesProvider } export type { OAppRemotesProvider } @@ -17,14 +13,6 @@ interface OAppRemotesProvider { getSupportedRemotes(oAppsAddress: EthereumAddress): Promise } -const oftIface = new utils.Interface([ - 'function trustedRemoteLookup(uint16 _remoteChainId) view returns (bytes)', -]) - -const stargateIface = new utils.Interface([ - 'function dstContractLookup(uint16 _remoteChainId) view returns (bytes)', -]) - /** * Fetches the supported remotes for an OApp from the blockchain directly. * Supports both OFT and Stargate-like contracts. @@ -36,6 +24,8 @@ class BlockchainOAppRemotesProvider implements OAppRemotesProvider { private readonly provider: providers.StaticJsonRpcProvider, private readonly multicall: MulticallClient, chainId: ChainId, + private readonly monitoredChains: ChainId[], + private readonly ifaceResolvers: OAppInterfaceResolver[], private readonly logger: Logger, ) { this.logger = this.logger.for(this).tag(ChainId.getName(chainId)) @@ -45,14 +35,12 @@ class BlockchainOAppRemotesProvider implements OAppRemotesProvider { ): Promise { const blockNumber = await this.provider.getBlockNumber() - const supportedChains = ChainId.getAll() - - const supportedEndpoints = supportedChains.flatMap( + const supportedEndpoints = this.monitoredChains.flatMap( (chainId) => EndpointID.encodeV1(chainId) ?? [], ) assert( - supportedEndpoints.length === supportedChains.length, + supportedEndpoints.length === this.monitoredChains.length, 'Cannot translate some chains to EID', ) @@ -70,12 +58,18 @@ class BlockchainOAppRemotesProvider implements OAppRemotesProvider { supportedEndpoints: number[], blockNumber: number, ): Promise { - const isOft = await this.checkForOft(oAppAddress, blockNumber) + console.log('Looking for resolver') + const resolver = await this.findResolver(oAppAddress, blockNumber) + console.log('Resolver found') - const encode = isOft ? encodeOft : encodeStargate - const decode = isOft ? decodeOft : decodeStargate + assert( + resolver, + `No interface resolver found for OApp: ${oAppAddress.toString()} at block ${blockNumber}`, + ) - const requests = supportedEndpoints.map((eid) => encode(oAppAddress, eid)) + const requests = supportedEndpoints.map((eid) => + resolver.encode(oAppAddress, eid), + ) const result = await this.multicall.multicall(requests, blockNumber) @@ -85,7 +79,7 @@ class BlockchainOAppRemotesProvider implements OAppRemotesProvider { .map((eid, i) => ({ eid, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - supported: decode(result[i]!), + supported: resolver.decode(result[i]!), })) .filter((x) => x.supported) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -93,72 +87,16 @@ class BlockchainOAppRemotesProvider implements OAppRemotesProvider { ) } - private async checkForOft( - oApp: EthereumAddress, + private async findResolver( + oAppAddress: EthereumAddress, blockNumber: number, - ): Promise { - const data = oftIface.encodeFunctionData('trustedRemoteLookup', [ - // Example EID - EndpointID.encodeV1(ChainId.ETHEREUM) ?? 0, - ]) - - const request = { - address: oApp, - data: Bytes.fromHex(data), + ): Promise { + for (const ifaceResolver of this.ifaceResolvers) { + if (await ifaceResolver.isSupported(oAppAddress, blockNumber)) { + return ifaceResolver + } } - try { - const [result] = await this.multicall.multicall([request], blockNumber) - - return Boolean(result?.success) - } catch (error) { - return false - } - } -} - -function decodeStargate(response: MulticallResponse): boolean { - const [decoded] = stargateIface.decodeFunctionResult( - 'dstContractLookup', - response.data.toString(), - ) - - return decoded !== '0x' -} - -function encodeStargate( - oAppAddress: EthereumAddress, - eid: number, -): MulticallRequest { - { - const data = stargateIface.encodeFunctionData('dstContractLookup', [eid]) - - return { - address: oAppAddress, - data: Bytes.fromHex(data), - } - } -} - -function decodeOft(response: MulticallResponse): boolean { - const [decoded] = oftIface.decodeFunctionResult( - 'trustedRemoteLookup', - response.data.toString(), - ) - - return decoded !== '0x' -} - -function encodeOft( - oAppAddress: EthereumAddress, - eid: number, -): MulticallRequest { - { - const data = oftIface.encodeFunctionData('trustedRemoteLookup', [eid]) - - return { - address: oAppAddress, - data: Bytes.fromHex(data), - } + return null } } diff --git a/packages/backend/src/tracking/domain/providers/interface-resolvers/OFTInterfaceResolver.ts b/packages/backend/src/tracking/domain/providers/interface-resolvers/OFTInterfaceResolver.ts new file mode 100644 index 0000000..7c99275 --- /dev/null +++ b/packages/backend/src/tracking/domain/providers/interface-resolvers/OFTInterfaceResolver.ts @@ -0,0 +1,65 @@ +import { MulticallClient } from '@l2beat/discovery' +import { + MulticallRequest, + MulticallResponse, + // eslint-disable-next-line import/no-internal-modules +} from '@l2beat/discovery/dist/discovery/provider/multicall/types' +// eslint-disable-next-line import/no-internal-modules +import { Bytes } from '@l2beat/discovery/dist/utils/Bytes' +import { ChainId, EndpointID, EthereumAddress } from '@lz/libs' +import { utils } from 'ethers' + +import { OAppInterfaceResolver } from './resolver' + +export { OFTInterfaceResolver } + +class OFTInterfaceResolver implements OAppInterfaceResolver { + private readonly iface = new utils.Interface([ + 'function trustedRemoteLookup(uint16 _remoteChainId) view returns (bytes)', + ]) + + public constructor(private readonly multicall: MulticallClient) {} + + public async isSupported( + oAppAddress: EthereumAddress, + blockNumber: number, + ): Promise { + const data = this.iface.encodeFunctionData('trustedRemoteLookup', [ + // Example EID + EndpointID.encodeV1(ChainId.ETHEREUM) ?? 0, + ]) + + const request = { + address: oAppAddress, + data: Bytes.fromHex(data), + } + + try { + const [result] = await this.multicall.multicall([request], blockNumber) + + return Boolean(result?.success) + } catch (error) { + return false + } + } + + public encode(oAppAddress: EthereumAddress, eid: number): MulticallRequest { + { + const data = this.iface.encodeFunctionData('trustedRemoteLookup', [eid]) + + return { + address: oAppAddress, + data: Bytes.fromHex(data), + } + } + } + + public decode(response: MulticallResponse): boolean { + const [decoded] = this.iface.decodeFunctionResult( + 'trustedRemoteLookup', + response.data.toString(), + ) + + return decoded !== '0x' + } +} diff --git a/packages/backend/src/tracking/domain/providers/interface-resolvers/StargateResolver.ts b/packages/backend/src/tracking/domain/providers/interface-resolvers/StargateResolver.ts new file mode 100644 index 0000000..2b4453f --- /dev/null +++ b/packages/backend/src/tracking/domain/providers/interface-resolvers/StargateResolver.ts @@ -0,0 +1,65 @@ +import { MulticallClient } from '@l2beat/discovery' +import { + MulticallRequest, + MulticallResponse, + // eslint-disable-next-line import/no-internal-modules +} from '@l2beat/discovery/dist/discovery/provider/multicall/types' +// eslint-disable-next-line import/no-internal-modules +import { Bytes } from '@l2beat/discovery/dist/utils/Bytes' +import { ChainId, EndpointID, EthereumAddress } from '@lz/libs' +import { utils } from 'ethers' + +import { OAppInterfaceResolver } from './resolver' + +export { StargateInterfaceResolver } + +class StargateInterfaceResolver implements OAppInterfaceResolver { + private readonly iface = new utils.Interface([ + 'function dstContractLookup(uint16 _remoteChainId) view returns (bytes)', + ]) + + public constructor(private readonly multicall: MulticallClient) {} + + public async isSupported( + oAppAddress: EthereumAddress, + blockNumber: number, + ): Promise { + const data = this.iface.encodeFunctionData('dstContractLookup', [ + // Example EID + EndpointID.encodeV1(ChainId.ETHEREUM) ?? 0, + ]) + + const request = { + address: oAppAddress, + data: Bytes.fromHex(data), + } + + try { + const [result] = await this.multicall.multicall([request], blockNumber) + + return Boolean(result?.success) + } catch (error) { + return false + } + } + + public encode(oAppAddress: EthereumAddress, eid: number): MulticallRequest { + { + const data = this.iface.encodeFunctionData('dstContractLookup', [eid]) + + return { + address: oAppAddress, + data: Bytes.fromHex(data), + } + } + } + + public decode(response: MulticallResponse): boolean { + const [decoded] = this.iface.decodeFunctionResult( + 'dstContractLookup', + response.data.toString(), + ) + + return decoded !== '0x' + } +} diff --git a/packages/backend/src/tracking/domain/providers/interface-resolvers/resolver.ts b/packages/backend/src/tracking/domain/providers/interface-resolvers/resolver.ts new file mode 100644 index 0000000..221a5d4 --- /dev/null +++ b/packages/backend/src/tracking/domain/providers/interface-resolvers/resolver.ts @@ -0,0 +1,32 @@ +import { + MulticallRequest, + MulticallResponse, + // eslint-disable-next-line import/no-internal-modules +} from '@l2beat/discovery/dist/discovery/provider/multicall/types' +import { EthereumAddress } from '@lz/libs' + +export type { OAppInterfaceResolver } + +/** + * Interface for remote resolvers. + * Each resolver is responsible for checking if given oApp is supported for given resolver + * and if so, for encoding and decoding calls to multicall. + */ +interface OAppInterfaceResolver { + /** + * Check if resolver interface is supported for given oApp + */ + isSupported( + oAppAddress: EthereumAddress, + blockNumber: number, + ): Promise + + /** + * Encode request for multicall + */ + encode(oAppAddress: EthereumAddress, eid: number): MulticallRequest + /** + * Decode response and check if payload indicates that remote is supported + */ + decode(response: MulticallResponse): boolean +}