From 8e767997ff1723ddc64a7e496e2a0c1aaedbd85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Podsiad=C5=82y?= Date: Thu, 14 Mar 2024 20:09:33 +0100 Subject: [PATCH 1/4] feat: track supported remotes for oApps --- .../database/OAppRemoteRepository.ts | 66 +++++++ .../database/migrations/017_oapps_remotes.ts | 26 +++ .../src/peripherals/database/shared/types.ts | 6 + .../backend/src/tracking/TrackingModule.ts | 31 +++- .../indexers/OAppConfigurationIndexer.ts | 9 + .../domain/indexers/OAppListIndexer.ts | 2 + .../domain/indexers/OAppRemotesIndexer.ts | 58 +++++++ .../providers/OAppConfigurationProvider.ts | 8 +- .../domain/providers/OAppRemotesProvider.ts | 164 ++++++++++++++++++ 9 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 packages/backend/src/peripherals/database/OAppRemoteRepository.ts create mode 100644 packages/backend/src/peripherals/database/migrations/017_oapps_remotes.ts create mode 100644 packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts create mode 100644 packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts diff --git a/packages/backend/src/peripherals/database/OAppRemoteRepository.ts b/packages/backend/src/peripherals/database/OAppRemoteRepository.ts new file mode 100644 index 00000000..17e93bc5 --- /dev/null +++ b/packages/backend/src/peripherals/database/OAppRemoteRepository.ts @@ -0,0 +1,66 @@ +import { Logger } from '@l2beat/backend-tools' +import { ChainId } from '@lz/libs' +import type { OAppRemoteRow } from 'knex/types/tables' + +import { BaseRepository, CheckConvention } from './shared/BaseRepository' +import { Database } from './shared/Database' + +export interface OAppRemoteRecord { + oAppId: number + targetChainId: ChainId +} + +export class OAppRemoteRepository extends BaseRepository { + constructor(database: Database, logger: Logger) { + super(database, logger) + this.autoWrap>(this) + } + + public async addMany(records: OAppRemoteRecord[]): Promise { + const rows = records.map(toRow) + const knex = await this.knex() + + await knex('oapp_remote') + .insert(rows) + .onConflict(['oapp_id', 'target_chain_id']) + .merge() + + return rows.length + } + + public async findAll(): Promise { + const knex = await this.knex() + + const rows = await knex('oapp_remote').select('*') + + 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() + return knex('oapp_remote').delete() + } +} + +function toRow(record: OAppRemoteRecord): OAppRemoteRow { + return { + oapp_id: record.oAppId, + target_chain_id: Number(record.targetChainId), + } +} + +function toRecord(row: OAppRemoteRow): OAppRemoteRecord { + return { + oAppId: row.oapp_id, + targetChainId: ChainId(row.target_chain_id), + } +} diff --git a/packages/backend/src/peripherals/database/migrations/017_oapps_remotes.ts b/packages/backend/src/peripherals/database/migrations/017_oapps_remotes.ts new file mode 100644 index 00000000..99996ca8 --- /dev/null +++ b/packages/backend/src/peripherals/database/migrations/017_oapps_remotes.ts @@ -0,0 +1,26 @@ +/* + ====== IMPORTANT NOTICE ====== + +DO NOT EDIT OR RENAME THIS FILE + +This is a migration file. Once created the file should not be renamed or edited, +because migrations are only run once on the production server. + +If you find that something was incorrectly set up in the `up` function you +should create a new migration file that fixes the issue. + +*/ + +import { Knex } from 'knex' + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('oapp_remote', (table) => { + table.integer('oapp_id').notNullable() + table.integer('target_chain_id').notNullable() + table.unique(['oapp_id', 'target_chain_id']) + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTable('oapp_remote') +} diff --git a/packages/backend/src/peripherals/database/shared/types.ts b/packages/backend/src/peripherals/database/shared/types.ts index b1fd16cf..8bff804c 100644 --- a/packages/backend/src/peripherals/database/shared/types.ts +++ b/packages/backend/src/peripherals/database/shared/types.ts @@ -82,6 +82,11 @@ declare module 'knex/types/tables' { configuration: string } + interface OAppRemoteRow { + oapp_id: number + target_chain_id: number + } + interface Tables { block_numbers: BlockNumberRow indexer_states: IndexerStateRow @@ -94,5 +99,6 @@ declare module 'knex/types/tables' { oapp: OAppRow oapp_configuration: OAppConfigurationRow oapp_default_configuration: OAppDefaultConfigurationRow + oapp_remote: OAppRemoteRow } } diff --git a/packages/backend/src/tracking/TrackingModule.ts b/packages/backend/src/tracking/TrackingModule.ts index 4fdb9ea0..0eb09608 100644 --- a/packages/backend/src/tracking/TrackingModule.ts +++ b/packages/backend/src/tracking/TrackingModule.ts @@ -16,6 +16,7 @@ import { ApplicationModule } from '../modules/ApplicationModule' import { CurrentDiscoveryRepository } from '../peripherals/database/CurrentDiscoveryRepository' import { OAppConfigurationRepository } from '../peripherals/database/OAppConfigurationRepository' import { OAppDefaultConfigurationRepository } from '../peripherals/database/OAppDefaultConfigurationRepository' +import { OAppRemoteRepository } from '../peripherals/database/OAppRemoteRepository' import { OAppRepository } from '../peripherals/database/OAppRepository' import { Database } from '../peripherals/database/shared/Database' import { ProtocolVersion } from './domain/const' @@ -23,8 +24,10 @@ import { ClockIndexer } from './domain/indexers/ClockIndexer' import { DefaultConfigurationIndexer } from './domain/indexers/DefaultConfigurationIndexer' import { OAppConfigurationIndexer } from './domain/indexers/OAppConfigurationIndexer' import { OAppListIndexer } from './domain/indexers/OAppListIndexer' +import { OAppRemoteIndexer } from './domain/indexers/OAppRemotesIndexer' import { DiscoveryDefaultConfigurationsProvider } from './domain/providers/DefaultConfigurationsProvider' import { BlockchainOAppConfigurationProvider } from './domain/providers/OAppConfigurationProvider' +import { BlockchainOAppRemotesProvider } from './domain/providers/OAppRemotesProvider' import { HttpOAppListProvider } from './domain/providers/OAppsListProvider' import { TrackingController } from './http/TrackingController' import { createTrackingRouter } from './http/TrackingRouter' @@ -45,6 +48,7 @@ interface SubmoduleDependencies { oApp: OAppRepository oAppConfiguration: OAppConfigurationRepository oAppDefaultConfiguration: OAppDefaultConfigurationRepository + oAppRemote: OAppRemoteRepository } } @@ -75,6 +79,11 @@ function createTrackingModule(dependencies: Dependencies): ApplicationModule { dependencies.logger, ) + const oAppRemoteRepo = new OAppRemoteRepository( + dependencies.database, + dependencies.logger, + ) + const controller = new TrackingController( oAppRepo, oAppConfigurationRepo, @@ -100,6 +109,7 @@ function createTrackingModule(dependencies: Dependencies): ApplicationModule { oApp: oAppRepo, oAppConfiguration: oAppConfigurationRepo, oAppDefaultConfiguration: oAppDefaultConfigurationRepo, + oAppRemote: oAppRemoteRepo, }, }, chainName, @@ -155,6 +165,13 @@ function createTrackingSubmodule( logger, ) + const oAppRemotesProvider = new BlockchainOAppRemotesProvider( + provider, + multicall, + chainId, + logger, + ) + const clockIndexer = new ClockIndexer(logger, config.tickIntervalMs, chainId) const oAppListIndexer = new OAppListIndexer( logger, @@ -164,13 +181,23 @@ function createTrackingSubmodule( [clockIndexer], ) + const remotesIndexer = new OAppRemoteIndexer( + logger, + chainId, + repositories.oApp, + repositories.oAppRemote, + oAppRemotesProvider, + [oAppListIndexer], + ) + const oAppConfigurationIndexer = new OAppConfigurationIndexer( logger, chainId, oAppConfigProvider, repositories.oApp, + repositories.oAppRemote, repositories.oAppConfiguration, - [oAppListIndexer], + [remotesIndexer], ) const defaultConfigurationIndexer = new DefaultConfigurationIndexer( @@ -189,6 +216,8 @@ function createTrackingSubmodule( await oAppListIndexer.start() await oAppConfigurationIndexer.start() await defaultConfigurationIndexer.start() + + await remotesIndexer.start() statusLogger.info('Tracking submodule started') }, } diff --git a/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts b/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts index 331bbbfb..b618aa39 100644 --- a/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts +++ b/packages/backend/src/tracking/domain/indexers/OAppConfigurationIndexer.ts @@ -6,6 +6,7 @@ import { OAppConfigurationRecord, OAppConfigurationRepository, } from '../../../peripherals/database/OAppConfigurationRepository' +import { OAppRemoteRepository } from '../../../peripherals/database/OAppRemoteRepository' import { OAppRepository } from '../../../peripherals/database/OAppRepository' import { OAppConfigurations } from '../configuration' import { OAppConfigurationProvider } from '../providers/OAppConfigurationProvider' @@ -17,6 +18,7 @@ export class OAppConfigurationIndexer extends ChildIndexer { private readonly chainId: ChainId, private readonly oAppConfigProvider: OAppConfigurationProvider, private readonly oAppRepo: OAppRepository, + private readonly oAppRemoteRepo: OAppRemoteRepository, private readonly oAppConfigurationRepo: OAppConfigurationRepository, parents: Indexer[], ) { @@ -25,17 +27,24 @@ 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 configurationRecords = await Promise.all( oApps.map(async (oApp) => { + const supportedChains = oAppsRemotes + .filter((remote) => remote.oAppId === oApp.id) + .map((remote) => remote.targetChainId) + const oAppConfigs = await this.oAppConfigProvider.getConfiguration( oApp.address, + supportedChains, ) return configToRecord(oAppConfigs, oApp.id) }), ) + await this.oAppConfigurationRepo.deleteAll() await this.oAppConfigurationRepo.addMany(configurationRecords.flat()) return to diff --git a/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts b/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts index d2a96030..57419dcf 100644 --- a/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts +++ b/packages/backend/src/tracking/domain/indexers/OAppListIndexer.ts @@ -25,6 +25,8 @@ export class OAppListIndexer extends ChildIndexer { amount: oApps.length, }) + await this.oAppRepo.deleteAll() + await this.oAppRepo.addMany( oApps.map((oApp) => ({ ...oApp, diff --git a/packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts b/packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts new file mode 100644 index 00000000..0b98588b --- /dev/null +++ b/packages/backend/src/tracking/domain/indexers/OAppRemotesIndexer.ts @@ -0,0 +1,58 @@ +import { Logger } from '@l2beat/backend-tools' +import { ChildIndexer, Indexer } from '@l2beat/uif' +import { ChainId } from '@lz/libs' + +import { + OAppRemoteRecord, + OAppRemoteRepository, +} from '../../../peripherals/database/OAppRemoteRepository' +import { OAppRepository } from '../../../peripherals/database/OAppRepository' +import { OAppRemotesProvider } from '../providers/OAppRemotesProvider' + +export class OAppRemoteIndexer extends ChildIndexer { + protected height = 0 + constructor( + logger: Logger, + private readonly chainId: ChainId, + private readonly oAppRepo: OAppRepository, + private readonly oAppRemotesRepo: OAppRemoteRepository, + private readonly oAppRemoteProvider: OAppRemotesProvider, + parents: Indexer[], + ) { + super(logger.tag(ChainId.getName(chainId)), parents) + } + + protected override async update(_from: number, to: number): Promise { + const oApps = await this.oAppRepo.getBySourceChain(this.chainId) + + const records: OAppRemoteRecord[][] = await Promise.all( + oApps.map(async (oApp) => { + const supportedRemoteChains = + await this.oAppRemoteProvider.getSupportedRemotes(oApp.address) + + return supportedRemoteChains.map((chainId) => ({ + oAppId: oApp.id, + targetChainId: chainId, + })) + }), + ) + + await this.oAppRemotesRepo.deleteAll() + await this.oAppRemotesRepo.addMany(records.flat()) + + 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/OAppConfigurationProvider.ts b/packages/backend/src/tracking/domain/providers/OAppConfigurationProvider.ts index a4a88f97..a7031ebd 100644 --- a/packages/backend/src/tracking/domain/providers/OAppConfigurationProvider.ts +++ b/packages/backend/src/tracking/domain/providers/OAppConfigurationProvider.ts @@ -16,7 +16,10 @@ export { BlockchainOAppConfigurationProvider } export type { OAppConfigurationProvider } interface OAppConfigurationProvider { - getConfiguration(address: EthereumAddress): Promise + getConfiguration( + address: EthereumAddress, + supportedChains: ChainId[], + ): Promise } const iface = new utils.Interface([ @@ -35,11 +38,10 @@ class BlockchainOAppConfigurationProvider implements OAppConfigurationProvider { } public async getConfiguration( address: EthereumAddress, + supportedChains: ChainId[], ): Promise { const blockNumber = await this.provider.getBlockNumber() - const supportedChains = ChainId.getAll() - const supportedEids = supportedChains.flatMap( (chainId) => EndpointID.encodeV1(chainId) ?? [], ) diff --git a/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts new file mode 100644 index 00000000..bb669eb7 --- /dev/null +++ b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts @@ -0,0 +1,164 @@ +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' + +export { BlockchainOAppRemotesProvider } +export type { OAppRemotesProvider } + +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. + * Classic OFT V1 remotes can be resolved via trusted remotes lookup + * Stargate-like remotes can be resolved via dstContractLookup + */ +class BlockchainOAppRemotesProvider implements OAppRemotesProvider { + constructor( + private readonly provider: providers.StaticJsonRpcProvider, + private readonly multicall: MulticallClient, + chainId: ChainId, + private readonly logger: Logger, + ) { + this.logger = this.logger.for(this).tag(ChainId.getName(chainId)) + } + public async getSupportedRemotes( + oAppAddress: EthereumAddress, + ): Promise { + const blockNumber = await this.provider.getBlockNumber() + + const supportedChains = ChainId.getAll() + + const supportedEndpoints = supportedChains.flatMap( + (chainId) => EndpointID.encodeV1(chainId) ?? [], + ) + + assert( + supportedEndpoints.length === supportedChains.length, + 'Cannot translate some chains to EID', + ) + + const oAppSupportedRemotes = await this.getSingleOAppRemotes( + oAppAddress, + supportedEndpoints, + blockNumber, + ) + + return oAppSupportedRemotes + } + + private async getSingleOAppRemotes( + oAppAddress: EthereumAddress, + supportedEndpoints: number[], + blockNumber: number, + ): Promise { + const isOft = await this.checkForOft(oAppAddress, blockNumber) + + const encode = isOft ? encodeOft : encodeStargate + const decode = isOft ? decodeOft : decodeStargate + + const requests = supportedEndpoints.map((eid) => encode(oAppAddress, eid)) + + const result = await this.multicall.multicall(requests, blockNumber) + + // Remap EIDs according to indexes, and return only supported ones + return ( + supportedEndpoints + .map((eid, i) => ({ + eid, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + supported: decode(result[i]!), + })) + .filter((x) => x.supported) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .map((x) => EndpointID.decodeV1(x.eid)!) + ) + } + + private async checkForOft( + oApp: EthereumAddress, + blockNumber: number, + ): Promise { + const data = oftIface.encodeFunctionData('trustedRemoteLookup', [ + // Example EID + EndpointID.encodeV1(ChainId.ETHEREUM) ?? 0, + ]) + + const request = { + address: oApp, + data: Bytes.fromHex(data), + } + + 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), + } + } +} From 140305320fff6aad86224ba14e91171577359204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Podsiad=C5=82y?= Date: Thu, 14 Mar 2024 20:10:37 +0100 Subject: [PATCH 2/4] test: resolve chains via argument --- .../domain/providers/OAppConfigurationProvider.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/tracking/domain/providers/OAppConfigurationProvider.test.ts b/packages/backend/src/tracking/domain/providers/OAppConfigurationProvider.test.ts index f83f4d14..08f5049a 100644 --- a/packages/backend/src/tracking/domain/providers/OAppConfigurationProvider.test.ts +++ b/packages/backend/src/tracking/domain/providers/OAppConfigurationProvider.test.ts @@ -40,7 +40,10 @@ describe(BlockchainOAppConfigurationProvider.name, () => { Logger.SILENT, ) - const result = await provider.getConfiguration(oAppAddress) + const result = await provider.getConfiguration( + oAppAddress, + ChainId.getAll(), + ) const keys = Object.keys(result) From 58e6805c4075ecd1ac4bcdd659d5f8cd25120711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Podsiad=C5=82y?= Date: Tue, 19 Mar 2024 17:18:14 +0100 Subject: [PATCH 3/4] feat: add oapp remotes resolver layer refac: abstract away resolvers refac: migrate memory height into abstract indexer --- .../database/OAppRemoteRepository.test.ts | 40 ++++++ .../database/OAppRemoteRepository.ts | 9 -- .../backend/src/tracking/TrackingModule.ts | 10 ++ .../indexers/DefaultConfigurationIndexer.ts | 19 +-- .../domain/indexers/InMemoryIndexer.ts | 19 +++ .../indexers/OAppConfigurationIndexer.ts | 25 ++-- .../domain/indexers/OAppListIndexer.ts | 19 +-- .../domain/indexers/OAppRemotesIndexer.ts | 19 +-- .../providers/OAppRemotesProvider.test.ts | 58 +++++++++ .../domain/providers/OAppRemotesProvider.ts | 114 ++++-------------- .../OFTInterfaceResolver.ts | 65 ++++++++++ .../interface-resolvers/StargateResolver.ts | 65 ++++++++++ .../providers/interface-resolvers/resolver.ts | 32 +++++ 13 files changed, 331 insertions(+), 163 deletions(-) create mode 100644 packages/backend/src/peripherals/database/OAppRemoteRepository.test.ts create mode 100644 packages/backend/src/tracking/domain/indexers/InMemoryIndexer.ts create mode 100644 packages/backend/src/tracking/domain/providers/OAppRemotesProvider.test.ts create mode 100644 packages/backend/src/tracking/domain/providers/interface-resolvers/OFTInterfaceResolver.ts create mode 100644 packages/backend/src/tracking/domain/providers/interface-resolvers/StargateResolver.ts create mode 100644 packages/backend/src/tracking/domain/providers/interface-resolvers/resolver.ts 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 00000000..b2ecafe2 --- /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 17e93bc5..e302d290 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 0eb09608..690ae0f8 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 4a277c68..82f4cf8c 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 00000000..7a71e702 --- /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 b618aa39..b6cbb2f6 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 57419dcf..e8472994 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 0b98588b..c069c1eb 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 00000000..c78bc99f --- /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 bb669eb7..e7033a88 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 00000000..7c992750 --- /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 00000000..2b4453fc --- /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 00000000..221a5d44 --- /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 +} From 2e4afffe4bad5ca5c7922e6f2a26db48a2ec6535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Podsiad=C5=82y?= Date: Tue, 19 Mar 2024 17:21:14 +0100 Subject: [PATCH 4/4] docs: remove comments --- .../src/tracking/domain/providers/OAppRemotesProvider.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts index e7033a88..35f4e2ec 100644 --- a/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts +++ b/packages/backend/src/tracking/domain/providers/OAppRemotesProvider.ts @@ -58,9 +58,7 @@ class BlockchainOAppRemotesProvider implements OAppRemotesProvider { supportedEndpoints: number[], blockNumber: number, ): Promise { - console.log('Looking for resolver') const resolver = await this.findResolver(oAppAddress, blockNumber) - console.log('Resolver found') assert( resolver,