diff --git a/packages/backend/src/tracking/TrackingModule.ts b/packages/backend/src/tracking/TrackingModule.ts index 690ae0f8..b7e6aa42 100644 --- a/packages/backend/src/tracking/TrackingModule.ts +++ b/packages/backend/src/tracking/TrackingModule.ts @@ -90,6 +90,7 @@ function createTrackingModule(dependencies: Dependencies): ApplicationModule { oAppRepo, oAppConfigurationRepo, oAppDefaultConfigurationRepo, + currDiscoveryRepo, ) const router = createTrackingRouter(controller) diff --git a/packages/backend/src/tracking/http/TrackingController.test.ts b/packages/backend/src/tracking/http/TrackingController.test.ts index 7062df2f..06d2829c 100644 --- a/packages/backend/src/tracking/http/TrackingController.test.ts +++ b/packages/backend/src/tracking/http/TrackingController.test.ts @@ -1,7 +1,8 @@ import { assert } from '@l2beat/backend-tools' import { ChainId, EthereumAddress } from '@lz/libs' -import { expect, mockObject } from 'earl' +import { expect, mockFn, mockObject } from 'earl' +import { CurrentDiscoveryRepository } from '../../peripherals/database/CurrentDiscoveryRepository' import { OAppConfigurationRecord, OAppConfigurationRepository, @@ -28,11 +29,15 @@ describe(TrackingController.name, () => { mockObject({ getBySourceChain: () => Promise.resolve([]), }) + const currDiscoveryRepo = mockObject({ + find: mockFn().resolvesTo(null), + }) const controller = new TrackingController( oAppRepo, oAppConfigRepo, oAppDefaultConfigRepo, + currDiscoveryRepo, ) const result = await controller.getOApps(chainId) @@ -127,6 +132,31 @@ describe(TrackingController.name, () => { }, ] + const mockDiscoveryOutput = { + contracts: [ + { + name: 'Oracle', + address: EthereumAddress.random(), + unverified: false, + }, + { + name: 'Relayer', + address: EthereumAddress.random(), + unverified: true, + }, + { + name: 'Endpoint', + address: EthereumAddress.random(), + unverified: false, + }, + ], + eoas: [ + EthereumAddress.random(), + EthereumAddress.random(), + EthereumAddress.random(), + ], + } + const oAppRepo = mockObject({ getBySourceChain: () => Promise.resolve([oAppA, oAppB]), }) @@ -138,11 +168,18 @@ describe(TrackingController.name, () => { mockObject({ getBySourceChain: () => Promise.resolve(mockDefaultConfigurations), }) + const currDiscoveryRepo = mockObject({ + find: mockFn().resolvesTo({ + chainId, + discoveryOutput: mockDiscoveryOutput, + }), + }) const controller = new TrackingController( oAppRepo, oAppConfigRepo, oAppDefaultConfigRepo, + currDiscoveryRepo, ) const result = await controller.getOApps(chainId) @@ -199,6 +236,19 @@ describe(TrackingController.name, () => { configuration: c.configuration, })), ) + + expect(result.addressInfo).toEqual([ + ...mockDiscoveryOutput.contracts.map((contract) => ({ + name: contract.name, + address: contract.address, + verified: !contract.unverified, + })), + ...mockDiscoveryOutput.eoas.map((eoa) => ({ + name: 'EOA', + address: eoa, + verified: true, + })), + ]) }) }) }) diff --git a/packages/backend/src/tracking/http/TrackingController.ts b/packages/backend/src/tracking/http/TrackingController.ts index e7cf63d8..ad388320 100644 --- a/packages/backend/src/tracking/http/TrackingController.ts +++ b/packages/backend/src/tracking/http/TrackingController.ts @@ -1,11 +1,14 @@ import { assert } from '@l2beat/backend-tools' +import { DiscoveryOutput } from '@l2beat/discovery-types' import { + AddressInfo, ChainId, OAppsResponse, OAppWithConfigs, ResolvedConfigurationWithAppId, } from '@lz/libs' +import { CurrentDiscoveryRepository } from '../../peripherals/database/CurrentDiscoveryRepository' import { OAppConfigurationRecord, OAppConfigurationRepository, @@ -27,9 +30,18 @@ class TrackingController { private readonly oAppRepo: OAppRepository, private readonly oAppConfigurationRepo: OAppConfigurationRepository, private readonly oAppDefaultConfigRepo: OAppDefaultConfigurationRepository, + private readonly currDiscoveryRepository: CurrentDiscoveryRepository, ) {} async getOApps(chainId: ChainId): Promise { + const discovery = await this.currDiscoveryRepository.find(chainId) + + if (!discovery) { + return null + } + + const addressInfo = outputToAddressInfo(discovery.discoveryOutput) + const defaultConfigurations = await this.oAppDefaultConfigRepo.getBySourceChain(chainId) @@ -62,10 +74,23 @@ class TrackingController { targetChainId: record.targetChainId, configuration: record.configuration, })), + + addressInfo, } } } +function outputToAddressInfo(output: DiscoveryOutput): AddressInfo[] { + const { eoas, contracts } = output + + return contracts + .map((contract) => ({ + address: contract.address, + name: contract.name, + verified: !contract.unverified, + })) + .concat(eoas.map((eoa) => ({ address: eoa, name: 'EOA', verified: true }))) +} function attachConfigurations( oApps: OAppRecord[], configurations: ResolvedConfigurationWithAppId[], diff --git a/packages/frontend/src/hooks/useTrackingApi.ts b/packages/frontend/src/hooks/useTrackingApi.ts new file mode 100644 index 00000000..8ced87fc --- /dev/null +++ b/packages/frontend/src/hooks/useTrackingApi.ts @@ -0,0 +1,62 @@ +import { ChainId, OAppsResponse } from '@lz/libs' +import { useEffect, useState } from 'react' + +import { hasBeenAborted } from './utils' + +export interface UseTrackingApiHookOptions { + chainId: ChainId + apiUrl: string +} + +export interface TrackingData { + data: OAppsResponse + chainId: ChainId +} + +export function useTrackingApi({ chainId, apiUrl }: UseTrackingApiHookOptions) { + const [isLoading, setIsLoading] = useState(true) + const [isError, setIsError] = useState(false) + const [data, setData] = useState(null) + + useEffect(() => { + const abortController = new AbortController() + + setIsLoading(true) + setIsError(false) + async function fetchData() { + try { + const result = await fetch( + apiUrl + 'tracking/' + ChainId.getName(chainId), + { + signal: abortController.signal, + }, + ) + + if (!result.ok) { + setIsLoading(false) + setIsError(true) + } + + const data = await result.text() + const parsed = OAppsResponse.parse(JSON.parse(data)) + setData({ data: parsed, chainId }) + setIsError(false) + setIsLoading(false) + } catch (e) { + console.error(e) + if (hasBeenAborted(e)) { + return + } + setIsError(true) + } + } + + void fetchData() + + return () => { + abortController.abort() + } + }, [chainId, apiUrl]) + + return [data, isLoading, isError] as const +} diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx index f4223791..7bd7fc59 100644 --- a/packages/frontend/src/main.tsx +++ b/packages/frontend/src/main.tsx @@ -5,6 +5,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { BrowserRouter, Route, Routes } from 'react-router-dom' +import { Applications } from './pages/Applications' import { Main } from './pages/Main' import { Status } from './pages/Status' @@ -15,6 +16,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render( } /> } /> + } /> , diff --git a/packages/frontend/src/pages/Applications.tsx b/packages/frontend/src/pages/Applications.tsx new file mode 100644 index 00000000..ba696457 --- /dev/null +++ b/packages/frontend/src/pages/Applications.tsx @@ -0,0 +1,57 @@ +import { ChainId } from '@lz/libs' +import { SkeletonTheme } from 'react-loading-skeleton' + +import { config } from '../config' +import { AddressInfoContext } from '../hooks/addressInfoContext' +import { useTrackingApi } from '../hooks/useTrackingApi' +import { Layout } from '../view/components/Layout' +import { Navbar } from '../view/components/Navbar' +import { OAppTable, OAppTableSkeleton } from '../view/components/oapp/OAppTable' +import { Warning } from '../view/components/Warning' + +export function Applications() { + const chainsToDisplay = [ChainId.ETHEREUM] as [ChainId, ...ChainId[]] + + const [oApps, isLoading, isError] = useTrackingApi({ + chainId: chainsToDisplay[0], + apiUrl: config.apiUrl, + }) + + if (isLoading) { + return ( + + + + + + ) + } + + if (!oApps || isError) { + return ( + + + + ) + } + + return ( + + + + + + ) +} + +function Page({ children }: { children: React.ReactNode }) { + return ( + <> + + {children} + + ) +} diff --git a/packages/frontend/src/view/components/Navbar.tsx b/packages/frontend/src/view/components/Navbar.tsx index ea0d68ed..9927c090 100644 --- a/packages/frontend/src/view/components/Navbar.tsx +++ b/packages/frontend/src/view/components/Navbar.tsx @@ -1,17 +1,58 @@ +import cx from 'classnames' +import { useNavigate } from 'react-router-dom' + import { CollabLogo } from '../icons/CollabIcon' import { L2BeatLogo } from '../icons/L2BeatLogo' import { LayerZeroLogo } from '../icons/LayerZeroLogo' export function Navbar(): JSX.Element { + const navigate = useNavigate() + const path = location.pathname + return ( -