All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 2m18s
708 lines
27 KiB
TypeScript
708 lines
27 KiB
TypeScript
'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<string, unknown>
|
||
}
|
||
|
||
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<unknown>
|
||
}
|
||
|
||
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<TokenListToken>
|
||
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<CapabilitiesCatalog>
|
||
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<string | null>(null)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [networks, setNetworks] = useState<NetworksCatalog | null>(initialNetworks)
|
||
const [tokenList, setTokenList] = useState<TokenListCatalog | null>(initialTokenList)
|
||
const [capabilities, setCapabilities] = useState<CapabilitiesCatalog | null>(
|
||
initialCapabilities || FALLBACK_CAPABILITIES_138,
|
||
)
|
||
const [networksMeta, setNetworksMeta] = useState<FetchMetadata | null>(initialNetworksMeta)
|
||
const [tokenListMeta, setTokenListMeta] = useState<FetchMetadata | null>(initialTokenListMeta)
|
||
const [capabilitiesMeta, setCapabilitiesMeta] = useState<FetchMetadata | null>(
|
||
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<typeof setTimeout> | 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<number, WalletChain>()
|
||
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<string, TokenListToken>()
|
||
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 (
|
||
<div className="space-y-4 rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800 sm:p-5">
|
||
<h2 className="text-lg font-semibold">Add to MetaMask</h2>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
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.
|
||
</p>
|
||
|
||
<div className="grid gap-3 md:grid-cols-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => addChain(chains.chain138)}
|
||
className="rounded bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700"
|
||
>
|
||
Add Chain 138
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => addChain(chains.ethereum)}
|
||
className="rounded bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
|
||
>
|
||
Add Ethereum Mainnet
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => addChain(chains.allMainnet)}
|
||
className="rounded bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
|
||
>
|
||
Add ALL Mainnet
|
||
</button>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-primary-200 bg-primary-50/40 p-4 dark:border-primary-900 dark:bg-primary-950/20">
|
||
<div className="text-sm font-semibold text-gray-900 dark:text-white">Chain 138 Open Snap</div>
|
||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||
Optional MetaMask Snap that uses{' '}
|
||
<span className="font-medium text-gray-800 dark:text-gray-200">only open Snap permissions</span> (minimal
|
||
privileged APIs in the Snap itself).{' '}
|
||
<span className="font-medium text-gray-800 dark:text-gray-200">Stable MetaMask</span> still only installs npm
|
||
Snaps that appear on MetaMask's install allowlist; if install fails with "not on the allowlist",
|
||
use <span className="font-medium text-gray-800 dark:text-gray-200">MetaMask Flask</span> 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{' '}
|
||
<code className="break-all rounded bg-gray-100 px-1 text-xs dark:bg-gray-900">{CHAIN138_OPEN_SNAP_ID}</code>
|
||
— publish from the repo with <code className="break-all rounded bg-gray-100 px-1 text-xs dark:bg-gray-900">scripts/deployment/publish-chain138-open-snap.sh</code> after{' '}
|
||
<code className="rounded bg-gray-100 px-1 text-xs dark:bg-gray-900">npm login</code>.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => void installOpenSnap()}
|
||
className="mt-3 rounded bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700"
|
||
>
|
||
Install Open Snap
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-700">
|
||
<div className="text-sm font-semibold text-gray-900 dark:text-white">Explorer-served MetaMask metadata</div>
|
||
<div className="mt-3 space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||
<p>Networks catalog: {chains.total > 0 ? `${chains.total} chains` : 'using frontend fallback values'}</p>
|
||
<p>Chain 138 token entries: {tokenCount138}</p>
|
||
<p>Networks source: {networksMeta?.source || 'unknown'}</p>
|
||
<p>Token list source: {tokenListMeta?.source || 'unknown'}</p>
|
||
{metadataKeywordString ? <p>Keywords: {metadataKeywordString}</p> : null}
|
||
</div>
|
||
<div className="mt-4 space-y-3">
|
||
<div>
|
||
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Networks config URL</p>
|
||
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{networksUrl}</code>
|
||
<div className="mt-2 flex flex-wrap gap-2">
|
||
<button type="button" onClick={() => copyText(networksUrl, 'networks config URL')} className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
|
||
Copy URL
|
||
</button>
|
||
<a href={networksUrl} target="_blank" rel="noopener noreferrer" className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
|
||
Open JSON
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Capabilities URL</p>
|
||
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{displayedCapabilitiesUrl}</code>
|
||
<div className="mt-2 flex flex-wrap gap-2">
|
||
<button type="button" onClick={() => copyText(displayedCapabilitiesUrl, 'capabilities URL')} className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
|
||
Copy URL
|
||
</button>
|
||
<a href={displayedCapabilitiesUrl} target="_blank" rel="noopener noreferrer" className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
|
||
Open JSON
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Token list URL</p>
|
||
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{tokenListUrl}</code>
|
||
<div className="mt-2 flex flex-wrap gap-2">
|
||
<button type="button" onClick={() => copyText(tokenListUrl, 'token list URL')} className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
|
||
Copy URL
|
||
</button>
|
||
<a href={tokenListUrl} target="_blank" rel="noopener noreferrer" className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
|
||
Open JSON
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-700">
|
||
<div className="text-sm font-semibold text-gray-900 dark:text-white">Chain 138 RPC capabilities</div>
|
||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||
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.
|
||
</p>
|
||
<div className="mt-4 space-y-3 text-sm text-gray-600 dark:text-gray-400">
|
||
<p>
|
||
RPC endpoint:{' '}
|
||
<span className="font-medium text-gray-900 dark:text-white">
|
||
{capabilities?.rpcUrl || 'using published explorer fallback'}
|
||
</span>
|
||
</p>
|
||
<p>
|
||
Capabilities source:{' '}
|
||
<span className="font-medium text-gray-900 dark:text-white">
|
||
{capabilitiesMeta?.source || 'unknown'}
|
||
</span>
|
||
</p>
|
||
<p>
|
||
HTTP methods: {supportedHTTPMethods.length > 0 ? supportedHTTPMethods.join(', ') : 'metadata unavailable'}
|
||
</p>
|
||
<p>
|
||
Missing wallet-facing methods:{' '}
|
||
{unsupportedHTTPMethods.length > 0 ? unsupportedHTTPMethods.join(', ') : 'none listed'}
|
||
</p>
|
||
<p>
|
||
Trace methods: {supportedTraceMethods.length > 0 ? supportedTraceMethods.join(', ') : 'metadata unavailable'}
|
||
</p>
|
||
{capabilities?.walletSupport?.notes?.map((note) => (
|
||
<p key={note} className="text-xs">
|
||
{note}
|
||
</p>
|
||
))}
|
||
{capabilities?.http?.notes?.map((note) => (
|
||
<p key={note} className="text-xs">
|
||
{note}
|
||
</p>
|
||
))}
|
||
{capabilitiesMeta?.lastModified ? (
|
||
<p className="text-xs">
|
||
Last modified: {new Date(capabilitiesMeta.lastModified).toLocaleString()}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-700">
|
||
<div className="text-sm font-semibold text-gray-900 dark:text-white">Featured Chain 138 tokens</div>
|
||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||
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.
|
||
</p>
|
||
<div className="mt-4 space-y-3">
|
||
{featuredTokens.length === 0 ? (
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">Featured token metadata is not available right now.</p>
|
||
) : featuredTokens.map((token) => (
|
||
<div key={token.address} className="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
|
||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||
<div>
|
||
<div className="text-sm font-semibold text-gray-900 dark:text-white">
|
||
{token.symbol} <span className="font-normal text-gray-500 dark:text-gray-400">({token.name})</span>
|
||
</div>
|
||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||
{token.address}
|
||
</div>
|
||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||
Decimals: {token.decimals}
|
||
{token.tags?.length ? ` • Tags: ${token.tags.join(', ')}` : ''}
|
||
</div>
|
||
{typeof token.extensions?.unitDescription === 'string' ? (
|
||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{token.extensions.unitDescription}</div>
|
||
) : null}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => watchToken(token)}
|
||
className="rounded bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-700"
|
||
>
|
||
Add {token.symbol}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{status ? <p className="text-sm text-green-600 dark:text-green-400">{status}</p> : null}
|
||
{error ? <p className="text-sm text-red-600 dark:text-red-400">{error}</p> : null}
|
||
</div>
|
||
)
|
||
}
|