Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OApps tracking PoC #112

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/backend/src/tracking/TrackingModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function createTrackingModule(dependencies: Dependencies): ApplicationModule {
oAppRepo,
oAppConfigurationRepo,
oAppDefaultConfigurationRepo,
currDiscoveryRepo,
)

const router = createTrackingRouter(controller)
Expand Down
52 changes: 51 additions & 1 deletion packages/backend/src/tracking/http/TrackingController.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -28,11 +29,15 @@ describe(TrackingController.name, () => {
mockObject<OAppDefaultConfigurationRepository>({
getBySourceChain: () => Promise.resolve([]),
})
const currDiscoveryRepo = mockObject<CurrentDiscoveryRepository>({
find: mockFn().resolvesTo(null),
})

const controller = new TrackingController(
oAppRepo,
oAppConfigRepo,
oAppDefaultConfigRepo,
currDiscoveryRepo,
)
const result = await controller.getOApps(chainId)

Expand Down Expand Up @@ -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<OAppRepository>({
getBySourceChain: () => Promise.resolve([oAppA, oAppB]),
})
Expand All @@ -138,11 +168,18 @@ describe(TrackingController.name, () => {
mockObject<OAppDefaultConfigurationRepository>({
getBySourceChain: () => Promise.resolve(mockDefaultConfigurations),
})
const currDiscoveryRepo = mockObject<CurrentDiscoveryRepository>({
find: mockFn().resolvesTo({
chainId,
discoveryOutput: mockDiscoveryOutput,
}),
})

const controller = new TrackingController(
oAppRepo,
oAppConfigRepo,
oAppDefaultConfigRepo,
currDiscoveryRepo,
)

const result = await controller.getOApps(chainId)
Expand Down Expand Up @@ -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,
})),
])
})
})
})
Expand Down
25 changes: 25 additions & 0 deletions packages/backend/src/tracking/http/TrackingController.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<OAppsResponse | null> {
const discovery = await this.currDiscoveryRepository.find(chainId)

if (!discovery) {
return null
}

const addressInfo = outputToAddressInfo(discovery.discoveryOutput)

const defaultConfigurations =
await this.oAppDefaultConfigRepo.getBySourceChain(chainId)

Expand Down Expand Up @@ -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[],
Expand Down
62 changes: 62 additions & 0 deletions packages/frontend/src/hooks/useTrackingApi.ts
Original file line number Diff line number Diff line change
@@ -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<TrackingData | null>(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
}
2 changes: 2 additions & 0 deletions packages/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -15,6 +16,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<Routes>
<Route path="/" element={<Main />} />
<Route path="/status" element={<Status />} />
<Route path="/applications" element={<Applications />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
Expand Down
57 changes: 57 additions & 0 deletions packages/frontend/src/pages/Applications.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Page>
<SkeletonTheme baseColor="#27272A" highlightColor="#525252">
<OAppTableSkeleton />
</SkeletonTheme>
</Page>
)
}

if (!oApps || isError) {
return (
<Page>
<Warning
title="Could not load applications"
subtitle="Applications API might be unreachable or no applications were discovered. Please try again later."
/>
</Page>
)
}

return (
<Page>
<AddressInfoContext.Provider value={oApps.data.addressInfo}>
<OAppTable oAppsList={oApps.data} />
</AddressInfoContext.Provider>
</Page>
)
}

function Page({ children }: { children: React.ReactNode }) {
return (
<>
<Navbar />
<Layout>{children}</Layout>
</>
)
}
45 changes: 43 additions & 2 deletions packages/frontend/src/view/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className="bg-black px-8 py-4">
<a href="/">
<nav className="flex items-center justify-between bg-black px-8 py-4">
<a onClick={() => navigate('/')} className="cursor-pointer">
<h1 className="inline-flex items-center gap-5">
<LayerZeroLogo className="h-7" />
<CollabLogo className="h-2.5" />
<L2BeatLogo className="h-7" />
</h1>
</a>
<div>
<NavItem
active={path === '/'}
onClick={() => navigate('/')}
label="Defaults"
/>
<NavItem
active={path === '/applications'}
onClick={() => navigate('/applications')}
label="Applications"
/>
</div>
</nav>
)
}

interface NavItemProps {
active: boolean
onClick: () => void
label: string
}

function NavItem(props: NavItemProps) {
const highlight = props.active
? 'text-yellow-100'
: 'text-white hover:text-yellow-200'
return (
<button
onClick={props.onClick}
className={cx(
'bg-transparent px-2 py-1 text-xs uppercase duration-300',
highlight,
)}
>
{props.label}
</button>
)
}
Loading
Loading