|
|
|
|
@@ -1,44 +1,191 @@
|
|
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react'
|
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
|
|
|
|
|
|
const CHAIN_138 = {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TokenListToken = {
|
|
|
|
|
chainId: number
|
|
|
|
|
address: string
|
|
|
|
|
name: string
|
|
|
|
|
symbol: string
|
|
|
|
|
decimals: number
|
|
|
|
|
logoURI?: string
|
|
|
|
|
tags?: string[]
|
|
|
|
|
extensions?: Record<string, unknown>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type NetworksCatalog = {
|
|
|
|
|
name?: string
|
|
|
|
|
version?: {
|
|
|
|
|
major?: number
|
|
|
|
|
minor?: number
|
|
|
|
|
patch?: number
|
|
|
|
|
}
|
|
|
|
|
defaultChainId?: number
|
|
|
|
|
chains?: WalletChain[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TokenListCatalog = {
|
|
|
|
|
name?: string
|
|
|
|
|
version?: {
|
|
|
|
|
major?: number
|
|
|
|
|
minor?: number
|
|
|
|
|
patch?: number
|
|
|
|
|
}
|
|
|
|
|
keywords?: string[]
|
|
|
|
|
tokens?: TokenListToken[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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'],
|
|
|
|
|
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 CHAIN_MAINNET = {
|
|
|
|
|
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 CHAIN_ALL_MAINNET = {
|
|
|
|
|
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']
|
|
|
|
|
|
|
|
|
|
function getApiBase() {
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
return process.env.NEXT_PUBLIC_API_URL || window.location.origin
|
|
|
|
|
}
|
|
|
|
|
return process.env.NEXT_PUBLIC_API_URL || 'https://explorer.d-bis.org'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function AddToMetaMask() {
|
|
|
|
|
const [status, setStatus] = useState<string | null>(null)
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
const [networks, setNetworks] = useState<NetworksCatalog | null>(null)
|
|
|
|
|
const [tokenList, setTokenList] = useState<TokenListCatalog | null>(null)
|
|
|
|
|
|
|
|
|
|
const ethereum = typeof window !== 'undefined' ? (window as unknown as { ethereum?: { request: (args: { method: string; params: unknown[] }) => Promise<unknown> } }).ethereum : undefined
|
|
|
|
|
const ethereum = typeof window !== 'undefined'
|
|
|
|
|
? (window as unknown as { ethereum?: EthereumProvider }).ethereum
|
|
|
|
|
: undefined
|
|
|
|
|
|
|
|
|
|
const addChain = async (chain: typeof CHAIN_138) => {
|
|
|
|
|
const apiBase = getApiBase().replace(/\/$/, '')
|
|
|
|
|
const tokenListUrl = `${apiBase}/api/config/token-list`
|
|
|
|
|
const networksUrl = `${apiBase}/api/config/networks`
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let active = true
|
|
|
|
|
|
|
|
|
|
async function loadCatalogs() {
|
|
|
|
|
try {
|
|
|
|
|
const [networksResponse, tokenListResponse] = await Promise.all([
|
|
|
|
|
fetch(networksUrl),
|
|
|
|
|
fetch(tokenListUrl),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const [networksJson, tokenListJson] = await Promise.all([
|
|
|
|
|
networksResponse.ok ? networksResponse.json() : null,
|
|
|
|
|
tokenListResponse.ok ? tokenListResponse.json() : null,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
if (!active) return
|
|
|
|
|
setNetworks(networksJson)
|
|
|
|
|
setTokenList(tokenListJson)
|
|
|
|
|
} catch {
|
|
|
|
|
if (!active) return
|
|
|
|
|
setNetworks(null)
|
|
|
|
|
setTokenList(null)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadCatalogs()
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
active = false
|
|
|
|
|
}
|
|
|
|
|
}, [networksUrl, tokenListUrl])
|
|
|
|
|
|
|
|
|
|
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 tokenList?.tokens || []) {
|
|
|
|
|
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)
|
|
|
|
|
}, [tokenList])
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
|
@@ -50,53 +197,168 @@ export function AddToMetaMask() {
|
|
|
|
|
if (err.code === 4902) {
|
|
|
|
|
setStatus(`Added ${chain.chainName}. You can switch to it in your wallet.`)
|
|
|
|
|
} else {
|
|
|
|
|
setError(err.message || 'Failed to add network')
|
|
|
|
|
setError(err.message || `Failed to add ${chain.chainName}.`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Production (explorer.d-bis.org): same origin; dev: NEXT_PUBLIC_API_URL or localhost
|
|
|
|
|
const apiBase =
|
|
|
|
|
typeof window !== 'undefined'
|
|
|
|
|
? process.env.NEXT_PUBLIC_API_URL || window.location.origin
|
|
|
|
|
: process.env.NEXT_PUBLIC_API_URL || 'https://explorer.d-bis.org'
|
|
|
|
|
const tokenListUrl = apiBase.replace(/\/$/, '') + '/api/config/token-list'
|
|
|
|
|
const watchToken = async (token: TokenListToken) => {
|
|
|
|
|
setError(null)
|
|
|
|
|
setStatus(null)
|
|
|
|
|
|
|
|
|
|
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 = (tokenList?.tokens || []).filter((token) => token.chainId === 138).length
|
|
|
|
|
const metadataKeywordString = (tokenList?.keywords || []).join(', ')
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 space-y-4">
|
|
|
|
|
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
|
|
|
|
<h2 className="text-lg font-semibold">Add to MetaMask</h2>
|
|
|
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
|
|
|
Add Chain 138 (DeFi Oracle Meta Mainnet), Ethereum Mainnet, or ALL Mainnet to your wallet. Then add the token list URL in MetaMask Settings so tokens appear automatically.
|
|
|
|
|
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.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => addChain(CHAIN_138)}
|
|
|
|
|
className="px-4 py-2 rounded bg-primary-600 text-white hover:bg-primary-700 text-sm font-medium"
|
|
|
|
|
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(CHAIN_MAINNET)}
|
|
|
|
|
className="px-4 py-2 rounded bg-gray-600 text-white hover:bg-gray-700 text-sm font-medium"
|
|
|
|
|
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(CHAIN_ALL_MAINNET)}
|
|
|
|
|
className="px-4 py-2 rounded bg-gray-600 text-white hover:bg-gray-700 text-sm font-medium"
|
|
|
|
|
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="text-sm">
|
|
|
|
|
<p className="text-gray-600 dark:text-gray-400 mb-1">Token list URL (add in MetaMask Settings → Token lists):</p>
|
|
|
|
|
<code className="block p-2 rounded bg-gray-100 dark:bg-gray-900 break-all text-xs">{tokenListUrl}</code>
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
{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">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">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>}
|
|
|
|
|
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
|
|
|
|
|
|
|
|
|
{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>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|