chore: metamask networks, explorer SPA, nginx scripts; ignore Python cache
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 12s

- Dual-chain / GRU deployment JSON sync
- Frontend explorer SPA + MetaMask components
- Scripts: nginx fixes, link deploy, local SPA serve helper
- Token icon chain-138.png; .gitignore __pycache__

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-10 12:56:30 -07:00
parent e5df7c2ea3
commit d4f922c26e
17 changed files with 1449 additions and 91 deletions

View File

@@ -13,8 +13,14 @@ function toneClasses(tone: 'neutral' | 'success' | 'warning' | 'info') {
}
}
export function getEntityBadgeTone(tag: string): 'neutral' | 'success' | 'warning' | 'info' {
const normalized = tag.toLowerCase()
function normalizeBadgeLabel(value: unknown): string {
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') return String(value)
return 'unknown'
}
export function getEntityBadgeTone(tag: unknown): 'neutral' | 'success' | 'warning' | 'info' {
const normalized = normalizeBadgeLabel(tag).toLowerCase()
if (normalized === 'compliant' || normalized === 'listed' || normalized === 'verified' || normalized === 'gru') {
return 'success'
}
@@ -27,15 +33,16 @@ export function getEntityBadgeTone(tag: string): 'neutral' | 'success' | 'warnin
return 'neutral'
}
export function formatEntityBadgeLabel(label: string): string {
const normalized = label.toLowerCase()
export function formatEntityBadgeLabel(label: unknown): string {
const resolvedLabel = normalizeBadgeLabel(label)
const normalized = resolvedLabel.toLowerCase()
const labels: Record<string, string> = {
'reference-asset': 'reference asset',
'electronic-money': 'cash e-money',
'treasury-bond': 'treasury / gov bond',
gru: 'GRU',
}
return labels[normalized] || label
return labels[normalized] || resolvedLabel
}
export default function EntityBadge({
@@ -43,7 +50,7 @@ export default function EntityBadge({
tone,
className,
}: {
label: string
label: unknown
tone?: 'neutral' | 'success' | 'warning' | 'info'
className?: string
}) {

View File

@@ -84,6 +84,32 @@ export type CapabilitiesCatalog = {
}
}
type WatchAssetEntry = {
type: 'ERC20'
options: {
address: string
symbol: string
decimals: number
image?: string
}
metadata?: {
name?: string
registryFamily?: string
familySymbol?: string
deploymentVersion?: string
deploymentStatus?: string
}
}
type MetaMaskConfig = {
source?: string
version?: string
chainId?: number
addEthereumChain?: WalletChain
watchAssets?: WatchAssetEntry[]
caveats?: string[]
}
export type FetchMetadata = {
source?: string | null
lastModified?: string | null
@@ -109,7 +135,11 @@ const FALLBACK_CHAIN_138: WalletChain = {
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'],
iconUrls: [
'https://explorer.d-bis.org/api/v1/report/logo/chain-138',
'https://explorer.d-bis.org/token-icons/chain-138.png',
'https://explorer.d-bis.org/favicon.ico',
],
shortName: 'dbis',
infoURL: 'https://explorer.d-bis.org',
explorerApiUrl: 'https://explorer.d-bis.org/api/v2',
@@ -139,6 +169,22 @@ const FALLBACK_ALL_MAINNET: WalletChain = {
infoURL: 'https://alltra.global',
}
const MAINNET_CWUSDC_TOKEN: TokenListToken = {
chainId: 1,
address: '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a',
symbol: 'cWUSDC',
name: 'Wrapped cUSDC',
decimals: 6,
logoURI: 'https://explorer.d-bis.org/api/v1/report/logo/cUSDC?v=20260510',
tags: ['mainnet', 'cw', 'usd'],
extensions: {
registryFamily: 'iso4217',
familySymbol: 'USD',
canonicalSourceChainId: 138,
canonicalSourceSymbol: 'cUSDC',
},
}
const FEATURED_TOKEN_SYMBOLS = ['cUSDT', 'cUSDC', 'USDT', 'USDC', 'cXAUC', 'cXAUT']
/** npm-published Snap using open Snap permissions only; stable MetaMask still requires MetaMasks install allowlist. */
@@ -218,12 +264,62 @@ function isCapabilitiesCatalog(value: unknown): value is CapabilitiesCatalog {
)
}
function isWatchAssetEntry(value: unknown): value is WatchAssetEntry {
if (!value || typeof value !== 'object') return false
const candidate = value as Partial<WatchAssetEntry>
const options = (candidate.options || {}) as Partial<WatchAssetEntry['options']>
return (
candidate.type === 'ERC20' &&
typeof options.address === 'string' &&
options.address.trim().length > 0 &&
typeof options.symbol === 'string' &&
options.symbol.trim().length > 0 &&
typeof options.decimals === 'number'
)
}
function isMetaMaskConfig(value: unknown): value is MetaMaskConfig {
if (!value || typeof value !== 'object') return false
const candidate = value as Partial<MetaMaskConfig>
return (
typeof candidate.chainId === 'number' &&
!!candidate.addEthereumChain &&
Array.isArray(candidate.watchAssets)
)
}
function watchAssetToToken(entry: WatchAssetEntry): TokenListToken {
return {
chainId: 138,
address: entry.options.address,
symbol: entry.options.symbol,
name: entry.metadata?.name || entry.options.symbol,
decimals: entry.options.decimals,
logoURI: entry.options.image,
extensions: {
registryFamily: entry.metadata?.registryFamily,
familySymbol: entry.metadata?.familySymbol,
deploymentVersion: entry.metadata?.deploymentVersion,
deploymentStatus: entry.metadata?.deploymentStatus,
},
}
}
function getApiBase() {
return resolveExplorerApiBase({
serverFallback: 'https://blockscout.defi-oracle.io',
browserOrigin: '',
serverFallback: 'https://explorer.d-bis.org',
})
}
function formatStableTimestamp(value: string): string {
const timestamp = Date.parse(value)
if (Number.isNaN(timestamp)) return value
return new Date(timestamp).toISOString()
}
export function AddToMetaMask({
initialNetworks = null,
initialTokenList = null,
@@ -253,19 +349,20 @@ export function AddToMetaMask({
lastModified: FALLBACK_CAPABILITIES_138.timestamp || null,
}),
)
const [metamaskConfig, setMetamaskConfig] = useState<MetaMaskConfig | null>(null)
const [metamaskConfigMeta, setMetamaskConfigMeta] = useState<FetchMetadata | null>(null)
const [watchAssetProgress, setWatchAssetProgress] = useState<{ current: number; total: number } | null>(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 tokenListUrl = `${apiBase}/api/v1/report/token-list?chainId=138`
const networksUrl = `${apiBase}/api/config/networks`
const metamaskConfigUrl = `${apiBase}/api/v1/config/metamask?chainId=138`
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`
const staticCapabilitiesUrl = `${apiBase}/config/CHAIN138_RPC_CAPABILITIES.json`
useEffect(() => {
let active = true
@@ -293,6 +390,7 @@ export function AddToMetaMask({
fetchJson(tokenListUrl),
fetchJson(capabilitiesUrl),
])
const metamaskConfigResponse = await fetchJson(metamaskConfigUrl).catch(() => null)
let resolvedCapabilities = capabilitiesResponse
if (!isCapabilitiesCatalog(resolvedCapabilities.json)) {
@@ -320,6 +418,10 @@ export function AddToMetaMask({
setNetworks(networksResponse.json)
setTokenList(tokenListResponse.json)
setCapabilities(resolvedCapabilities.json)
if (isMetaMaskConfig(metamaskConfigResponse?.json)) {
setMetamaskConfig(metamaskConfigResponse.json)
setMetamaskConfigMeta(metamaskConfigResponse.meta)
}
setNetworksMeta(networksResponse.meta)
setTokenListMeta(tokenListResponse.meta)
setCapabilitiesMeta(resolvedCapabilities.meta)
@@ -328,6 +430,7 @@ export function AddToMetaMask({
setNetworks((current) => current)
setTokenList((current) => current)
setCapabilities((current) => current || FALLBACK_CAPABILITIES_138)
setMetamaskConfig((current) => current)
setNetworksMeta((current) => current)
setTokenListMeta((current) => current)
setCapabilitiesMeta((current) =>
@@ -351,7 +454,7 @@ export function AddToMetaMask({
active = false
if (timer) clearTimeout(timer)
}
}, [capabilitiesUrl, networksUrl, staticCapabilitiesUrl, tokenListUrl])
}, [capabilitiesUrl, metamaskConfigUrl, networksUrl, staticCapabilitiesUrl, tokenListUrl])
const catalogTokens = useMemo(
() => (Array.isArray(tokenList?.tokens) ? tokenList.tokens.filter(isTokenListToken) : []),
@@ -367,12 +470,12 @@ export function AddToMetaMask({
}
return {
chain138: chainMap.get(138) || FALLBACK_CHAIN_138,
chain138: metamaskConfig?.addEthereumChain || chainMap.get(138) || FALLBACK_CHAIN_138,
ethereum: chainMap.get(1) || FALLBACK_ETHEREUM,
allMainnet: chainMap.get(651940) || FALLBACK_ALL_MAINNET,
total: (networks?.chains || []).length,
}
}, [networks])
}, [metamaskConfig, networks])
const featuredTokens = useMemo(() => {
const tokenMap = new Map<string, TokenListToken>()
@@ -387,6 +490,15 @@ export function AddToMetaMask({
.filter((token): token is TokenListToken => !!token)
}, [catalogTokens])
const watchAssetTokens = useMemo(() => {
const endpointTokens = (metamaskConfig?.watchAssets || [])
.filter(isWatchAssetEntry)
.map(watchAssetToToken)
if (endpointTokens.length > 0) return endpointTokens
return catalogTokens.filter((token) => token.chainId === 138)
}, [catalogTokens, metamaskConfig])
const addChain = async (chain: WalletChain) => {
setError(null)
setStatus(null)
@@ -412,6 +524,39 @@ export function AddToMetaMask({
}
}
const switchOrAddChain = async (chain: WalletChain) => {
if (!ethereum) {
setError('MetaMask or another Web3 wallet is not installed.')
return false
}
try {
await ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chain.chainId }],
})
return true
} catch (e) {
const err = e as { code?: number; message?: string }
if (err.code !== 4902) {
setError(err.message || `Failed to switch to ${chain.chainName}.`)
return false
}
}
try {
await ethereum.request({
method: 'wallet_addEthereumChain',
params: [chain],
})
return true
} catch (e) {
const err = e as { message?: string }
setError(err.message || `Failed to add ${chain.chainName}.`)
return false
}
}
const installOpenSnap = async () => {
setError(null)
setStatus(null)
@@ -435,7 +580,7 @@ export function AddToMetaMask({
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 MetaMasks Snaps documentation.`,
`${msg} This is expected on Stable MetaMask until this exact Snap package and version are accepted on MetaMask's install allowlist. The production path on this page is Add Chain 138 plus EIP-747 Add Tokens; use MetaMask Flask for Snap testing or submit/update the Snap allowlist request before using this button with Stable MetaMask.`,
)
} else {
setError(
@@ -481,6 +626,63 @@ export function AddToMetaMask({
}
}
const refreshMainnetCwusdc = async () => {
setError(null)
setStatus(null)
const switched = await switchOrAddChain(chains.ethereum)
if (!switched) return
await watchToken(MAINNET_CWUSDC_TOKEN)
}
const watchTokensSequentially = async (tokens: TokenListToken[], label: string) => {
setError(null)
setStatus(null)
setWatchAssetProgress(null)
if (!ethereum) {
setError('MetaMask or another Web3 wallet is not installed.')
return
}
const validTokens = tokens.filter(isTokenListToken)
if (validTokens.length === 0) {
setError('No complete token metadata is available for wallet_watchAsset right now.')
return
}
let addedCount = 0
for (let index = 0; index < validTokens.length; index += 1) {
const token = validTokens[index]
setWatchAssetProgress({ current: index + 1, total: validTokens.length })
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,
},
},
})
if (added) addedCount += 1
} catch (e) {
const err = e as { message?: string }
setError(err.message || `Stopped while adding ${token.symbol}.`)
setStatus(`${addedCount} of ${validTokens.length} ${label} token requests were accepted before the flow stopped.`)
setWatchAssetProgress(null)
return
}
}
setWatchAssetProgress(null)
setStatus(`${addedCount} of ${validTokens.length} ${label} token requests were accepted by the wallet.`)
}
const copyText = async (value: string, label: string) => {
setError(null)
setStatus(null)
@@ -510,8 +712,8 @@ export function AddToMetaMask({
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.
as Chain 138, so this page uses EIP-747 wallet_watchAsset prompts from the live MetaMask payload to add token
metadata directly to the wallet.
</p>
<div className="grid gap-3 md:grid-cols-3">
@@ -538,17 +740,19 @@ export function AddToMetaMask({
</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>
<div className="rounded-lg border border-amber-200 bg-amber-50/50 p-4 dark:border-amber-900 dark:bg-amber-950/20">
<div className="text-sm font-semibold text-gray-900 dark:text-white">Optional Chain 138 Open Snap</div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Optional MetaMask Snap that uses{' '}
This is <span className="font-medium text-gray-800 dark:text-gray-200">not required</span> for the production
wallet flow above. The normal production path is to add Chain 138, then add tokens through EIP-747
wallet_watchAsset prompts. The optional Snap 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&apos;s install allowlist; if install fails with &quot;not on the allowlist&quot;,
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{' '}
Snaps that appear on MetaMask&apos;s install allowlist; if install fails with &quot;not on the allowlist&quot;, that is
an external MetaMask review gate rather than an explorer/network failure. Use{' '}
<span className="font-medium text-gray-800 dark:text-gray-200">MetaMask Flask</span> for development or apply
for allowlisting before using this with Stable MetaMask. 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>.
@@ -556,9 +760,9 @@ export function AddToMetaMask({
<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"
className="mt-3 rounded bg-amber-700 px-4 py-2 text-sm font-medium text-white hover:bg-amber-800"
>
Install Open Snap
Install Snap (Flask or allowlisted Stable)
</button>
</div>
@@ -568,8 +772,10 @@ export function AddToMetaMask({
<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>EIP-747 watchAsset entries: {watchAssetTokens.length}</p>
<p>Networks source: {networksMeta?.source || 'unknown'}</p>
<p>Token list source: {tokenListMeta?.source || 'unknown'}</p>
<p>MetaMask payload source: {metamaskConfigMeta?.source || 'unknown'}</p>
{metadataKeywordString ? <p>Keywords: {metadataKeywordString}</p> : null}
</div>
<div className="mt-4 space-y-3">
@@ -597,6 +803,18 @@ export function AddToMetaMask({
</a>
</div>
</div>
<div>
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">EIP-747 MetaMask payload URL</p>
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{metamaskConfigUrl}</code>
<div className="mt-2 flex flex-wrap gap-2">
<button type="button" onClick={() => copyText(metamaskConfigUrl, 'MetaMask payload 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={metamaskConfigUrl} 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>
@@ -653,7 +871,7 @@ export function AddToMetaMask({
))}
{capabilitiesMeta?.lastModified ? (
<p className="text-xs">
Last modified: {new Date(capabilitiesMeta.lastModified).toLocaleString()}
Last modified: {formatStableTimestamp(capabilitiesMeta.lastModified)}
</p>
) : null}
</div>
@@ -662,9 +880,31 @@ export function AddToMetaMask({
<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.
These tokens come from the explorer MetaMask payload and use wallet_watchAsset so the wallet gets the same
symbol, decimals, image, and optional token metadata that the explorer publishes. MetaMask requires a user
approval for each token, so the bulk actions below run as a guided sequence of wallet prompts.
</p>
<div className="mt-4 flex flex-wrap gap-2">
<button
type="button"
onClick={() => void watchTokensSequentially(featuredTokens, 'featured Chain 138')}
className="rounded bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-700"
>
Add featured tokens
</button>
<button
type="button"
onClick={() => void watchTokensSequentially(watchAssetTokens, 'Chain 138')}
className="rounded bg-gray-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-gray-700"
>
Add all Chain 138 tokens
</button>
{watchAssetProgress ? (
<span className="self-center text-sm text-gray-600 dark:text-gray-400">
Prompt {watchAssetProgress.current} of {watchAssetProgress.total}
</span>
) : null}
</div>
<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>
@@ -698,6 +938,35 @@ export function AddToMetaMask({
))}
</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">Ethereum Mainnet cWUSDC</div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
This refreshes the Mainnet cWUSDC custom asset metadata with the DBIS-hosted image URL. MetaMask fiat price
display still depends on MetaMask and upstream asset/price providers accepting the Mainnet listing.
</p>
<div className="mt-4 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">
{MAINNET_CWUSDC_TOKEN.symbol}{' '}
<span className="font-normal text-gray-500 dark:text-gray-400">({MAINNET_CWUSDC_TOKEN.name})</span>
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{MAINNET_CWUSDC_TOKEN.address}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Ethereum Mainnet Decimals: {MAINNET_CWUSDC_TOKEN.decimals}
</div>
</div>
<button
type="button"
onClick={() => void refreshMainnetCwusdc()}
className="rounded bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-700"
>
Refresh Mainnet cWUSDC
</button>
</div>
</div>
</div>
</div>
{status ? <p className="text-sm text-green-600 dark:text-green-400">{status}</p> : null}