From 22f58456e11ecf4ba11f0a66aa64655256f865bf Mon Sep 17 00:00:00 2001 From: Augusto Lemble Date: Tue, 21 Apr 2026 10:18:27 -0300 Subject: [PATCH 1/2] feat(networks): add 5 EVM testnets from metadata v1.2.1-alpha.0 Bumps @openscan/metadata to 1.2.1-alpha.0 and registers Arbitrum Sepolia (421614), Optimism Sepolia (11155420), Base Sepolia (84532), Polygon Amoy (80002), and Avalanche Fuji (43113). Each new chain ID is mapped to its L1 family adapter (Arbitrum, OP, Base, Polygon, EVM) in AdapterFactory. Since @openscan/network-connectors does not yet register these testnet chain IDs in its ClientFactory, DataService instantiates the L1 family client directly for them. The L2 adapter constructors and AppChainId are widened to accept the new testnet chain IDs. --- src/config/networks.json | 120 ++++++++++++++++++ src/services/DataService.ts | 30 ++++- src/services/MetadataService.ts | 2 +- .../ArbitrumAdapter/ArbitrumAdapter.ts | 2 +- .../adapters/BaseAdapter/BaseAdapter.ts | 2 +- .../OptimismAdapter/OptimismAdapter.ts | 2 +- .../adapters/PolygonAdapter/PolygonAdapter.ts | 2 +- src/services/adapters/adaptersFactory.ts | 10 +- src/types/index.ts | 6 +- 9 files changed, 161 insertions(+), 15 deletions(-) diff --git a/src/config/networks.json b/src/config/networks.json index eb08c278..98c90881 100644 --- a/src/config/networks.json +++ b/src/config/networks.json @@ -285,6 +285,126 @@ } ] }, + { + "type": "evm", + "networkId": "eip155:421614", + "slug": "arb-sepolia", + "name": "Arbitrum Sepolia", + "shortName": "Arb Sepolia", + "description": "Arbitrum testnet for developers", + "currency": "ETH", + "color": "#28A0F0", + "isTestnet": true, + "logo": "assets/networks/421614.svg", + "links": [ + { + "name": "Bridge", + "url": "https://bridge.arbitrum.io", + "description": "Bridge from Sepolia" + }, + { + "name": "Docs", + "url": "https://docs.arbitrum.io", + "description": "Developer documentation" + } + ] + }, + { + "type": "evm", + "networkId": "eip155:11155420", + "slug": "op-sepolia", + "name": "Optimism Sepolia", + "shortName": "OP Sepolia", + "description": "Optimism testnet for developers", + "currency": "ETH", + "color": "#FF0420", + "isTestnet": true, + "logo": "assets/networks/11155420.svg", + "links": [ + { + "name": "Bridge", + "url": "https://app.optimism.io/bridge", + "description": "Bridge from Sepolia" + }, + { + "name": "Docs", + "url": "https://docs.optimism.io", + "description": "Developer documentation" + } + ] + }, + { + "type": "evm", + "networkId": "eip155:84532", + "slug": "base-sepolia", + "name": "Base Sepolia", + "shortName": "Base Sepolia", + "description": "Base testnet for developers", + "currency": "ETH", + "color": "#0052FF", + "isTestnet": true, + "logo": "assets/networks/84532.svg", + "links": [ + { + "name": "Faucet", + "url": "https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet", + "description": "Get testnet ETH" + }, + { + "name": "Docs", + "url": "https://docs.base.org", + "description": "Developer documentation" + } + ] + }, + { + "type": "evm", + "networkId": "eip155:80002", + "slug": "polygon-amoy", + "name": "Polygon Amoy", + "shortName": "Amoy", + "description": "Polygon testnet for developers", + "currency": "POL", + "color": "#8247E5", + "isTestnet": true, + "logo": "assets/networks/80002.svg", + "links": [ + { + "name": "Faucet", + "url": "https://faucet.polygon.technology", + "description": "Get testnet POL" + }, + { + "name": "Docs", + "url": "https://docs.polygon.technology", + "description": "Developer documentation" + } + ] + }, + { + "type": "evm", + "networkId": "eip155:43113", + "slug": "avax-fuji", + "name": "Avalanche Fuji", + "shortName": "Fuji", + "description": "Avalanche testnet for developers", + "currency": "AVAX", + "color": "#E84142", + "isTestnet": true, + "logo": "assets/networks/43113.svg", + "links": [ + { + "name": "Faucet", + "url": "https://faucet.avax.network", + "description": "Get testnet AVAX" + }, + { + "name": "Docs", + "url": "https://docs.avax.network", + "description": "Developer documentation" + } + ] + }, { "type": "solana", "networkId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", diff --git a/src/services/DataService.ts b/src/services/DataService.ts index 29c26b5a..45925a7c 100644 --- a/src/services/DataService.ts +++ b/src/services/DataService.ts @@ -1,8 +1,13 @@ import { type SupportedChainId, type SupportedSolanaChainId, - ClientFactory, + ArbitrumClient, + BaseClient, BitcoinClient, + ClientFactory, + EthereumClient, + OptimismClient, + PolygonClient, } from "@openscan/network-connectors"; import { AdapterFactory } from "./adapters/adaptersFactory"; @@ -13,6 +18,21 @@ import type { NetworkConfig, RpcUrlsContextType } from "../types"; import { getRPCUrls } from "../config/rpcConfig"; import { getNetworkRpcKey, getChainIdFromNetwork } from "../utils/networkResolver"; +type EVMClientConfig = { + rpcUrls: string[]; + type: "fallback" | "parallel" | "race"; +}; + +// EVM testnets not yet registered in @openscan/network-connectors ClientFactory. +// Mapped to their L1 family's client since they share the same JSON-RPC surface. +const EVM_TESTNET_CLIENTS: Record unknown> = { + 421614: (config) => new ArbitrumClient(config), + 11155420: (config) => new OptimismClient(config), + 84532: (config) => new BaseClient(config), + 80002: (config) => new PolygonClient(config), + 43113: (config) => new EthereumClient(config), +}; + /** * DataService supports EVM, Bitcoin, and Solana networks * The adapter type varies based on network type @@ -58,10 +78,10 @@ export class DataService { } else { // Create EVM client and adapter const chainId = getChainIdFromNetwork(network) as SupportedChainId; - const networkClient = ClientFactory.createTypedClient(chainId, { - rpcUrls, - type: strategy, - }); + const clientConfig = { rpcUrls, type: strategy }; + const networkClient = + EVM_TESTNET_CLIENTS[chainId as number]?.(clientConfig) ?? + ClientFactory.createTypedClient(chainId, clientConfig); this.networkAdapter = AdapterFactory.createAdapter(chainId, networkClient); } } diff --git a/src/services/MetadataService.ts b/src/services/MetadataService.ts index ecd6e8d0..85660515 100644 --- a/src/services/MetadataService.ts +++ b/src/services/MetadataService.ts @@ -7,7 +7,7 @@ import networksData from "../config/networks.json"; import { logger } from "../utils/logger"; import { extractChainIdFromNetworkId } from "../utils/networkResolver"; -export const METADATA_VERSION = "1.2.0-alpha.0"; +export const METADATA_VERSION = "1.2.1-alpha.0"; const METADATA_BASE_URL = `https://cdn.jsdelivr.net/npm/@openscan/metadata@${METADATA_VERSION}/dist`; export interface NetworkLink { diff --git a/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts b/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts index 78b65827..b037da64 100644 --- a/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts +++ b/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts @@ -23,7 +23,7 @@ import type { ArbitrumClient, EthereumClient } from "@openscan/network-connector export class ArbitrumAdapter extends NetworkAdapter { private client: ArbitrumClient; - constructor(networkId: 42161, client: ArbitrumClient) { + constructor(networkId: 42161 | 421614, client: ArbitrumClient) { super(networkId); this.client = client; this.initTxSearch(client as unknown as EthereumClient); diff --git a/src/services/adapters/BaseAdapter/BaseAdapter.ts b/src/services/adapters/BaseAdapter/BaseAdapter.ts index dd23c7f0..60939d41 100644 --- a/src/services/adapters/BaseAdapter/BaseAdapter.ts +++ b/src/services/adapters/BaseAdapter/BaseAdapter.ts @@ -22,7 +22,7 @@ import type { BaseClient, EthereumClient } from "@openscan/network-connectors"; export class BaseAdapter extends NetworkAdapter { private client: BaseClient; - constructor(networkId: 8453, client: BaseClient) { + constructor(networkId: 8453 | 84532, client: BaseClient) { super(networkId); this.client = client; this.initTxSearch(client as unknown as EthereumClient); diff --git a/src/services/adapters/OptimismAdapter/OptimismAdapter.ts b/src/services/adapters/OptimismAdapter/OptimismAdapter.ts index cb1935e4..fd629682 100644 --- a/src/services/adapters/OptimismAdapter/OptimismAdapter.ts +++ b/src/services/adapters/OptimismAdapter/OptimismAdapter.ts @@ -22,7 +22,7 @@ import type { OptimismClient, EthereumClient } from "@openscan/network-connector export class OptimismAdapter extends NetworkAdapter { private client: OptimismClient; - constructor(networkId: 10, client: OptimismClient) { + constructor(networkId: 10 | 11155420, client: OptimismClient) { super(networkId); this.client = client; this.initTxSearch(client as unknown as EthereumClient); diff --git a/src/services/adapters/PolygonAdapter/PolygonAdapter.ts b/src/services/adapters/PolygonAdapter/PolygonAdapter.ts index eaf106eb..5848cb13 100644 --- a/src/services/adapters/PolygonAdapter/PolygonAdapter.ts +++ b/src/services/adapters/PolygonAdapter/PolygonAdapter.ts @@ -21,7 +21,7 @@ import type { PolygonClient, SupportedChainId, EthereumClient } from "@openscan/ export class PolygonAdapter extends NetworkAdapter { private client: PolygonClient; - constructor(networkId: SupportedChainId, client: PolygonClient) { + constructor(networkId: SupportedChainId | 80002, client: PolygonClient) { super(networkId); this.client = client; this.initTxSearch(client as unknown as EthereumClient); diff --git a/src/services/adapters/adaptersFactory.ts b/src/services/adapters/adaptersFactory.ts index d59f5737..8bf206e5 100644 --- a/src/services/adapters/adaptersFactory.ts +++ b/src/services/adapters/adaptersFactory.ts @@ -29,7 +29,7 @@ export class AdapterFactory { * Create an EVM network adapter */ static createAdapter( - networkId: SupportedChainId, + networkId: SupportedChainId | number, client: | EthereumClient | OptimismClient @@ -39,25 +39,31 @@ export class AdapterFactory { | ArbitrumClient | AvalancheClient | AztecClient - | HardhatClient, + | HardhatClient + | unknown, ): NetworkAdapter { switch (networkId) { case 1: case 11155111: case 43114: + case 43113: return new EVMAdapter(networkId, client as unknown as EthereumClient); case 31337: return new HardhatAdapter(client as HardhatClient); case 10: + case 11155420: return new OptimismAdapter(networkId, client as OptimismClient); case 56: case 97: return new BNBAdapter(networkId, client as BNBClient); case 137: + case 80002: return new PolygonAdapter(networkId, client as PolygonClient); case 8453: + case 84532: return new BaseAdapter(networkId, client as BaseClient); case 42161: + case 421614: return new ArbitrumAdapter(networkId, client as ArbitrumClient); default: throw new Error(`Unknown adapter for networkId: ${networkId}`); diff --git a/src/types/index.ts b/src/types/index.ts index ee904ab4..ff27f57b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,10 +10,10 @@ export type NetworkType = "evm" | "bitcoin" | "solana"; /** * All EVM chain IDs supported by the app. - * Maps directly to the connector library's SupportedChainId. - * When adding a new EVM network, add its chain ID to network-connectors first. + * Extends the connector library's SupportedChainId with testnet chain IDs + * that reuse their L1 family's client (not yet registered in network-connectors). */ -export type AppChainId = SupportedChainId; +export type AppChainId = SupportedChainId | 43113 | 421614 | 11155420 | 84532 | 80002; // ==================== CORE DOMAIN TYPES ==================== From 23816b4388ed988229c7d63cec33bc210077f8a5 Mon Sep 17 00:00:00 2001 From: Augusto Lemble Date: Tue, 21 Apr 2026 10:22:33 -0300 Subject: [PATCH 2/2] refactor(adapters): tighten testnet client typing at factory boundary Types EVM_TESTNET_CLIENTS to return the client union instead of unknown so AdapterFactory.createAdapter can keep its strict client union without `| unknown`. Also narrows PolygonAdapter's constructor from SupportedChainId to `137 | 80002` to match sibling L2 adapters. --- src/services/DataService.ts | 9 ++++++++- src/services/adapters/PolygonAdapter/PolygonAdapter.ts | 4 ++-- src/services/adapters/adaptersFactory.ts | 3 +-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/services/DataService.ts b/src/services/DataService.ts index 45925a7c..6c8d5dab 100644 --- a/src/services/DataService.ts +++ b/src/services/DataService.ts @@ -23,9 +23,16 @@ type EVMClientConfig = { type: "fallback" | "parallel" | "race"; }; +type EVMTestnetClient = + | ArbitrumClient + | OptimismClient + | BaseClient + | PolygonClient + | EthereumClient; + // EVM testnets not yet registered in @openscan/network-connectors ClientFactory. // Mapped to their L1 family's client since they share the same JSON-RPC surface. -const EVM_TESTNET_CLIENTS: Record unknown> = { +const EVM_TESTNET_CLIENTS: Record EVMTestnetClient> = { 421614: (config) => new ArbitrumClient(config), 11155420: (config) => new OptimismClient(config), 84532: (config) => new BaseClient(config), diff --git a/src/services/adapters/PolygonAdapter/PolygonAdapter.ts b/src/services/adapters/PolygonAdapter/PolygonAdapter.ts index 5848cb13..942e305a 100644 --- a/src/services/adapters/PolygonAdapter/PolygonAdapter.ts +++ b/src/services/adapters/PolygonAdapter/PolygonAdapter.ts @@ -12,7 +12,7 @@ import { import { normalizeBlockNumber } from "../shared/normalizeBlockNumber"; import { mergeMetadata } from "../shared/mergeMetadata"; -import type { PolygonClient, SupportedChainId, EthereumClient } from "@openscan/network-connectors"; +import type { PolygonClient, EthereumClient } from "@openscan/network-connectors"; /** * Polygon blockchain service @@ -21,7 +21,7 @@ import type { PolygonClient, SupportedChainId, EthereumClient } from "@openscan/ export class PolygonAdapter extends NetworkAdapter { private client: PolygonClient; - constructor(networkId: SupportedChainId | 80002, client: PolygonClient) { + constructor(networkId: 137 | 80002, client: PolygonClient) { super(networkId); this.client = client; this.initTxSearch(client as unknown as EthereumClient); diff --git a/src/services/adapters/adaptersFactory.ts b/src/services/adapters/adaptersFactory.ts index 8bf206e5..faa3cbcc 100644 --- a/src/services/adapters/adaptersFactory.ts +++ b/src/services/adapters/adaptersFactory.ts @@ -39,8 +39,7 @@ export class AdapterFactory { | ArbitrumClient | AvalancheClient | AztecClient - | HardhatClient - | unknown, + | HardhatClient, ): NetworkAdapter { switch (networkId) { case 1: