From e0e3900c722ae94c5f4b499d15ae1030e61fc6e6 Mon Sep 17 00:00:00 2001 From: Jeff Reiner <8116716+mirshko@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:47:48 +0200 Subject: [PATCH 1/7] Implement first pass at chainregistry as middleware --- apps/contract-verification/.env.example | 4 + apps/contract-verification/env.d.ts | 5 + apps/contract-verification/src/index.tsx | 24 +- .../src/lib/chain-registry.ts | 254 +++++++++++++ .../contract-verification/src/route.lookup.ts | 51 +-- .../src/route.verify-legacy.ts | 9 +- .../contract-verification/src/route.verify.ts | 13 +- .../contract-verification/src/wagmi.config.ts | 25 +- .../test/e2e/verification.test.ts | 5 +- .../test/integration/route.lookup.test.ts | 8 +- .../integration/route.verify-legacy.test.ts | 22 +- .../test/unit/chain-registry.test.ts | 350 ++++++++++++++++++ apps/contract-verification/wrangler.json | 4 +- 13 files changed, 707 insertions(+), 67 deletions(-) create mode 100644 apps/contract-verification/src/lib/chain-registry.ts create mode 100644 apps/contract-verification/test/unit/chain-registry.test.ts diff --git a/apps/contract-verification/.env.example b/apps/contract-verification/.env.example index f2b64b04f..587d4ca15 100644 --- a/apps/contract-verification/.env.example +++ b/apps/contract-verification/.env.example @@ -11,3 +11,7 @@ VITE_LOG_LEVEL="debug" VITE_BASE_URL="http://localhost:6969" WRANGLER_SEND_METRICS=false + +# Dynamic chain registry (optional) +# CHAINS_CONFIG_URL="https://example.com/chains" +# CHAINS_CONFIG_AUTH_TOKEN is a Cloudflare Secrets Store binding (see wrangler.json secrets_store_secrets) diff --git a/apps/contract-verification/env.d.ts b/apps/contract-verification/env.d.ts index 9ca5c6eb1..cf70cdd0a 100644 --- a/apps/contract-verification/env.d.ts +++ b/apps/contract-verification/env.d.ts @@ -19,6 +19,11 @@ interface EnvironmentVariables { readonly CLOUDFLARE_DATABASE_ID: string readonly CLOUDFLARE_D1_TOKEN: string readonly CLOUDFLARE_D1_ENVIRONMENT: 'local' | (string & {}) + + /** URL to fetch dynamic chain configs from (optional). */ + readonly CHAINS_CONFIG_URL?: string + /** Bearer token for authenticating with the chain config endpoint (optional). */ + readonly CHAINS_CONFIG_AUTH_TOKEN?: string } // Node.js `process.env` auto-completion diff --git a/apps/contract-verification/src/index.tsx b/apps/contract-verification/src/index.tsx index 865cdb35e..f276698a6 100644 --- a/apps/contract-verification/src/index.tsx +++ b/apps/contract-verification/src/index.tsx @@ -13,10 +13,11 @@ import { contextStorage } from 'hono/context-storage' import { docsRoute } from '#route.docs.tsx' import { verifyRoute } from '#route.verify.ts' -import { sourcifyChains } from '#wagmi.config.ts' +import { staticChains } from '#wagmi.config.ts' import { VerificationContainer } from '#container.ts' import { VerificationJobRunner } from '#job-runner.ts' import { legacyVerifyRoute } from '#route.verify-legacy.ts' +import { type ChainRegistry, chainRegistry } from '#lib/chain-registry.ts' import { configureLogger, getLogger, withContext } from '#lib/logger.ts' import { lookupAllChainContractsRoute, lookupRoute } from '#route.lookup.ts' import { handleError, originMatches, sourcifyError } from '#lib/utilities.ts' @@ -42,7 +43,11 @@ function isWhitelistedOrigin(origin: string | undefined) { ) } -type AppEnv = { Bindings: Cloudflare.Env } +export type AppEnv = { + Bindings: Cloudflare.Env + Variables: { chainRegistry: ChainRegistry } +} + const factory = createFactory() export const app = factory.createApp() @@ -66,6 +71,16 @@ app.use(async (context, next) => { next, ) }) + +// Chain registry middleware -- fetches dynamic chains if CHAINS_CONFIG_URL is set +app.use( + chainRegistry({ + staticChains, + url: env.CHAINS_CONFIG_URL || undefined, + authToken: env.CHAINS_CONFIG_AUTH_TOKEN || undefined, + }), +) + app.use( cors({ allowMethods: ['GET', 'POST', 'OPTIONS', 'HEAD'], @@ -129,8 +144,9 @@ app.get('/favicon.ico', (context) => app .get('/health', (context) => context.text('ok')) .get('/', (context) => context.redirect('/docs')) - // TODO: match sourcify `https://sourcify.dev/server/chains` response schema - .get('/chains', (context) => context.json(sourcifyChains)) + .get('/chains', (context) => + context.json(context.get('chainRegistry').getSourcifyChains()), + ) .get('/version', async (context) => context.json({ version: packageJSON.version, diff --git a/apps/contract-verification/src/lib/chain-registry.ts b/apps/contract-verification/src/lib/chain-registry.ts new file mode 100644 index 000000000..23766f805 --- /dev/null +++ b/apps/contract-verification/src/lib/chain-registry.ts @@ -0,0 +1,254 @@ +import * as z from 'zod/mini' +import type { Chain } from 'viem' +import { defineChain } from 'viem' +import { createMiddleware } from 'hono/factory' + +import { getLogger } from '#lib/logger.ts' + +const logger = getLogger(['tempo', 'chain-registry']) + +// --------------------------------------------------------------------------- +// Zod schema -- chainlist.org-compatible, most fields optional +// --------------------------------------------------------------------------- + +const zExplorer = z.object({ + name: z.string(), + url: z.string(), + standard: z.optional(z.string()), +}) + +const zNativeCurrency = z.object({ + name: z.string(), + symbol: z.string(), + decimals: z.number(), +}) + +const zChainEntry = z.object({ + chainId: z.number(), + rpc: z.array(z.string()).check(z.minLength(1)), + hidden: z.optional(z.boolean()), + name: z.optional(z.string()), + chain: z.optional(z.string()), + shortName: z.optional(z.string()), + infoURL: z.optional(z.string()), + nativeCurrency: z.optional(zNativeCurrency), + explorers: z.optional(z.array(zExplorer)), +}) + +const zChainsResponse = z.record(z.string(), zChainEntry) + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type ChainEntry = { + chain: Chain + hidden: boolean +} + +export type SourcifyChain = { + name: string + title: string + chainId: number + rpc: string[] + supported: boolean + etherscanAPI: boolean + _extra: Record +} + +// --------------------------------------------------------------------------- +// ChainRegistry +// --------------------------------------------------------------------------- + +export class ChainRegistry { + private readonly entries: Map + + constructor(entries: Map) { + this.entries = entries + } + + /** Build a registry from static chains only (no external fetch). */ + static fromStatic(staticChains: readonly Chain[]): ChainRegistry { + const entries = new Map() + for (const chain of staticChains) { + entries.set(chain.id, { chain, hidden: false }) + } + return new ChainRegistry(entries) + } + + /** Fetch chain configs from an external URL and merge with static chains. */ + static async fromUrl(options: { + url: string + authToken?: string | undefined + staticChains: readonly Chain[] + }): Promise { + const registry = ChainRegistry.fromStatic(options.staticChains) + + try { + const headers = new Headers({ Accept: 'application/json' }) + if (options.authToken) { + headers.set('Authorization', `Bearer ${options.authToken}`) + } + + const response = await fetch(options.url, { headers }) + + if (!response.ok) { + logger.warn('chain_registry_fetch_failed', { + status: response.status, + url: options.url, + }) + return registry + } + + const json = await response.json() + const parsed = zChainsResponse.safeParse(json) + + if (!parsed.success) { + logger.warn('chain_registry_parse_failed', { + error: parsed.error, + url: options.url, + }) + return registry + } + + for (const [key, entry] of Object.entries(parsed.data)) { + const chainId = entry.chainId + + // static chains always take precedence + if (registry.entries.has(chainId)) { + logger.debug('chain_registry_skip_static', { + chainId, + key, + }) + continue + } + + const httpUrls = entry.rpc.filter( + (url) => url.startsWith('http://') || url.startsWith('https://'), + ) + if (httpUrls.length === 0) { + logger.warn('chain_registry_no_http_rpc', { chainId, key }) + continue + } + + const defaultExplorer = entry.explorers?.[0] + + const chain = defineChain({ + id: chainId, + name: entry.name ?? `Chain ${chainId}`, + nativeCurrency: entry.nativeCurrency ?? { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: { + default: { + http: httpUrls as [string, ...string[]], + }, + }, + ...(defaultExplorer + ? { + blockExplorers: { + default: { + name: defaultExplorer.name, + url: defaultExplorer.url, + }, + }, + } + : {}), + }) + + registry.entries.set(chainId, { + chain, + hidden: entry.hidden ?? false, + }) + } + + logger.info('chain_registry_loaded', { + total: registry.entries.size, + dynamic: registry.entries.size - options.staticChains.length, + url: options.url, + }) + } catch (error) { + logger.warn('chain_registry_error', { + error: error instanceof Error ? error.message : String(error), + url: options.url, + }) + } + + return registry + } + + /** Returns the viem Chain for a given ID, regardless of hidden flag. */ + getChain(chainId: number): Chain | undefined { + return this.entries.get(chainId)?.chain + } + + /** Whether a chain ID exists in the registry, regardless of hidden flag. */ + isSupported(chainId: number): boolean { + return this.entries.has(chainId) + } + + /** Whether a chain ID is marked as hidden. Returns false for unknown chains. */ + isHidden(chainId: number): boolean { + return this.entries.get(chainId)?.hidden ?? false + } + + /** Returns all non-hidden chains in Sourcify-compatible format. */ + getSourcifyChains(): SourcifyChain[] { + const result: SourcifyChain[] = [] + for (const { chain, hidden } of this.entries.values()) { + if (hidden) continue + const extra: Record = {} + if (chain.blockExplorers) { + extra.blockExplorer = chain.blockExplorers.default + } + result.push({ + name: chain.name, + title: chain.name, + chainId: chain.id, + rpc: [...chain.rpcUrls.default.http], + supported: true, + etherscanAPI: false, + _extra: extra, + }) + } + return result + } +} + +// --------------------------------------------------------------------------- +// Hono middleware factory +// --------------------------------------------------------------------------- + +/** + * Creates a Hono middleware that initializes a `ChainRegistry` and sets it on + * the context. + * + * ```ts + * app.use(chainRegistry({ + * staticChains, + * url: context.env.CHAINS_CONFIG_URL, + * authToken: context.env.CHAINS_CONFIG_AUTH_TOKEN, + * })) + * ``` + */ +export function chainRegistry(options: { + staticChains: readonly Chain[] + url?: string | undefined + authToken?: string | undefined +}) { + return createMiddleware<{ + Variables: { chainRegistry: ChainRegistry } + }>(async (context, next) => { + const registry = options.url + ? await ChainRegistry.fromUrl({ + url: options.url, + authToken: options.authToken, + staticChains: options.staticChains, + }) + : ChainRegistry.fromStatic(options.staticChains) + context.set('chainRegistry', registry) + await next() + }) +} diff --git a/apps/contract-verification/src/route.lookup.ts b/apps/contract-verification/src/route.lookup.ts index 61ee653b9..a4d1f99b5 100644 --- a/apps/contract-verification/src/route.lookup.ts +++ b/apps/contract-verification/src/route.lookup.ts @@ -16,8 +16,8 @@ import { compiledContractsSignaturesTable, nativeContractRevisionSourcesTable, } from '#database/schema.ts' +import type { AppEnv } from '#index.tsx' import { getLogger } from '#lib/logger.ts' -import { chainIds } from '#wagmi.config.ts' import { formatError, getDb, sourcifyError } from '#lib/utilities.ts' const logger = getLogger(['tempo']) @@ -484,8 +484,8 @@ async function getNativeLookupResponse( * GET /v2/contracts/{chainId} */ -const lookupRoute = new Hono<{ Bindings: Cloudflare.Env }>() -const lookupAllChainContractsRoute = new Hono<{ Bindings: Cloudflare.Env }>() +const lookupRoute = new Hono() +const lookupAllChainContractsRoute = new Hono() // GET /v2/contract/all-chains/:address - Get verified contract at an address on all chains // Note: This route must be defined before /:chainId/:address to avoid matching conflicts @@ -538,24 +538,31 @@ lookupRoute .where(eq(nativeContractsTable.address, addressBytes)), ]) + const registry = context.get('chainRegistry') + + // Filter out results for hidden chains to prevent leaking chain IDs const contracts = [ - ...verifiedResults.map((row) => - buildVerifiedMinimalResponse({ - matchId: row.matchId, - runtimeMatch: row.runtimeMatch, - creationMatch: row.creationMatch, - chainId: row.chainId, - address: row.address as ArrayBuffer, - verifiedAt: row.verifiedAt, - }), - ), - ...nativeResults.map((row) => - buildNativeMinimalResponse({ - id: row.id, - chainId: row.chainId, - address: row.address as ArrayBuffer, - }), - ), + ...verifiedResults + .filter((row) => !registry.isHidden(row.chainId)) + .map((row) => + buildVerifiedMinimalResponse({ + matchId: row.matchId, + runtimeMatch: row.runtimeMatch, + creationMatch: row.creationMatch, + chainId: row.chainId, + address: row.address as ArrayBuffer, + verifiedAt: row.verifiedAt, + }), + ), + ...nativeResults + .filter((row) => !registry.isHidden(row.chainId)) + .map((row) => + buildNativeMinimalResponse({ + id: row.id, + chainId: row.chainId, + address: row.address as ArrayBuffer, + }), + ), ].toSorted( (a, b) => Number(a.chainId) - Number(b.chainId) || @@ -592,7 +599,7 @@ lookupRoute 'invalid_chain_id', `Invalid chainId format: ${chainId}`, ) - if (!chainIds.includes(chainIdNumber)) + if (!context.get('chainRegistry').isSupported(chainIdNumber)) return sourcifyError( context, 400, @@ -981,7 +988,7 @@ lookupAllChainContractsRoute.get('/:chainId', async (context) => { 'invalid_chain_id', `Invalid chainId format: ${chainId}`, ) - if (!chainIds.includes(chainIdNumber)) + if (!context.get('chainRegistry').isSupported(chainIdNumber)) return sourcifyError( context, 400, diff --git a/apps/contract-verification/src/route.verify-legacy.ts b/apps/contract-verification/src/route.verify-legacy.ts index fdc11b843..adc13c26f 100644 --- a/apps/contract-verification/src/route.verify-legacy.ts +++ b/apps/contract-verification/src/route.verify-legacy.ts @@ -31,7 +31,7 @@ import { type ImmutableReferences, getVyperImmutableReferences, } from '#lib/bytecode-matching.ts' -import { chains, chainIds } from '#wagmi.config.ts' +import type { AppEnv } from '#index.tsx' import { getLogger } from '#lib/logger.ts' const logger = getLogger(['tempo']) @@ -54,7 +54,7 @@ const LegacyVyperRequestSchema = z.object({ creatorTxHash: z.optional(z.string()), }) -const legacyVerifyRoute = new Hono<{ Bindings: Cloudflare.Env }>() +const legacyVerifyRoute = new Hono() // POST /verify/vyper - Legacy Sourcify Vyper verification (used by Foundry) legacyVerifyRoute.post('/vyper', async (context) => { @@ -104,7 +104,8 @@ legacyVerifyRoute.post('/vyper', async (context) => { } = body const chainId = Number(chain) - if (!chainIds.includes(chainId)) { + const registry = context.get('chainRegistry') + if (!registry.isSupported(chainId)) { return sourcifyError( context, 400, @@ -167,7 +168,7 @@ legacyVerifyRoute.post('/vyper', async (context) => { }) } - const chainConfig = chains.find((chain) => chain.id === chainId) + const chainConfig = registry.getChain(chainId) if (!chainConfig) { return sourcifyError( context, diff --git a/apps/contract-verification/src/route.verify.ts b/apps/contract-verification/src/route.verify.ts index 1e252d5da..5cb1fc483 100644 --- a/apps/contract-verification/src/route.verify.ts +++ b/apps/contract-verification/src/route.verify.ts @@ -35,7 +35,8 @@ import { type ImmutableReferences, getVyperImmutableReferences, } from '#lib/bytecode-matching.ts' -import { chains, chainIds } from '#wagmi.config.ts' +import type { AppEnv } from '#index.tsx' +import type { ChainRegistry } from '#lib/chain-registry.ts' import { getLogger } from '#lib/logger.ts' const logger = getLogger(['tempo']) @@ -77,7 +78,7 @@ function timestampToMs(value: string): number { * POST /verify/solc-json */ -const verifyRoute = new Hono<{ Bindings: Cloudflare.Env }>() +const verifyRoute = new Hono() // POST /v2/verify/metadata/:chainId/:address - Verify Contract (using Solidity metadata.json) verifyRoute @@ -149,7 +150,8 @@ verifyRoute } const chainId = Number(_chainId) - if (!chainIds.includes(chainId)) { + const registry = context.get('chainRegistry') + if (!registry.isSupported(chainId)) { return sourcifyError( context, 400, @@ -617,7 +619,7 @@ type VerificationDeps = { name: string, ) => ContainerLike createPublicClient?: (params: { - chain: (typeof chains)[keyof typeof chains] + chain: import('viem').Chain transport: ReturnType }) => PublicClientLike } @@ -666,6 +668,7 @@ async function runVerificationJob( chainId: number, address: string, body: VerificationInput, + chainRegistry: ChainRegistry, deps?: VerificationDeps, ): Promise { const db = getDb(env.CONTRACTS_DB) @@ -682,7 +685,7 @@ async function runVerificationJob( const contractName = contractIdentifier.slice(lastColonIndex + 1) try { - const chain = chains.find((chain) => chain.id === chainId) + const chain = chainRegistry.getChain(chainId) if (!chain) { throw new Error(`Chain ${chainId} is not supported`) } diff --git a/apps/contract-verification/src/wagmi.config.ts b/apps/contract-verification/src/wagmi.config.ts index 455aaf02e..622334853 100644 --- a/apps/contract-verification/src/wagmi.config.ts +++ b/apps/contract-verification/src/wagmi.config.ts @@ -20,38 +20,19 @@ export const tempoTestnetExtended = tempoTestnet.extend({ feeToken: '0x20c0000000000000000000000000000000000001', }) -export const chainIds = [ - tempoDevnet.id, - tempoTestnet.id, - tempoMainnet.id, -] as const -export type ChainId = (typeof chainIds)[number] -export const chains = [ +/** Static Tempo chains -- always available, cannot be overridden by dynamic config. */ +export const staticChains = [ tempoDevnetExtended, tempoTestnetExtended, tempoMainnetExtended, ] as const + export const chainFeeTokens = { [tempoDevnet.id]: tempoDevnetExtended.feeToken, [tempoTestnet.id]: tempoTestnetExtended.feeToken, [tempoMainnet.id]: tempoMainnetExtended.feeToken, } as const -export const sourcifyChains = chains.map((chain) => { - const returnValue = { - name: chain.name, - title: chain.name, - chainId: chain.id, - rpc: [chain.rpcUrls.default.http, chain.rpcUrls.default.webSocket].flat(), - supported: true, - etherscanAPI: false, - _extra: {}, - } - if (chain?.blockExplorers) - returnValue._extra = { blockExplorer: chain?.blockExplorers.default } - return returnValue -}) - export const zAddress = (opts?: { lowercase?: boolean }) => z.pipe( z.string(), diff --git a/apps/contract-verification/test/e2e/verification.test.ts b/apps/contract-verification/test/e2e/verification.test.ts index d4b94b4ea..21540024d 100644 --- a/apps/contract-verification/test/e2e/verification.test.ts +++ b/apps/contract-verification/test/e2e/verification.test.ts @@ -6,6 +6,8 @@ import { describe, expect, it } from 'vitest' import * as DB from '#database/schema.ts' import { runVerificationJob } from '#route.verify.ts' +import { staticChains } from '#wagmi.config.ts' +import { ChainRegistry } from '#lib/chain-registry.ts' import { counterFixture } from '../fixtures/counter.fixture.ts' const VerificationIdSchema = z.object({ verificationId: z.string() }) @@ -62,12 +64,13 @@ describe('full verification flow', () => { counterFixture.chainId, counterFixture.address, verifyRequestBody, + ChainRegistry.fromStatic(staticChains), { createPublicClient: () => ({ getCode: async () => counterFixture.onchainRuntimeBytecode, }), getContainer: () => ({ - fetch: async (request) => { + fetch: async (request: Request) => { const url = new URL(request.url) if (request.method === 'POST' && url.pathname === '/compile') { return Response.json(counterFixture.solcOutput, { status: 200 }) diff --git a/apps/contract-verification/test/integration/route.lookup.test.ts b/apps/contract-verification/test/integration/route.lookup.test.ts index a22ad5263..b877cb74e 100644 --- a/apps/contract-verification/test/integration/route.lookup.test.ts +++ b/apps/contract-verification/test/integration/route.lookup.test.ts @@ -7,9 +7,11 @@ import { describe, it, expect } from 'vitest' import { app } from '#index.tsx' import * as DB from '#database/schema.ts' -import { chainIds } from '#wagmi.config.ts' +import { staticChains } from '#wagmi.config.ts' import { validatorConfigV2Manifest } from '../../scripts/precompile-seed/manifest.ts' +const staticChainIds = staticChains.map((c) => c.id) + async function insertNativePrecompileFixture(): Promise<{ nativeContractId: string chainId: number @@ -23,7 +25,7 @@ async function insertNativePrecompileFixture(): Promise<{ sourceIds: Record }> { const db = drizzle(env.CONTRACTS_DB) - const chainId = chainIds.includes(4217) ? 4217 : chainIds[0] + const chainId = staticChainIds.includes(4217) ? 4217 : staticChainIds[0] if (!chainId) { throw new Error('expected at least one configured chain ID') } @@ -147,7 +149,7 @@ describe('gET /v2/contract/all-chains/:address', () => { it('returns verified contracts for a valid address', async () => { const db = drizzle(env.CONTRACTS_DB) - const chainId = chainIds[0] + const chainId = staticChains[0].id const address = '0x1111111111111111111111111111111111111111' const addressBytes = Hex.toBytes(address) const runtimeHash = new Uint8Array(32).fill(1) diff --git a/apps/contract-verification/test/integration/route.verify-legacy.test.ts b/apps/contract-verification/test/integration/route.verify-legacy.test.ts index 8c6f5e0b6..dcfe59f12 100644 --- a/apps/contract-verification/test/integration/route.verify-legacy.test.ts +++ b/apps/contract-verification/test/integration/route.verify-legacy.test.ts @@ -6,7 +6,9 @@ import { Hash, Hex } from 'ox' import { beforeEach, describe, expect, it, vi } from 'vitest' import * as DB from '#database/schema.ts' -import { chainIds } from '#wagmi.config.ts' +import { staticChains } from '#wagmi.config.ts' +import { ChainRegistry } from '#lib/chain-registry.ts' +import type { AppEnv } from '#index.tsx' const { mockCreatePublicClient, mockGetRandom } = vi.hoisted(() => ({ mockCreatePublicClient: vi.fn(), @@ -44,7 +46,7 @@ describe('POST /verify/vyper', () => { }) it('stores deployment metadata when creatorTxHash is provided', async () => { - const chainId = chainIds[0] + const chainId = staticChains[0].id const address = '0x1111111111111111111111111111111111111111' as const const creatorTxHash = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const @@ -94,7 +96,12 @@ describe('POST /verify/vyper', () => { }) const { legacyVerifyRoute } = await import('#route.verify-legacy.ts') - const app = new Hono<{ Bindings: Cloudflare.Env }>() + const app = new Hono() + const registry = ChainRegistry.fromStatic(staticChains) + app.use(async (c, next) => { + c.set('chainRegistry', registry) + await next() + }) app.route('/verify', legacyVerifyRoute) const response = await app.request( @@ -135,7 +142,7 @@ describe('POST /verify/vyper', () => { }) it('leaves deployment metadata null when creatorTxHash is not provided', async () => { - const chainId = chainIds[0] + const chainId = staticChains[0].id const address = '0x1111111111111111111111111111111111111111' as const const runtimeBytecode = createVyperBytecode('6000') const creationBytecode = createVyperBytecode('60016000') @@ -175,7 +182,12 @@ describe('POST /verify/vyper', () => { }) const { legacyVerifyRoute } = await import('#route.verify-legacy.ts') - const app = new Hono<{ Bindings: Cloudflare.Env }>() + const app = new Hono() + const registry2 = ChainRegistry.fromStatic(staticChains) + app.use(async (c, next) => { + c.set('chainRegistry', registry2) + await next() + }) app.route('/verify', legacyVerifyRoute) const response = await app.request( diff --git a/apps/contract-verification/test/unit/chain-registry.test.ts b/apps/contract-verification/test/unit/chain-registry.test.ts new file mode 100644 index 000000000..c46570711 --- /dev/null +++ b/apps/contract-verification/test/unit/chain-registry.test.ts @@ -0,0 +1,350 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +import { ChainRegistry } from '#lib/chain-registry.ts' +import { staticChains } from '#wagmi.config.ts' + +const FAKE_REGISTRY_URL = 'https://fake-registry.test/chains' + +function makeFakeResponse( + chains: Record< + string, + { + chainId: number + rpc: string[] + hidden?: boolean + name?: string + nativeCurrency?: { name: string; symbol: string; decimals: number } + explorers?: Array<{ name: string; url: string; standard?: string }> + } + >, +) { + return new Response(JSON.stringify(chains), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) +} + +describe('ChainRegistry', () => { + const originalFetch = globalThis.fetch + let mockFetch: ReturnType> + + beforeEach(() => { + mockFetch = vi.fn() + globalThis.fetch = mockFetch + }) + + afterEach(() => { + globalThis.fetch = originalFetch + }) + + describe('fromStatic', () => { + it('creates a registry with only static chains', () => { + const registry = ChainRegistry.fromStatic(staticChains) + + for (const chain of staticChains) { + expect(registry.isSupported(chain.id)).toBe(true) + expect(registry.getChain(chain.id)).toBeDefined() + expect(registry.isHidden(chain.id)).toBe(false) + } + + expect(registry.isSupported(999999)).toBe(false) + expect(registry.getChain(999999)).toBeUndefined() + }) + + it('returns sourcify chains for all static chains', () => { + const registry = ChainRegistry.fromStatic(staticChains) + const sourcify = registry.getSourcifyChains() + + expect(sourcify).toHaveLength(staticChains.length) + for (const chain of staticChains) { + expect(sourcify.some((s) => s.chainId === chain.id)).toBe(true) + } + }) + }) + + describe('fromUrl', () => { + it('fetches and merges dynamic chains with static chains', async () => { + mockFetch.mockResolvedValue( + makeFakeResponse({ + '42161': { + chainId: 42161, + rpc: ['https://arb1.arbitrum.io/rpc'], + name: 'Arbitrum One', + }, + }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + // static chains still present + for (const chain of staticChains) { + expect(registry.isSupported(chain.id)).toBe(true) + } + + // dynamic chain added + expect(registry.isSupported(42161)).toBe(true) + const chain = registry.getChain(42161) + expect(chain).toBeDefined() + expect(chain?.name).toBe('Arbitrum One') + expect(chain?.rpcUrls.default.http).toContain( + 'https://arb1.arbitrum.io/rpc', + ) + }) + + it('sends Authorization header when authToken is provided', async () => { + mockFetch.mockResolvedValue(makeFakeResponse({})) + + await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + authToken: 'test-secret-token', + staticChains, + }) + + expect(mockFetch).toHaveBeenCalledOnce() + const callHeaders = mockFetch.mock.calls[0]?.[1]?.headers as Headers + expect(callHeaders.get('Authorization')).toBe('Bearer test-secret-token') + }) + + it('does not send Authorization header when authToken is not provided', async () => { + mockFetch.mockResolvedValue(makeFakeResponse({})) + + await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + expect(mockFetch).toHaveBeenCalledOnce() + const callHeaders = mockFetch.mock.calls[0]?.[1]?.headers as Headers + expect(callHeaders.has('Authorization')).toBe(false) + }) + + it('hidden chains are functional but excluded from getSourcifyChains', async () => { + mockFetch.mockResolvedValue( + makeFakeResponse({ + '10': { + chainId: 10, + rpc: ['https://mainnet.optimism.io'], + name: 'Optimism', + hidden: true, + }, + '42161': { + chainId: 42161, + rpc: ['https://arb1.arbitrum.io/rpc'], + name: 'Arbitrum One', + hidden: false, + }, + }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + // both chains are functional + expect(registry.isSupported(10)).toBe(true) + expect(registry.isSupported(42161)).toBe(true) + expect(registry.getChain(10)).toBeDefined() + expect(registry.getChain(42161)).toBeDefined() + + // hidden flag + expect(registry.isHidden(10)).toBe(true) + expect(registry.isHidden(42161)).toBe(false) + + // sourcify chains exclude hidden + const sourcify = registry.getSourcifyChains() + expect(sourcify.some((s) => s.chainId === 10)).toBe(false) + expect(sourcify.some((s) => s.chainId === 42161)).toBe(true) + }) + + it('static chains take precedence over dynamic chains with same ID', async () => { + const staticChain = staticChains[0] + if (!staticChain) throw new Error('Expected at least one static chain') + mockFetch.mockResolvedValue( + makeFakeResponse({ + [String(staticChain.id)]: { + chainId: staticChain.id, + rpc: ['https://should-not-override.example.com'], + name: 'Override Attempt', + }, + }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + const chain = registry.getChain(staticChain.id) + expect(chain).toBeDefined() + // should keep the static chain's name, not the override + expect(chain?.name).toBe(staticChain.name) + expect(chain?.rpcUrls.default.http).not.toContain( + 'https://should-not-override.example.com', + ) + }) + + it('falls back to static-only registry on fetch failure', async () => { + mockFetch.mockResolvedValue( + new Response('Internal Server Error', { status: 500 }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + // static chains still work + for (const chain of staticChains) { + expect(registry.isSupported(chain.id)).toBe(true) + } + + // no dynamic chains + expect(registry.isSupported(42161)).toBe(false) + }) + + it('falls back to static-only registry on network error', async () => { + mockFetch.mockRejectedValue(new Error('Network error')) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + for (const chain of staticChains) { + expect(registry.isSupported(chain.id)).toBe(true) + } + expect(registry.isSupported(42161)).toBe(false) + }) + + it('rejects entries with invalid schema (missing rpc)', async () => { + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + '42161': { chainId: 42161 }, // missing rpc + '10': { + chainId: 10, + rpc: ['https://mainnet.optimism.io'], + }, + }), + { status: 200 }, + ), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + // entire response fails validation because of the invalid entry + // registry falls back to static only + for (const chain of staticChains) { + expect(registry.isSupported(chain.id)).toBe(true) + } + }) + + it('rejects entries with empty rpc array', async () => { + mockFetch.mockResolvedValue( + makeFakeResponse({ + '42161': { + chainId: 42161, + rpc: [], + }, + }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + // entire response fails Zod validation (rpc must have minLength 1) + for (const chain of staticChains) { + expect(registry.isSupported(chain.id)).toBe(true) + } + }) + + it('filters out websocket-only rpc entries', async () => { + mockFetch.mockResolvedValue( + makeFakeResponse({ + '42161': { + chainId: 42161, + rpc: ['wss://arb1.arbitrum.io/ws'], + }, + }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + // chain should not be added (no HTTP RPC URLs) + expect(registry.isSupported(42161)).toBe(false) + }) + + it('defaults name and nativeCurrency when not provided', async () => { + mockFetch.mockResolvedValue( + makeFakeResponse({ + '42161': { + chainId: 42161, + rpc: ['https://arb1.arbitrum.io/rpc'], + }, + }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + const chain = registry.getChain(42161) + expect(chain).toBeDefined() + expect(chain?.name).toBe('Chain 42161') + expect(chain?.nativeCurrency).toEqual({ + name: 'Ether', + symbol: 'ETH', + decimals: 18, + }) + }) + + it('includes block explorers in sourcify chains when provided', async () => { + mockFetch.mockResolvedValue( + makeFakeResponse({ + '42161': { + chainId: 42161, + rpc: ['https://arb1.arbitrum.io/rpc'], + name: 'Arbitrum One', + explorers: [ + { + name: 'Arbiscan', + url: 'https://arbiscan.io', + standard: 'EIP3091', + }, + ], + }, + }), + ) + + const registry = await ChainRegistry.fromUrl({ + url: FAKE_REGISTRY_URL, + staticChains, + }) + + const sourcify = registry.getSourcifyChains() + const arb = sourcify.find((s) => s.chainId === 42161) + expect(arb).toBeDefined() + expect(arb?._extra).toHaveProperty('blockExplorer') + }) + }) + + describe('isHidden', () => { + it('returns false for unknown chain IDs', () => { + const registry = ChainRegistry.fromStatic(staticChains) + expect(registry.isHidden(999999)).toBe(false) + }) + }) +}) diff --git a/apps/contract-verification/wrangler.json b/apps/contract-verification/wrangler.json index b5de1f4bc..cd8ca265c 100644 --- a/apps/contract-verification/wrangler.json +++ b/apps/contract-verification/wrangler.json @@ -9,7 +9,9 @@ "preview_urls": true, "logpush": true, "vars": { - "WHITELISTED_ORIGINS": "https://tempo.xyz,https://*.tempo.xyz,https://*.porto.workers.dev" + "WHITELISTED_ORIGINS": "https://tempo.xyz,https://*.tempo.xyz,https://*.porto.workers.dev", + "CHAINS_CONFIG_URL": "", + "CHAINS_CONFIG_AUTH_TOKEN": "" }, "ratelimits": [ { From 5a0db6f5b03573af09e18d9d87c46083db3c819f Mon Sep 17 00:00:00 2001 From: Jeff Reiner <8116716+mirshko@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:56:57 +0200 Subject: [PATCH 2/7] fix(contract-verification): make /chains response Sourcify-compliant Replace non-standard _extra field with traceSupportedRPCs (required by Sourcify spec). Make title optional per spec. --- apps/contract-verification/src/lib/chain-registry.ts | 10 +++------- .../test/unit/chain-registry.test.ts | 12 ++++++++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/contract-verification/src/lib/chain-registry.ts b/apps/contract-verification/src/lib/chain-registry.ts index 23766f805..caec7c62d 100644 --- a/apps/contract-verification/src/lib/chain-registry.ts +++ b/apps/contract-verification/src/lib/chain-registry.ts @@ -48,12 +48,12 @@ type ChainEntry = { export type SourcifyChain = { name: string - title: string + title?: string | undefined chainId: number rpc: string[] + traceSupportedRPCs: Array<{ type?: string; index?: number }> supported: boolean etherscanAPI: boolean - _extra: Record } // --------------------------------------------------------------------------- @@ -199,18 +199,14 @@ export class ChainRegistry { const result: SourcifyChain[] = [] for (const { chain, hidden } of this.entries.values()) { if (hidden) continue - const extra: Record = {} - if (chain.blockExplorers) { - extra.blockExplorer = chain.blockExplorers.default - } result.push({ name: chain.name, title: chain.name, chainId: chain.id, rpc: [...chain.rpcUrls.default.http], + traceSupportedRPCs: [], supported: true, etherscanAPI: false, - _extra: extra, }) } return result diff --git a/apps/contract-verification/test/unit/chain-registry.test.ts b/apps/contract-verification/test/unit/chain-registry.test.ts index c46570711..d048bd78a 100644 --- a/apps/contract-verification/test/unit/chain-registry.test.ts +++ b/apps/contract-verification/test/unit/chain-registry.test.ts @@ -311,7 +311,7 @@ describe('ChainRegistry', () => { }) }) - it('includes block explorers in sourcify chains when provided', async () => { + it('returns sourcify-compliant shape for dynamic chains', async () => { mockFetch.mockResolvedValue( makeFakeResponse({ '42161': { @@ -337,7 +337,15 @@ describe('ChainRegistry', () => { const sourcify = registry.getSourcifyChains() const arb = sourcify.find((s) => s.chainId === 42161) expect(arb).toBeDefined() - expect(arb?._extra).toHaveProperty('blockExplorer') + expect(arb).toMatchObject({ + name: 'Arbitrum One', + title: 'Arbitrum One', + chainId: 42161, + rpc: ['https://arb1.arbitrum.io/rpc'], + traceSupportedRPCs: [], + supported: true, + etherscanAPI: false, + }) }) }) From 649cf2e9c1b2800ec6570d6ee9332afe55fd4a4e Mon Sep 17 00:00:00 2001 From: Jeff Reiner <8116716+mirshko@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:25:31 +0200 Subject: [PATCH 3/7] refactor(contract-verification): derive SourcifyChain type from Zod schema --- .../src/lib/chain-registry.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/contract-verification/src/lib/chain-registry.ts b/apps/contract-verification/src/lib/chain-registry.ts index caec7c62d..135f0c786 100644 --- a/apps/contract-verification/src/lib/chain-registry.ts +++ b/apps/contract-verification/src/lib/chain-registry.ts @@ -46,15 +46,22 @@ type ChainEntry = { hidden: boolean } -export type SourcifyChain = { - name: string - title?: string | undefined - chainId: number - rpc: string[] - traceSupportedRPCs: Array<{ type?: string; index?: number }> - supported: boolean - etherscanAPI: boolean -} +export const zSourcifyChain = z.object({ + name: z.string(), + title: z.optional(z.string()), + chainId: z.number(), + rpc: z.array(z.string()), + traceSupportedRPCs: z.array( + z.object({ + type: z.optional(z.string()), + index: z.optional(z.number()), + }), + ), + supported: z.boolean(), + etherscanAPI: z.boolean(), +}) + +export type SourcifyChain = z.infer // --------------------------------------------------------------------------- // ChainRegistry From 6c56eae2e36bb3d9b61756c0f59101f69d531d43 Mon Sep 17 00:00:00 2001 From: Jeff Reiner <8116716+mirshko@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:14:46 +0200 Subject: [PATCH 4/7] feat(contract-verification): use Cloudflare Secrets Store for chain registry auth token Add secrets_store_secrets binding for CHAINS_CONFIG_AUTH_TOKEN_SECRET with env var CHAINS_CONFIG_AUTH_TOKEN as fallback. Token is resolved at startup via resolveAuthToken() which checks type: SecretsStoreSecret (.get()) > string > undefined. --- apps/contract-verification/src/index.tsx | 10 +++++-- .../src/lib/chain-registry.ts | 26 +++++++++++++------ apps/contract-verification/wrangler.json | 7 +++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/apps/contract-verification/src/index.tsx b/apps/contract-verification/src/index.tsx index f276698a6..4a7216bfa 100644 --- a/apps/contract-verification/src/index.tsx +++ b/apps/contract-verification/src/index.tsx @@ -17,7 +17,11 @@ import { staticChains } from '#wagmi.config.ts' import { VerificationContainer } from '#container.ts' import { VerificationJobRunner } from '#job-runner.ts' import { legacyVerifyRoute } from '#route.verify-legacy.ts' -import { type ChainRegistry, chainRegistry } from '#lib/chain-registry.ts' +import { + type ChainRegistry, + chainRegistry, + resolveAuthToken, +} from '#lib/chain-registry.ts' import { configureLogger, getLogger, withContext } from '#lib/logger.ts' import { lookupAllChainContractsRoute, lookupRoute } from '#route.lookup.ts' import { handleError, originMatches, sourcifyError } from '#lib/utilities.ts' @@ -77,7 +81,9 @@ app.use( chainRegistry({ staticChains, url: env.CHAINS_CONFIG_URL || undefined, - authToken: env.CHAINS_CONFIG_AUTH_TOKEN || undefined, + authToken: await resolveAuthToken( + env.CHAINS_CONFIG_AUTH_TOKEN_SECRET || env.CHAINS_CONFIG_AUTH_TOKEN, + ), }), ) diff --git a/apps/contract-verification/src/lib/chain-registry.ts b/apps/contract-verification/src/lib/chain-registry.ts index 135f0c786..790176fb2 100644 --- a/apps/contract-verification/src/lib/chain-registry.ts +++ b/apps/contract-verification/src/lib/chain-registry.ts @@ -224,17 +224,27 @@ export class ChainRegistry { // Hono middleware factory // --------------------------------------------------------------------------- +/** + * Resolves an auth token from a SecretsStoreSecret binding, a plain string, + * or undefined. Secrets Store is tried first, falling back to string. + */ +export async function resolveAuthToken( + token: { get(): Promise } | string | undefined, +): Promise { + if (typeof token === 'string') return token || undefined + if (token && typeof token.get === 'function') { + try { + return (await token.get()) || undefined + } catch { + // Secrets Store binding may not be configured (e.g. in tests) + } + } + return undefined +} + /** * Creates a Hono middleware that initializes a `ChainRegistry` and sets it on * the context. - * - * ```ts - * app.use(chainRegistry({ - * staticChains, - * url: context.env.CHAINS_CONFIG_URL, - * authToken: context.env.CHAINS_CONFIG_AUTH_TOKEN, - * })) - * ``` */ export function chainRegistry(options: { staticChains: readonly Chain[] diff --git a/apps/contract-verification/wrangler.json b/apps/contract-verification/wrangler.json index cd8ca265c..1d95b066a 100644 --- a/apps/contract-verification/wrangler.json +++ b/apps/contract-verification/wrangler.json @@ -13,6 +13,13 @@ "CHAINS_CONFIG_URL": "", "CHAINS_CONFIG_AUTH_TOKEN": "" }, + "secrets_store_secrets": [ + { + "binding": "CHAINS_CONFIG_AUTH_TOKEN_SECRET", + "store_id": "", + "secret_name": "CHAINS_CONFIG_AUTH_TOKEN" + } + ], "ratelimits": [ { "name": "RATE_LIMITER", From 647ce64811bb697906a8579f5ab4a5f661bdef2e Mon Sep 17 00:00:00 2001 From: Jeff Reiner <8116716+mirshko@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:52:19 +0200 Subject: [PATCH 5/7] fix(contract-verification): integrate chain registry with upstream changes Adapt dynamic chain registry to work with upstream Durable Object job runner and native precompile support added to main since the branch diverged. --- apps/contract-verification/package.json | 1 - .../scripts/precompile-seed/manifest.ts | 5 +- apps/contract-verification/src/job-runner.ts | 12 + .../contract-verification/src/wagmi.config.ts | 6 +- apps/tokenlist/data/4217/tokenlist.json | 592 +++++++++--------- 5 files changed, 317 insertions(+), 299 deletions(-) diff --git a/apps/contract-verification/package.json b/apps/contract-verification/package.json index 1af60eb51..135d592b6 100644 --- a/apps/contract-verification/package.json +++ b/apps/contract-verification/package.json @@ -28,7 +28,6 @@ "db:seed:local": "pnpm db:prepare:local && pnpm db:seed", "db:seed:remote": "pnpm db:prepare:remote && pnpm db:seed", "gen:types": "wrangler types --env-interface='CloudflareBindings' --env-file='.env.example'" - }, "dependencies": { "@cloudflare/containers": "^0.2.3", diff --git a/apps/contract-verification/scripts/precompile-seed/manifest.ts b/apps/contract-verification/scripts/precompile-seed/manifest.ts index 5295ccda7..ad1c15491 100644 --- a/apps/contract-verification/scripts/precompile-seed/manifest.ts +++ b/apps/contract-verification/scripts/precompile-seed/manifest.ts @@ -1,7 +1,10 @@ import type { Abi } from 'viem' import { Abis, Addresses } from 'viem/tempo' -import { chainIds, type ChainId } from '#wagmi.config.ts' +import { staticChains } from '#wagmi.config.ts' + +const chainIds = staticChains.map((c) => c.id) +type ChainId = (typeof chainIds)[number] export type NativeContractRuntimeType = | 'precompile' diff --git a/apps/contract-verification/src/job-runner.ts b/apps/contract-verification/src/job-runner.ts index 3e83fef7b..ebd40ec8e 100644 --- a/apps/contract-verification/src/job-runner.ts +++ b/apps/contract-verification/src/job-runner.ts @@ -3,6 +3,8 @@ import { DurableObject } from 'cloudflare:workers' import { getLogger } from '#lib/logger.ts' import { formatError } from '#lib/utilities.ts' import { runVerificationJob } from '#route.verify.ts' +import { staticChains } from '#wagmi.config.ts' +import { ChainRegistry, resolveAuthToken } from '#lib/chain-registry.ts' const logger = getLogger(['tempo', 'job-runner']) @@ -48,12 +50,22 @@ export class VerificationJobRunner extends DurableObject { }) try { + const authToken = await resolveAuthToken( + this.env.CHAINS_CONFIG_AUTH_TOKEN_SECRET || + this.env.CHAINS_CONFIG_AUTH_TOKEN, + ) + const url = this.env.CHAINS_CONFIG_URL || undefined + const registry = url + ? await ChainRegistry.fromUrl({ url, authToken, staticChains }) + : ChainRegistry.fromStatic(staticChains) + await runVerificationJob( this.env, job.jobId, job.chainId, job.address, job.body, + registry, ) logger.info('job_finished', { jobId: job.jobId }) } catch (error) { diff --git a/apps/contract-verification/src/wagmi.config.ts b/apps/contract-verification/src/wagmi.config.ts index 622334853..34e7a0a48 100644 --- a/apps/contract-verification/src/wagmi.config.ts +++ b/apps/contract-verification/src/wagmi.config.ts @@ -1,6 +1,10 @@ import { Address } from 'ox' import * as z from 'zod/mini' -import { tempoDevnet, tempoMainnet, tempoTestnet } from '@wagmi/core/chains' +import { + tempoDevnet, + tempo as tempoMainnet, + tempoModerato as tempoTestnet, +} from '@wagmi/core/chains' const verifierUrl = import.meta.env?.VITE_VERIFIER_URL ?? 'https://contracts.tempo.xyz' diff --git a/apps/tokenlist/data/4217/tokenlist.json b/apps/tokenlist/data/4217/tokenlist.json index 4c78a8bcc..4727a0e52 100644 --- a/apps/tokenlist/data/4217/tokenlist.json +++ b/apps/tokenlist/data/4217/tokenlist.json @@ -1,298 +1,298 @@ { - "$schema": "https://esm.sh/gh/uniswap/token-lists/src/tokenlist.schema.json", - "name": "Tempo Mainnet (Presto)", - "logoURI": "https://esm.sh/gh/tempoxyz/tokenlist/data/4217/icon.svg", - "timestamp": "2026-04-14T17:12:36Z", - "version": { - "major": 1, - "minor": 2, - "patch": 18 - }, - "tokens": [ - { - "name": "PathUSD", - "symbol": "pathUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000000000000000000000", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000000000000000000.svg", - "extensions": { - "chain": "tempo", - "label": "PathUSD", - "coingeckoId": "pathusd" - } - }, - { - "name": "Bridged USDC (Stargate)", - "symbol": "USDC.e", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000b9537d11c60e8b50", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg", - "extensions": { - "chain": "tempo", - "label": "USDC.e", - "coingeckoId": "usd-coin", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - } - } - }, - { - "name": "Bridged EURC (Stargate)", - "symbol": "EURC.e", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000001621e21f71cf12fb", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg", - "extensions": { - "chain": "tempo", - "label": "EURC", - "coingeckoId": "euro-coin", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c" - } - } - }, - { - "name": "USDT0", - "symbol": "USDT0", - "decimals": 6, - "chainId": 4217, - "address": "0x20c00000000000000000000014f22ca97301eb73", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg", - "extensions": { - "chain": "tempo", - "coingeckoId": "usdt0", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" - } - } - }, - { - "name": "Frax USD", - "symbol": "frxUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000003554d28269e0f3c2", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg", - "extensions": { - "chain": "tempo", - "coingeckoId": "frax-usd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0xcacd6fd266af91b8aed52accc382b4e165586e29" - } - } - }, - { - "name": "Cap USD", - "symbol": "cUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000000520792dcccccccc", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg", - "extensions": { - "chain": "tempo", - "label": "cUSD", - "coingeckoId": "cap-usd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0xcccc62962d17b8914c62d74ffb843d73b2a3cccc" - } - }, - "dateAdded": "2026-03-11T04:44:38Z" - }, - { - "name": "Staked Cap USD", - "symbol": "stcUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000008ee4fcff88888888", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg", - "extensions": { - "chain": "tempo", - "label": "stcUSD", - "coingeckoId": "staked-cap-usd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0x88887be419578051ff9f4eb6c858a951921d8888" - } - }, - "dateAdded": "2026-03-16T02:56:16Z" - }, - { - "name": "Generic USD", - "symbol": "GUSD", - "address": "0x20c0000000000000000000005c0bac7cef389a11", - "decimals": 6, - "chainId": 4217, - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg", - "extensions": { - "chain": "tempo", - "label": "GUSD", - "coingeckoId": "generic-usd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0xece811d35f79c4868a2b911e55d9aa0821399edf" - } - }, - "dateAdded": "2026-03-21T05:24:26Z" - }, - { - "name": "Reservoir Stablecoin", - "symbol": "rUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000007f7ba549dd0251b9", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg", - "extensions": { - "chain": "tempo", - "label": "rUSD", - "coingeckoId": "reservoir-rusd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0x09d4214c03d01f49544c0448dbe3a27f768f2b34" - } - }, - "dateAdded": "2026-04-02T18:36:11Z" - }, - { - "name": "Wrapped Savings rUSD", - "symbol": "wsrUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000aeed2ec36a54d0e5", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg", - "extensions": { - "chain": "tempo", - "label": "wsrUSD", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0xd3fd63209fa2d55b07a0f6db36c2f43900be3094" - } - }, - "dateAdded": "2026-04-02T18:36:11Z" - }, - { - "name": "AllUnity EUR", - "symbol": "EURAU", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000009a4a4b17e0dc6651", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg", - "extensions": { - "chain": "tempo", - "label": "EURAU", - "coingeckoId": "allunity-eur" - }, - "dateAdded": "2026-04-02T18:36:11Z" - }, - { - "name": "Re Protocol reUSD", - "symbol": "reUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000383a23bacb546ab9", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg", - "extensions": { - "chain": "tempo", - "label": "reUSD", - "coingeckoId": "re-protocol-reusd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0x5086bf358635b81d8c47c66d1c8b9e567db70c72" - } - }, - "dateAdded": "2026-04-06T23:13:41Z" - }, - { - "name": "InfiniFi USD", - "symbol": "iUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000ab02d39df30bd17e", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg", - "extensions": { - "chain": "tempo", - "label": "iUSD", - "coingeckoId": "infinifi-usd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0x48f9e38f3070ad8945dfeae3fa70987722e3d89c" - } - }, - "dateAdded": "2026-04-14T04:51:53Z" - }, - { - "name": "InfiniFi Staked USD", - "symbol": "siUSD", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000048c8f36df1c9a4a", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg", - "extensions": { - "chain": "tempo", - "label": "siUSD", - "coingeckoId": "infinifi-staked-iusd", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0xdbdc1ef57537e34680b898e1febd3d68c7389bcb" - } - }, - "dateAdded": "2026-04-14T04:51:53Z" - }, - { - "name": "USDe", - "symbol": "USDe", - "decimals": 6, - "chainId": 4217, - "address": "0x20c0000000000000000000002f52d5cc21a3207b", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg", - "extensions": { - "chain": "tempo", - "label": "USDe", - "coingeckoId": "ethena-usde", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0x4c9edd5852cd905f086c759e8383e09bff1e68b3" - } - }, - "dateAdded": "2026-04-14T04:51:53Z" - }, - { - "name": "Staked USDe", - "symbol": "sUSDe", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000bd95bfb69fbe6ce3", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg", - "extensions": { - "chain": "tempo", - "coingeckoId": "ethena-staked-usde", - "bridgeInfo": { - "sourceChainId": 1, - "sourceAddress": "0x9d39a5de30e57443bff2a8307a4256c8797a3497" - } - }, - "dateAdded": "2026-04-14T04:51:53Z" - }, - { - "name": "Stable Coin", - "symbol": "SBC", - "decimals": 6, - "chainId": 4217, - "address": "0x20c000000000000000000000ae247a1130450f09", - "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg", - "extensions": { - "chain": "tempo", - "label": "SBC", - "coingeckoId": "stable-coin-2" - }, - "dateAdded": "2026-04-14T17:12:36Z" - } - ] + "$schema": "https://esm.sh/gh/uniswap/token-lists/src/tokenlist.schema.json", + "name": "Tempo Mainnet (Presto)", + "logoURI": "https://esm.sh/gh/tempoxyz/tokenlist/data/4217/icon.svg", + "timestamp": "2026-04-14T17:12:36Z", + "version": { + "major": 1, + "minor": 2, + "patch": 18 + }, + "tokens": [ + { + "name": "PathUSD", + "symbol": "pathUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000000000000000000000", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000000000000000000.svg", + "extensions": { + "chain": "tempo", + "label": "PathUSD", + "coingeckoId": "pathusd" + } + }, + { + "name": "Bridged USDC (Stargate)", + "symbol": "USDC.e", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000b9537d11c60e8b50", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg", + "extensions": { + "chain": "tempo", + "label": "USDC.e", + "coingeckoId": "usd-coin", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + } + } + }, + { + "name": "Bridged EURC (Stargate)", + "symbol": "EURC.e", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000001621e21f71cf12fb", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg", + "extensions": { + "chain": "tempo", + "label": "EURC", + "coingeckoId": "euro-coin", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c" + } + } + }, + { + "name": "USDT0", + "symbol": "USDT0", + "decimals": 6, + "chainId": 4217, + "address": "0x20c00000000000000000000014f22ca97301eb73", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg", + "extensions": { + "chain": "tempo", + "coingeckoId": "usdt0", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + } + }, + { + "name": "Frax USD", + "symbol": "frxUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000003554d28269e0f3c2", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg", + "extensions": { + "chain": "tempo", + "coingeckoId": "frax-usd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0xcacd6fd266af91b8aed52accc382b4e165586e29" + } + } + }, + { + "name": "Cap USD", + "symbol": "cUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000000520792dcccccccc", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg", + "extensions": { + "chain": "tempo", + "label": "cUSD", + "coingeckoId": "cap-usd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0xcccc62962d17b8914c62d74ffb843d73b2a3cccc" + } + }, + "dateAdded": "2026-03-11T04:44:38Z" + }, + { + "name": "Staked Cap USD", + "symbol": "stcUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000008ee4fcff88888888", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg", + "extensions": { + "chain": "tempo", + "label": "stcUSD", + "coingeckoId": "staked-cap-usd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0x88887be419578051ff9f4eb6c858a951921d8888" + } + }, + "dateAdded": "2026-03-16T02:56:16Z" + }, + { + "name": "Generic USD", + "symbol": "GUSD", + "address": "0x20c0000000000000000000005c0bac7cef389a11", + "decimals": 6, + "chainId": 4217, + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg", + "extensions": { + "chain": "tempo", + "label": "GUSD", + "coingeckoId": "generic-usd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0xece811d35f79c4868a2b911e55d9aa0821399edf" + } + }, + "dateAdded": "2026-03-21T05:24:26Z" + }, + { + "name": "Reservoir Stablecoin", + "symbol": "rUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000007f7ba549dd0251b9", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg", + "extensions": { + "chain": "tempo", + "label": "rUSD", + "coingeckoId": "reservoir-rusd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0x09d4214c03d01f49544c0448dbe3a27f768f2b34" + } + }, + "dateAdded": "2026-04-02T18:36:11Z" + }, + { + "name": "Wrapped Savings rUSD", + "symbol": "wsrUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000aeed2ec36a54d0e5", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg", + "extensions": { + "chain": "tempo", + "label": "wsrUSD", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0xd3fd63209fa2d55b07a0f6db36c2f43900be3094" + } + }, + "dateAdded": "2026-04-02T18:36:11Z" + }, + { + "name": "AllUnity EUR", + "symbol": "EURAU", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000009a4a4b17e0dc6651", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg", + "extensions": { + "chain": "tempo", + "label": "EURAU", + "coingeckoId": "allunity-eur" + }, + "dateAdded": "2026-04-02T18:36:11Z" + }, + { + "name": "Re Protocol reUSD", + "symbol": "reUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000383a23bacb546ab9", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg", + "extensions": { + "chain": "tempo", + "label": "reUSD", + "coingeckoId": "re-protocol-reusd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0x5086bf358635b81d8c47c66d1c8b9e567db70c72" + } + }, + "dateAdded": "2026-04-06T23:13:41Z" + }, + { + "name": "InfiniFi USD", + "symbol": "iUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000ab02d39df30bd17e", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg", + "extensions": { + "chain": "tempo", + "label": "iUSD", + "coingeckoId": "infinifi-usd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0x48f9e38f3070ad8945dfeae3fa70987722e3d89c" + } + }, + "dateAdded": "2026-04-14T04:51:53Z" + }, + { + "name": "InfiniFi Staked USD", + "symbol": "siUSD", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000048c8f36df1c9a4a", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg", + "extensions": { + "chain": "tempo", + "label": "siUSD", + "coingeckoId": "infinifi-staked-iusd", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0xdbdc1ef57537e34680b898e1febd3d68c7389bcb" + } + }, + "dateAdded": "2026-04-14T04:51:53Z" + }, + { + "name": "USDe", + "symbol": "USDe", + "decimals": 6, + "chainId": 4217, + "address": "0x20c0000000000000000000002f52d5cc21a3207b", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg", + "extensions": { + "chain": "tempo", + "label": "USDe", + "coingeckoId": "ethena-usde", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0x4c9edd5852cd905f086c759e8383e09bff1e68b3" + } + }, + "dateAdded": "2026-04-14T04:51:53Z" + }, + { + "name": "Staked USDe", + "symbol": "sUSDe", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000bd95bfb69fbe6ce3", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg", + "extensions": { + "chain": "tempo", + "coingeckoId": "ethena-staked-usde", + "bridgeInfo": { + "sourceChainId": 1, + "sourceAddress": "0x9d39a5de30e57443bff2a8307a4256c8797a3497" + } + }, + "dateAdded": "2026-04-14T04:51:53Z" + }, + { + "name": "Stable Coin", + "symbol": "SBC", + "decimals": 6, + "chainId": 4217, + "address": "0x20c000000000000000000000ae247a1130450f09", + "logoURI": "https://esm.sh/gh/tempoxyz/tempo-apps/apps/tokenlist/data/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg", + "extensions": { + "chain": "tempo", + "label": "SBC", + "coingeckoId": "stable-coin-2" + }, + "dateAdded": "2026-04-14T17:12:36Z" + } + ] } From 4a700dd0ce09c226f38d5548d36cce54768ed226 Mon Sep 17 00:00:00 2001 From: Jeff Reiner <8116716+mirshko@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:08:15 +0200 Subject: [PATCH 6/7] . --- biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.json b/biome.json index b9ed5f832..735c0ad32 100644 --- a/biome.json +++ b/biome.json @@ -116,7 +116,7 @@ }, { "includes": [ - "**/wrangler.json", + "**/wrangler.json", "**/biome.json", "**/.zed/*.json", "**/tsconfig.json", From e2142478d5d351b5ecf69f8a30bae46f8a636dbb Mon Sep 17 00:00:00 2001 From: Jeff Reiner <8116716+mirshko@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:08:59 +0200 Subject: [PATCH 7/7] . --- apps/contract-verification/test/e2e/verification.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/contract-verification/test/e2e/verification.test.ts b/apps/contract-verification/test/e2e/verification.test.ts index 21540024d..56138bac8 100644 --- a/apps/contract-verification/test/e2e/verification.test.ts +++ b/apps/contract-verification/test/e2e/verification.test.ts @@ -70,7 +70,7 @@ describe('full verification flow', () => { getCode: async () => counterFixture.onchainRuntimeBytecode, }), getContainer: () => ({ - fetch: async (request: Request) => { + fetch: async (request) => { const url = new URL(request.url) if (request.method === 'POST' && url.pathname === '/compile') { return Response.json(counterFixture.solcOutput, { status: 200 })