'use client' import { useEffect, useMemo, useState } from 'react' import { resolveExplorerApiBase } from '@/libs/frontend-api-client/api-base' export type WalletChain = { chainId: string chainIdDecimal?: number chainName: string rpcUrls: string[] blockExplorerUrls: string[] iconUrls?: string[] nativeCurrency: { name: string symbol: string decimals: number } shortName?: string infoURL?: string explorerApiUrl?: string } export type TokenListToken = { chainId: number address: string name: string symbol: string decimals: number logoURI?: string tags?: string[] extensions?: Record } export type NetworksCatalog = { name?: string version?: { major?: number minor?: number patch?: number } defaultChainId?: number chains?: WalletChain[] } export type TokenListCatalog = { name?: string version?: { major?: number minor?: number patch?: number } keywords?: string[] tokens?: TokenListToken[] } export type CapabilitiesCatalog = { name?: string version?: { major?: number minor?: number patch?: number } timestamp?: string chainId?: number chainName?: string rpcUrl?: string explorerUrl?: string explorerApiUrl?: string generatedBy?: string walletSupport?: { walletAddEthereumChain?: boolean walletWatchAsset?: boolean notes?: string[] } http?: { supportedMethods?: string[] unsupportedMethods?: string[] notes?: string[] } tracing?: { supportedMethods?: string[] unsupportedMethods?: string[] notes?: string[] } } export type FetchMetadata = { source?: string | null lastModified?: string | null } interface AddToMetaMaskProps { initialNetworks?: NetworksCatalog | null initialTokenList?: TokenListCatalog | null initialCapabilities?: CapabilitiesCatalog | null initialNetworksMeta?: FetchMetadata | null initialTokenListMeta?: FetchMetadata | null initialCapabilitiesMeta?: FetchMetadata | null } type EthereumProvider = { request: (args: { method: string; params?: unknown }) => Promise } const FALLBACK_CHAIN_138: WalletChain = { chainId: '0x8a', chainIdDecimal: 138, chainName: 'DeFi Oracle Meta Mainnet', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: ['https://rpc-http-pub.d-bis.org', 'https://rpc.d-bis.org', 'https://rpc2.d-bis.org'], blockExplorerUrls: ['https://explorer.d-bis.org', 'https://blockscout.defi-oracle.io'], iconUrls: ['https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png'], shortName: 'dbis', infoURL: 'https://explorer.d-bis.org', explorerApiUrl: 'https://explorer.d-bis.org/api/v2', } const FALLBACK_ETHEREUM: WalletChain = { chainId: '0x1', chainIdDecimal: 1, chainName: 'Ethereum Mainnet', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: ['https://eth.llamarpc.com', 'https://rpc.ankr.com/eth', 'https://ethereum.publicnode.com'], blockExplorerUrls: ['https://etherscan.io'], iconUrls: ['https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png'], shortName: 'eth', infoURL: 'https://ethereum.org', } const FALLBACK_ALL_MAINNET: WalletChain = { chainId: '0x9f2c4', chainIdDecimal: 651940, chainName: 'ALL Mainnet', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: ['https://mainnet-rpc.alltra.global'], blockExplorerUrls: ['https://alltra.global'], iconUrls: ['https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png'], shortName: 'all', infoURL: 'https://alltra.global', } const FEATURED_TOKEN_SYMBOLS = ['cUSDT', 'cUSDC', 'USDT', 'USDC', 'cXAUC', 'cXAUT'] /** npm-published Snap using open Snap permissions only; stable MetaMask still requires MetaMask’s install allowlist. */ const CHAIN138_OPEN_SNAP_ID = 'npm:chain138-open-snap' as const const FALLBACK_CAPABILITIES_138: CapabilitiesCatalog = { name: 'Chain 138 RPC Capabilities', version: { major: 1, minor: 1, patch: 0 }, timestamp: '2026-03-28T00:00:00Z', generatedBy: 'DBIS Explorer', chainId: 138, chainName: 'DeFi Oracle Meta Mainnet', rpcUrl: 'https://rpc-http-pub.d-bis.org', explorerUrl: 'https://explorer.d-bis.org', explorerApiUrl: 'https://explorer.d-bis.org/api/v2', walletSupport: { walletAddEthereumChain: true, walletWatchAsset: true, notes: [ 'MetaMask primarily relies on JSON-RPC correctness for balances, gas estimation, calls, and transaction submission.', 'Explorer-served network metadata and token list metadata complement wallet UX but do not replace RPC method support.', ], }, http: { supportedMethods: [ 'web3_clientVersion', 'net_version', 'eth_chainId', 'eth_blockNumber', 'eth_syncing', 'eth_gasPrice', 'eth_maxPriorityFeePerGas', 'eth_feeHistory', 'eth_estimateGas', 'eth_getCode', ], unsupportedMethods: [], notes: [ 'eth_feeHistory is available for wallet fee estimation.', 'eth_maxPriorityFeePerGas is exposed on the public RPC for wallet-grade fee suggestion compatibility.', ], }, tracing: { supportedMethods: ['trace_block', 'trace_replayBlockTransactions'], unsupportedMethods: ['debug_traceBlockByNumber'], notes: [ 'TRACE support is enabled for explorer-grade indexing and internal transaction analysis.', 'Debug tracing is intentionally not enabled on the public RPC tier.', ], }, } function isTokenListToken(value: unknown): value is TokenListToken { if (!value || typeof value !== 'object') return false const candidate = value as Partial return ( typeof candidate.chainId === 'number' && typeof candidate.address === 'string' && candidate.address.trim().length > 0 && typeof candidate.name === 'string' && typeof candidate.symbol === 'string' && typeof candidate.decimals === 'number' ) } function isCapabilitiesCatalog(value: unknown): value is CapabilitiesCatalog { if (!value || typeof value !== 'object') return false const candidate = value as Partial return ( typeof candidate.chainId === 'number' && typeof candidate.chainName === 'string' && candidate.chainName.trim().length > 0 && typeof candidate.rpcUrl === 'string' && candidate.rpcUrl.trim().length > 0 ) } function getApiBase() { return resolveExplorerApiBase({ serverFallback: 'https://blockscout.defi-oracle.io', }) } export function AddToMetaMask({ initialNetworks = null, initialTokenList = null, initialCapabilities = null, initialNetworksMeta = null, initialTokenListMeta = null, initialCapabilitiesMeta = null, }: AddToMetaMaskProps) { const [status, setStatus] = useState(null) const [error, setError] = useState(null) const [networks, setNetworks] = useState(initialNetworks) const [tokenList, setTokenList] = useState(initialTokenList) const [capabilities, setCapabilities] = useState( initialCapabilities || FALLBACK_CAPABILITIES_138, ) const [networksMeta, setNetworksMeta] = useState(initialNetworksMeta) const [tokenListMeta, setTokenListMeta] = useState(initialTokenListMeta) const [capabilitiesMeta, setCapabilitiesMeta] = useState( initialCapabilitiesMeta || (initialCapabilities ? { source: 'explorer-api', lastModified: initialCapabilities.timestamp || null, } : { source: 'frontend-fallback', lastModified: FALLBACK_CAPABILITIES_138.timestamp || null, }), ) const ethereum = typeof window !== 'undefined' ? (window as unknown as { ethereum?: EthereumProvider }).ethereum : undefined const apiBase = getApiBase().replace(/\/$/, '') const tokenListUrl = `${apiBase}/api/config/token-list` const networksUrl = `${apiBase}/api/config/networks` const capabilitiesUrl = `${apiBase}/api/config/capabilities` const staticCapabilitiesUrl = typeof window !== 'undefined' ? `${window.location.origin.replace(/\/$/, '')}/config/CHAIN138_RPC_CAPABILITIES.json` : `${apiBase}/config/CHAIN138_RPC_CAPABILITIES.json` useEffect(() => { let active = true let timer: ReturnType | null = null async function fetchJson(url: string) { const response = await fetch(url, { cache: 'no-store', headers: { 'Cache-Control': 'no-cache', }, }) const json = response.ok ? await response.json() : null const meta: FetchMetadata = { source: response.headers.get('X-Config-Source') || 'explorer-api', lastModified: response.headers.get('Last-Modified'), } return { json, meta } } async function loadCatalogs() { try { const [networksResponse, tokenListResponse, capabilitiesResponse] = await Promise.all([ fetchJson(networksUrl), fetchJson(tokenListUrl), fetchJson(capabilitiesUrl), ]) let resolvedCapabilities = capabilitiesResponse if (!isCapabilitiesCatalog(resolvedCapabilities.json)) { const staticCapabilitiesResponse = await fetchJson(staticCapabilitiesUrl) if (isCapabilitiesCatalog(staticCapabilitiesResponse.json)) { resolvedCapabilities = { json: staticCapabilitiesResponse.json, meta: { source: staticCapabilitiesResponse.meta.source || 'public-static-fallback', lastModified: staticCapabilitiesResponse.meta.lastModified, }, } } else { resolvedCapabilities = { json: FALLBACK_CAPABILITIES_138, meta: { source: 'frontend-fallback', lastModified: FALLBACK_CAPABILITIES_138.timestamp || null, }, } } } if (!active) return setNetworks(networksResponse.json) setTokenList(tokenListResponse.json) setCapabilities(resolvedCapabilities.json) setNetworksMeta(networksResponse.meta) setTokenListMeta(tokenListResponse.meta) setCapabilitiesMeta(resolvedCapabilities.meta) } catch { if (!active) return setNetworks((current) => current) setTokenList((current) => current) setCapabilities((current) => current || FALLBACK_CAPABILITIES_138) setNetworksMeta((current) => current) setTokenListMeta((current) => current) setCapabilitiesMeta((current) => current || { source: 'frontend-fallback', lastModified: FALLBACK_CAPABILITIES_138.timestamp || null, }, ) } finally { if (active) { timer = setTimeout(() => { void loadCatalogs() }, 60_000) } } } void loadCatalogs() return () => { active = false if (timer) clearTimeout(timer) } }, [capabilitiesUrl, networksUrl, staticCapabilitiesUrl, tokenListUrl]) const catalogTokens = useMemo( () => (Array.isArray(tokenList?.tokens) ? tokenList.tokens.filter(isTokenListToken) : []), [tokenList], ) const chains = useMemo(() => { const chainMap = new Map() for (const chain of networks?.chains || []) { if (typeof chain.chainIdDecimal === 'number') { chainMap.set(chain.chainIdDecimal, chain) } } return { chain138: chainMap.get(138) || FALLBACK_CHAIN_138, ethereum: chainMap.get(1) || FALLBACK_ETHEREUM, allMainnet: chainMap.get(651940) || FALLBACK_ALL_MAINNET, total: (networks?.chains || []).length, } }, [networks]) const featuredTokens = useMemo(() => { const tokenMap = new Map() for (const token of catalogTokens) { if (token.chainId !== 138) continue if (!FEATURED_TOKEN_SYMBOLS.includes(token.symbol)) continue tokenMap.set(token.symbol, token) } return FEATURED_TOKEN_SYMBOLS .map((symbol) => tokenMap.get(symbol)) .filter((token): token is TokenListToken => !!token) }, [catalogTokens]) const addChain = async (chain: WalletChain) => { setError(null) setStatus(null) if (!ethereum) { setError('MetaMask or another Web3 wallet is not installed.') return } try { await ethereum.request({ method: 'wallet_addEthereumChain', params: [chain], }) setStatus(`Added ${chain.chainName}. You can switch to it in your wallet.`) } catch (e) { const err = e as { code?: number; message?: string } if (err.code === 4902) { setStatus(`Added ${chain.chainName}. You can switch to it in your wallet.`) } else { setError(err.message || `Failed to add ${chain.chainName}.`) } } } const installOpenSnap = async () => { setError(null) setStatus(null) if (!ethereum) { setError('MetaMask or another Web3 wallet is not installed.') return } try { await ethereum.request({ method: 'wallet_requestSnaps', params: { [CHAIN138_OPEN_SNAP_ID]: {} }, }) setStatus( `Installed or connected to ${CHAIN138_OPEN_SNAP_ID}. In MetaMask, open Snaps → Chain 138 Open for the home page (token list URL, network info).`, ) } catch (e) { const err = e as { message?: string } const msg = err.message || '' const allowlistBlocked = /allowlist/i.test(msg) if (allowlistBlocked && msg) { setError( `${msg} Production MetaMask only installs allowlisted Snaps from npm. Use MetaMask Flask for unrestricted installs during development, or request allowlisting via MetaMask’s Snaps documentation.`, ) } else { setError( msg || `Could not install Snap. Enable MetaMask Snaps and ensure ${CHAIN138_OPEN_SNAP_ID} is published on npm.`, ) } } } const watchToken = async (token: TokenListToken) => { setError(null) setStatus(null) if (!isTokenListToken(token)) { setError('Token metadata is incomplete right now. Refresh the page and try again.') return } if (!ethereum) { setError('MetaMask or another Web3 wallet is not installed.') return } try { const added = await ethereum.request({ method: 'wallet_watchAsset', params: { type: 'ERC20', options: { address: token.address, symbol: token.symbol, decimals: token.decimals, image: token.logoURI, }, }, }) setStatus(added ? `Added ${token.symbol} to your wallet.` : `${token.symbol} request was dismissed.`) } catch (e) { const err = e as { message?: string } setError(err.message || `Failed to add ${token.symbol}.`) } } const copyText = async (value: string, label: string) => { setError(null) setStatus(null) try { await navigator.clipboard.writeText(value) setStatus(`Copied ${label}.`) } catch { setError(`Could not copy ${label}.`) } } const tokenCount138 = catalogTokens.filter((token) => token.chainId === 138).length const metadataKeywordString = (tokenList?.keywords || []).join(', ') const supportedHTTPMethods = capabilities?.http?.supportedMethods || [] const unsupportedHTTPMethods = capabilities?.http?.unsupportedMethods || [] const supportedTraceMethods = capabilities?.tracing?.supportedMethods || [] const displayedCapabilitiesUrl = capabilitiesMeta?.source === 'public-static-fallback' || capabilitiesMeta?.source === 'frontend-fallback' ? staticCapabilitiesUrl : capabilitiesUrl return (

Add to MetaMask

The wallet tools now read the same explorer-served network catalog and token list that MetaMask can consume. That keeps chain metadata, token metadata, and optional extensions aligned with the live explorer API instead of relying on stale frontend-only defaults. MetaMask does not run built-in token detection on custom networks such as Chain 138: add the token list URL below under Settings → Security & privacy → Token lists so tokens and icons load automatically when you are on this chain.

Chain 138 Open Snap

Optional MetaMask Snap that uses{' '} only open Snap permissions (minimal privileged APIs in the Snap itself).{' '} Stable MetaMask still only installs npm Snaps that appear on MetaMask's install allowlist; if install fails with "not on the allowlist", use MetaMask Flask for development or apply for allowlisting. It adds in-wallet weekly reminders, Chain 138 transaction/signature hints, and the token list URL on the Snap home page. The package on npm is{' '} {CHAIN138_OPEN_SNAP_ID} — publish from the repo with scripts/deployment/publish-chain138-open-snap.sh after{' '} npm login.

Explorer-served MetaMask metadata

Networks catalog: {chains.total > 0 ? `${chains.total} chains` : 'using frontend fallback values'}

Chain 138 token entries: {tokenCount138}

Networks source: {networksMeta?.source || 'unknown'}

Token list source: {tokenListMeta?.source || 'unknown'}

{metadataKeywordString ?

Keywords: {metadataKeywordString}

: null}

Networks config URL

{networksUrl}
Open JSON

Capabilities URL

{displayedCapabilitiesUrl}
Open JSON

Token list URL

{tokenListUrl}
Open JSON
Chain 138 RPC capabilities

This capability matrix documents what public RPC methods wallets can rely on today, what tracing the explorer can use, and where MetaMask will still fall back because the public node does not expose every optional fee method.

RPC endpoint:{' '} {capabilities?.rpcUrl || 'using published explorer fallback'}

Capabilities source:{' '} {capabilitiesMeta?.source || 'unknown'}

HTTP methods: {supportedHTTPMethods.length > 0 ? supportedHTTPMethods.join(', ') : 'metadata unavailable'}

Missing wallet-facing methods:{' '} {unsupportedHTTPMethods.length > 0 ? unsupportedHTTPMethods.join(', ') : 'none listed'}

Trace methods: {supportedTraceMethods.length > 0 ? supportedTraceMethods.join(', ') : 'metadata unavailable'}

{capabilities?.walletSupport?.notes?.map((note) => (

{note}

))} {capabilities?.http?.notes?.map((note) => (

{note}

))} {capabilitiesMeta?.lastModified ? (

Last modified: {new Date(capabilitiesMeta.lastModified).toLocaleString()}

) : null}
Featured Chain 138 tokens

These tokens come from the explorer token list and use `wallet_watchAsset` so the wallet gets the same symbol, decimals, image, and optional token metadata that the explorer publishes.

{featuredTokens.length === 0 ? (

Featured token metadata is not available right now.

) : featuredTokens.map((token) => (
{token.symbol} ({token.name})
{token.address}
Decimals: {token.decimals} {token.tags?.length ? ` • Tags: ${token.tags.join(', ')}` : ''}
{typeof token.extensions?.unitDescription === 'string' ? (
{token.extensions.unitDescription}
) : null}
))}
{status ?

{status}

: null} {error ?

{error}

: null}
) }