chore: metamask networks, explorer SPA, nginx scripts; ignore Python cache
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 12s
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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -55,6 +55,10 @@ backend/bin/
|
||||
backend/api/rest/cmd/api-server
|
||||
backend/cmd
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Tooling / scratch directories
|
||||
out/
|
||||
cache/
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"version": {"major": 1, "minor": 2, "patch": 0},
|
||||
"defaultChainId": 138,
|
||||
"explorerUrl": "https://explorer.d-bis.org",
|
||||
"tokenListUrl": "https://explorer.d-bis.org/api/config/token-list",
|
||||
"tokenListUrl": "https://explorer.d-bis.org/api/v1/report/token-list?chainId=138",
|
||||
"generatedBy": "DBIS Explorer",
|
||||
"chains": [
|
||||
{"chainId":"0x8a","chainIdDecimal":138,"chainName":"DeFi Oracle Meta Mainnet","shortName":"dbis","rpcUrls":["https://rpc-http-pub.d-bis.org","https://rpc.d-bis.org","https://rpc2.d-bis.org","https://rpc.defi-oracle.io"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"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"],"infoURL":"https://explorer.d-bis.org","explorerApiUrl":"https://explorer.d-bis.org/api/v2","testnet":false},
|
||||
{"chainId":"0x8a","chainIdDecimal":138,"chainName":"DeFi Oracle Meta Mainnet","shortName":"dbis","rpcUrls":["https://rpc-http-pub.d-bis.org","https://rpc.d-bis.org","https://rpc2.d-bis.org","https://rpc.defi-oracle.io"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://explorer.d-bis.org","https://blockscout.defi-oracle.io"],"iconUrls":["https://explorer.d-bis.org/token-icons/chain-138.png","https://explorer.d-bis.org/api/v1/report/logo/chain-138","https://explorer.d-bis.org/favicon.ico"],"infoURL":"https://explorer.d-bis.org","explorerApiUrl":"https://explorer.d-bis.org/api/v2","testnet":false},
|
||||
{"chainId":"0x1","chainIdDecimal":1,"chainName":"Ethereum Mainnet","shortName":"eth","rpcUrls":["https://eth.llamarpc.com","https://rpc.ankr.com/eth","https://ethereum.publicnode.com","https://1rpc.io/eth"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://etherscan.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"],"infoURL":"https://ethereum.org","testnet":false},
|
||||
{"chainId":"0x9f2c4","chainIdDecimal":651940,"chainName":"ALL Mainnet","shortName":"all","rpcUrls":["https://mainnet-rpc.alltra.global"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://alltra.global"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"],"infoURL":"https://alltra.global","testnet":false},
|
||||
{"chainId":"0x19","chainIdDecimal":25,"chainName":"Cronos Mainnet","rpcUrls":["https://evm.cronos.org","https://cronos-rpc.publicnode.com"],"nativeCurrency":{"name":"CRO","symbol":"CRO","decimals":18},"blockExplorerUrls":["https://cronos.org/explorer"],"iconUrls":["https://ipfs.io/ipfs/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong"]},
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
"coveredSymbols": 10,
|
||||
"missingSymbols": []
|
||||
},
|
||||
"note": "The public EVM cW token mesh is complete on the currently loaded 10-chain set, but Wemix remains a desired target without a cW suite in deployment-status.json."
|
||||
"note": "The public EVM cW token mesh is aligned to the nine-chain promoted surface (Cronos excluded from that count); Wemix remains a desired target without a cW suite in deployment-status.json."
|
||||
},
|
||||
"transport": {
|
||||
"liveTransportAssets": [
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
"https://explorer.d-bis.org"
|
||||
],
|
||||
"iconUrls": [
|
||||
"https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"
|
||||
"https://explorer.d-bis.org/token-icons/chain-138.png",
|
||||
"https://explorer.d-bis.org/api/v1/report/logo/chain-138",
|
||||
"https://explorer.d-bis.org/favicon.ico"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -90,4 +92,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
- **Wallet status (1639, 1722)** – `statusEl.innerHTML` uses `shortenHash(userAddress)`. If `userAddress` were ever from an untrusted source, it should be escaped. **Action:** Use `escapeHtml(shortenHash(userAddress))` for consistency (in **H1**).
|
||||
- **loadGasAndNetworkStats (2509)** – `el.innerHTML` uses `gasGwei`, `blockTimeSec`, `tps`. These are from API; escaping is low risk but recommended for defense in depth. **Action:** Escape these values (in **H1** or small follow-up).
|
||||
- **Token list: `#/token/' + contract`** – The `contract` in `href="#/token/' + contract + '"` can break the attribute if it contains a quote. **Action:** Encode or validate; include in **H2** (safe href/attributes).
|
||||
- **External link (3800)** – `'https://explorer.d-bis.org/address/' + addr + '/contract'` – `addr` should be validated or encoded so the URL cannot be malformed. **Action:** Use `encodeURIComponent(addr)` for the path segment (in **H2**).
|
||||
- **External link (3800)** – `'https://explorer.d-bis.org/addresses/' + addr + '/contract'` – `addr` should be validated or encoded so the URL cannot be malformed. **Action:** Use `encodeURIComponent(addr)` for the path segment (in **H2**).
|
||||
|
||||
### 2.3 SPA: onclick and attribute injection
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,6 +156,15 @@
|
||||
body.dark-theme .table th { background: #334155; color: var(--text); }
|
||||
body.dark-theme .table tr:hover { background: #1e293b; }
|
||||
body.dark-theme .skeleton { background: linear-gradient(90deg, #334155 25%, #475569 50%, #334155 75%); }
|
||||
body.explorer-compact-table-rows .table tbody td,
|
||||
body.explorer-compact-table-rows .table tbody th {
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
body.explorer-compact-table-rows .table thead th {
|
||||
padding: 0.45rem 0.55rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--light);
|
||||
@@ -267,6 +276,31 @@
|
||||
opacity: 1;
|
||||
}
|
||||
.nav-dropdown-menu li { margin: 0; }
|
||||
.nav-dropdown-menu li.user-menu-meta {
|
||||
padding: 0.55rem 1rem 0.65rem;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255,255,255,0.72);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.12);
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
.nav-links a:focus-visible,
|
||||
.nav-dropdown-trigger:focus-visible,
|
||||
.nav-actions button:focus-visible,
|
||||
.nav-actions select:focus-visible {
|
||||
outline: 2px solid rgba(255,255,255,0.95);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
body.dark-theme .nav-links a:focus-visible,
|
||||
body.dark-theme .nav-dropdown-trigger:focus-visible {
|
||||
outline-color: #fbbf24;
|
||||
}
|
||||
.nav-actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.nav-actions #walletConnect { width: 100%; justify-content: flex-end; margin-top: 0.35rem; }
|
||||
}
|
||||
.search-box {
|
||||
flex: 1;
|
||||
max-width: 560px;
|
||||
@@ -1251,6 +1285,19 @@
|
||||
</li>
|
||||
<li><a href="/snap/" aria-label="Chain 138 MetaMask Snap"><i class="fas fa-wallet" aria-hidden="true"></i> <span>MetaMask Snap</span></a></li>
|
||||
<li role="none"><a href="/operations" role="menuitem" onclick="event.preventDefault(); showMore(); updatePath('/operations'); closeNavMenu();" aria-label="View operations hub"><i class="fas fa-compass-drafting" aria-hidden="true"></i> <span>Operations</span></a></li>
|
||||
<li class="nav-dropdown" id="userAccountNav" style="display: none;">
|
||||
<button type="button" class="nav-dropdown-trigger" aria-expanded="false" aria-haspopup="true" aria-controls="userAccountMenu" id="userAccountTrigger"><i class="fas fa-user-circle" aria-hidden="true"></i> <span>User</span> <i class="fas fa-chevron-down" aria-hidden="true"></i></button>
|
||||
<ul class="nav-dropdown-menu" id="userAccountMenu" role="menu">
|
||||
<li role="none" class="user-menu-meta"><span id="userMenuIdentityLabel">Signed in</span></li>
|
||||
<li role="none" id="userMenuInstitutionRow" style="display: none;"><a href="#" role="menuitem" onclick="event.preventDefault(); if (typeof showInstitutionConsole === 'function') showInstitutionConsole(); closeNavMenu(); return false;"><i class="fas fa-building-columns" aria-hidden="true"></i> Institution console</a></li>
|
||||
<li role="none" id="userMenuCompareRow" style="display: none;"><a href="#" role="menuitem" onclick="event.preventDefault(); if (typeof showCompareAddresses === 'function') showCompareAddresses(); closeNavMenu(); return false;"><i class="fas fa-columns" aria-hidden="true"></i> Compare addresses</a></li>
|
||||
<li role="none" id="userMenuKeyboardRow" style="display: none;"><a href="#" role="menuitem" onclick="event.preventDefault(); if (typeof showKeyboardShortcutsModal === 'function') showKeyboardShortcutsModal(); closeNavMenu(); return false;"><i class="fas fa-keyboard" aria-hidden="true"></i> Keyboard shortcuts</a></li>
|
||||
<li role="none" id="userMenuDiagnosticsRow" style="display: none;"><a href="#" role="menuitem" onclick="event.preventDefault(); if (typeof copyExplorerDiagnosticsBundle === 'function') copyExplorerDiagnosticsBundle(); closeNavMenu(); return false;"><i class="fas fa-clipboard-list" aria-hidden="true"></i> Copy diagnostics bundle</a></li>
|
||||
<li role="none" id="userMenuIncidentsRow" style="display: none;"><a href="#" role="menuitem" onclick="event.preventDefault(); if (typeof showIncidentLinksModal === 'function') showIncidentLinksModal(); closeNavMenu(); return false;"><i class="fas fa-triangle-exclamation" aria-hidden="true"></i> Incident & ops links</a></li>
|
||||
<li role="none" id="userMenuVerifyContractsRow" style="display: none;"><a href="#" role="menuitem" onclick="event.preventDefault(); if (typeof showContractVerificationHelp === 'function') showContractVerificationHelp(); closeNavMenu(); return false;"><i class="fas fa-file-contract" aria-hidden="true"></i> Verify & publish contracts</a></li>
|
||||
<li role="none"><a href="#" role="menuitem" onclick="event.preventDefault(); if (typeof disconnectExplorerWallet === 'function') disconnectExplorerWallet(); closeNavMenu(); return false;"><i class="fas fa-right-from-bracket" aria-hidden="true"></i> Sign out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<select id="localeSelect" onchange="setLocale(this.value)" style="padding: 0.35rem 0.5rem; border-radius: 6px; background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); font-size: 0.875rem;" aria-label="Language">
|
||||
@@ -1327,6 +1374,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contractVerifyHelpModal" class="contract-verify-help-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="contractVerifyHelpTitle" style="display:none; position:fixed; inset:0; z-index:20001;">
|
||||
<div id="contractVerifyHelpBackdrop" style="position:absolute; inset:0; background: rgba(8, 15, 32, 0.62); backdrop-filter: blur(10px);"></div>
|
||||
<div style="position:relative; z-index:1; display:flex; justify-content:center; align-items:flex-start; padding: 6vh 1rem 2rem; min-height:100%;">
|
||||
<div style="width:min(640px, 100%); background: var(--card-bg, var(--light)); color: var(--text); border:1px solid var(--border); border-radius: 20px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); overflow:hidden;">
|
||||
<div style="padding: 1.1rem 1.25rem; border-bottom:1px solid var(--border); display:flex; align-items:flex-start; justify-content:space-between; gap:1rem;">
|
||||
<div>
|
||||
<div id="contractVerifyHelpTitle" style="font-size:1.05rem; font-weight:700;">Verify and publish smart contracts</div>
|
||||
<div style="margin-top:0.35rem; color:var(--text-light); font-size:0.9rem;">For operator-approved accounts: submit sources in Blockscout or run the LAN verification script from the Proxmox repo.</div>
|
||||
</div>
|
||||
<button id="contractVerifyHelpCloseBtn" type="button" class="btn btn-secondary" style="padding:0.45rem 0.75rem;">Close</button>
|
||||
</div>
|
||||
<div style="padding:1.1rem 1.25rem 1.35rem; display:grid; gap:1rem;">
|
||||
<div>
|
||||
<label for="contractVerifyAddressInput" style="display:block; font-weight:600; margin-bottom:0.35rem;">Contract address (optional)</label>
|
||||
<input type="text" id="contractVerifyAddressInput" placeholder="0x…" autocomplete="off" spellcheck="false" style="width:100%; padding:0.55rem 0.75rem; border-radius:10px; border:1px solid var(--border); background:var(--light); color:var(--text); font-size:0.95rem;">
|
||||
<div style="margin-top:0.5rem; display:flex; flex-wrap:wrap; gap:0.5rem;">
|
||||
<a id="contractVerifyBlockscoutLink" href="#" class="btn btn-primary" style="text-decoration:none; display:inline-flex; align-items:center; gap:0.4rem;" target="_blank" rel="noopener noreferrer"><i class="fas fa-external-link-alt" aria-hidden="true"></i> Open in Blockscout (verify tab)</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border:1px solid var(--border); border-radius:12px; padding:0.85rem 1rem; background:var(--muted-surface);">
|
||||
<div style="font-weight:600; margin-bottom:0.4rem;"><i class="fas fa-terminal" aria-hidden="true"></i> Proxmox repo (LAN + Foundry)</div>
|
||||
<p style="color:var(--text-light); font-size:0.88rem; line-height:1.5; margin-bottom:0.65rem;">From the repository root on a host that can reach Blockscout and your RPC (see <code style="font-size:0.85em;">forge-verification-proxy</code> in-repo):</p>
|
||||
<div style="display:flex; gap:0.5rem; align-items:stretch; flex-wrap:wrap;">
|
||||
<code id="contractVerifyScriptCommand" style="flex:1; min-width:0; display:block; padding:0.55rem 0.65rem; border-radius:8px; background:var(--card-bg); border:1px solid var(--border); font-size:0.8rem; word-break:break-all;">./scripts/verify/run-contract-verification-with-proxy.sh</code>
|
||||
<button type="button" id="contractVerifyCopyCommandBtn" class="btn btn-secondary" style="white-space:nowrap;">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:var(--text-light); font-size:0.85rem; margin:0;">The public SPA cannot run <code>forge verify</code>. Use Blockscout’s form or the script above. Optional alternate bridge verify: set <code>VERIFY_ALTERNATE_CCIPWETH9_BRIDGE=1</code> when using <code>scripts/verify-contracts-blockscout.sh</code> (see repo docs).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="keyboardShortcutsModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="keyboardShortcutsTitle" style="display:none; position:fixed; inset:0; z-index:20002;">
|
||||
<div id="keyboardShortcutsBackdrop" style="position:absolute; inset:0; background: rgba(8, 15, 32, 0.62); backdrop-filter: blur(10px);"></div>
|
||||
<div style="position:relative; z-index:1; display:flex; justify-content:center; align-items:flex-start; padding: 6vh 1rem 2rem; min-height:100%;">
|
||||
<div style="width:min(520px, 100%); background: var(--card-bg, var(--light)); color: var(--text); border:1px solid var(--border); border-radius: 20px; box-shadow: 0 24px 80px rgba(0,0,0,0.35);">
|
||||
<div style="padding: 1.1rem 1.25rem; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; gap:1rem;">
|
||||
<div id="keyboardShortcutsTitle" style="font-size:1.05rem; font-weight:700;">Keyboard shortcuts</div>
|
||||
<button type="button" id="keyboardShortcutsCloseBtn" class="btn btn-secondary">Close</button>
|
||||
</div>
|
||||
<div style="padding:1rem 1.25rem 1.25rem; font-size:0.92rem; line-height:1.55;">
|
||||
<table class="table" style="margin:0;"><tbody>
|
||||
<tr><td><kbd style="padding:0.2rem 0.45rem;border-radius:6px;border:1px solid var(--border);">/</kbd> or <kbd style="padding:0.2rem 0.45rem;border-radius:6px;border:1px solid var(--border);">Ctrl</kbd>+<kbd style="padding:0.2rem 0.45rem;border-radius:6px;border:1px solid var(--border);">K</kbd></td><td>Open search</td></tr>
|
||||
<tr><td><kbd style="padding:0.2rem 0.45rem;border-radius:6px;border:1px solid var(--border);">Esc</kbd></td><td>Close modals</td></tr>
|
||||
</tbody></table>
|
||||
<p style="margin-top:0.75rem; color:var(--text-light); margin-bottom:0;">Institution users: open the User menu for session tools, diagnostics export, and the institution console.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="incidentLinksModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="incidentLinksTitle" style="display:none; position:fixed; inset:0; z-index:20002;">
|
||||
<div id="incidentLinksBackdrop" style="position:absolute; inset:0; background: rgba(8, 15, 32, 0.62); backdrop-filter: blur(10px);"></div>
|
||||
<div style="position:relative; z-index:1; display:flex; justify-content:center; align-items:flex-start; padding: 6vh 1rem 2rem; min-height:100%;">
|
||||
<div style="width:min(560px, 100%); background: var(--card-bg, var(--light)); color: var(--text); border:1px solid var(--border); border-radius: 20px; box-shadow: 0 24px 80px rgba(0,0,0,0.35);">
|
||||
<div style="padding: 1.1rem 1.25rem; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; gap:1rem;">
|
||||
<div id="incidentLinksTitle" style="font-size:1.05rem; font-weight:700;">Incident & ops bookmarks</div>
|
||||
<button type="button" id="incidentLinksCloseBtn" class="btn btn-secondary">Close</button>
|
||||
</div>
|
||||
<div style="padding:1rem 1.25rem 1.25rem; display:grid; gap:0.65rem; font-size:0.92rem;">
|
||||
<p style="color:var(--text-light); margin:0;">Read-only shortcuts for operator response (no secrets). Adjust hosts to match your deployment.</p>
|
||||
<a class="btn btn-secondary" href="https://explorer.d-bis.org/" target="_blank" rel="noopener noreferrer" style="text-align:center; text-decoration:none;">Public explorer</a>
|
||||
<a class="btn btn-secondary" href="http://192.168.11.140:4000/" target="_blank" rel="noopener noreferrer" style="text-align:center; text-decoration:none;">Blockscout (LAN template)</a>
|
||||
<a class="btn btn-secondary" href="https://gitea.d-bis.org/d-bis/explorer-monorepo/src/branch/main/docs/EXPLORER_API_ACCESS.md" target="_blank" rel="noopener noreferrer" style="text-align:center; text-decoration:none;">Explorer API access doc</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="explorerLiveRegion" aria-live="polite" aria-atomic="true" class="sr-only" role="status"></div>
|
||||
<div class="container" id="mainContent">
|
||||
<!-- Home View -->
|
||||
@@ -1778,6 +1895,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="institutionView" class="detail-view">
|
||||
<div class="breadcrumb" id="institutionBreadcrumb"><a href="/">Home</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">Institution</span></div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
|
||||
<h2 class="card-title"><i class="fas fa-building-columns" aria-hidden="true"></i> Institution console</h2>
|
||||
</div>
|
||||
<div id="institutionContent">
|
||||
<div class="loading"><i class="fas fa-spinner" aria-hidden="true"></i> Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="compareView" class="detail-view">
|
||||
<div class="breadcrumb" id="compareBreadcrumb"><a href="/">Home</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">Compare addresses</span></div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
|
||||
<h2 class="card-title"><i class="fas fa-columns" aria-hidden="true"></i> Compare addresses</h2>
|
||||
</div>
|
||||
<div id="compareContent">
|
||||
<div class="loading"><i class="fas fa-spinner" aria-hidden="true"></i> Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
@@ -1816,6 +1959,6 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/explorer-spa.js?v=35"></script>
|
||||
<script src="/explorer-spa.js?v=38"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
frontend/public/token-icons/chain-138.png
Normal file
BIN
frontend/public/token-icons/chain-138.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 213 KiB |
@@ -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
|
||||
}) {
|
||||
|
||||
@@ -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 MetaMask’s 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 MetaMask’s 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'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{' '}
|
||||
Snaps that appear on MetaMask's install allowlist; if install fails with "not on the allowlist", 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}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Add nginx proxy for token-aggregation service at /api/v1/ on explorer.d-bis.org (VMID 5000).
|
||||
# Run on the explorer VM. Requires token-aggregation running (default port 3000).
|
||||
# Run on the explorer VM. Requires token-aggregation listening on TOKEN_AGG_PORT (VMID 5000
|
||||
# production default 3001 per fix-nginx-conflicts-vmid5000.sh; local dev often PORT=3000).
|
||||
# Chain 138 Snap companion site (GATSBY_SNAP_API_BASE_URL=https://explorer.d-bis.org) then gets
|
||||
# market data, swap quotes, and bridge routes from this API.
|
||||
# Usage: [TOKEN_AGG_PORT=3000] [CONFIG_FILE=/etc/nginx/sites-available/blockscout] bash apply-nginx-token-aggregation-proxy.sh
|
||||
# Usage: [TOKEN_AGG_PORT=3001] [CONFIG_FILE=/etc/nginx/sites-available/blockscout] bash apply-nginx-token-aggregation-proxy.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TOKEN_AGG_PORT="${TOKEN_AGG_PORT:-3000}"
|
||||
TOKEN_AGG_PORT="${TOKEN_AGG_PORT:-3001}"
|
||||
CONFIG_FILE="${CONFIG_FILE:-/etc/nginx/sites-available/blockscout}"
|
||||
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
|
||||
@@ -128,5 +128,5 @@ echo ""
|
||||
echo "If deployment still pending:"
|
||||
echo " 1. Wait additional time (5-10 minutes)"
|
||||
echo " 2. Use Remix IDE (instructions above)"
|
||||
echo " 3. Check block explorer: https://explorer.d-bis.org/address/$ACCOUNT"
|
||||
echo " 3. Check block explorer: https://explorer.d-bis.org/addresses/$ACCOUNT"
|
||||
echo ""
|
||||
|
||||
@@ -154,7 +154,7 @@ if [ "$CONFIRMED" != "true" ]; then
|
||||
echo "2. Transaction may have failed"
|
||||
echo "3. RPC node may be out of sync"
|
||||
echo ""
|
||||
echo "Check block explorer: https://explorer.d-bis.org/address/$ACCOUNT"
|
||||
echo "Check block explorer: https://explorer.d-bis.org/addresses/$ACCOUNT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ if [ -n "$DEPLOYED_ADDRESS" ] && [ "$DEPLOYED_ADDRESS" != "0x0000000000000000000
|
||||
log_info "Next steps:"
|
||||
log_info "1. Update config/address-inventory.json with LINK token address"
|
||||
log_info "2. Fund bridge contracts: ./scripts/fund-bridge-contracts.sh 10"
|
||||
log_info "3. Verify contract on explorer: https://explorer.d-bis.org/address/$DEPLOYED_ADDRESS"
|
||||
log_info "3. Verify contract on explorer: https://explorer.d-bis.org/addresses/$DEPLOYED_ADDRESS"
|
||||
|
||||
exit 0
|
||||
else
|
||||
|
||||
@@ -66,6 +66,36 @@ server {
|
||||
}
|
||||
location = /snap { rewrite ^ /snap/ last; }
|
||||
|
||||
# Token-aggregation + static config on HTTP (plain :80) so /api/v1/* never hits Blockscout by mistake
|
||||
location /api/v1/ {
|
||||
proxy_pass http://127.0.0.1:3001/api/v1/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 60s;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
location = /api/config/token-list {
|
||||
default_type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
alias /var/www/html/config/DUAL_CHAIN_TOKEN_LIST.tokenlist.json;
|
||||
}
|
||||
location = /api/config/networks {
|
||||
default_type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
alias /var/www/html/config/DUAL_CHAIN_NETWORKS.json;
|
||||
}
|
||||
location = /api/config/capabilities {
|
||||
default_type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
alias /var/www/html/config/CHAIN138_RPC_CAPABILITIES.json;
|
||||
}
|
||||
|
||||
location / {
|
||||
if ($redirect_to_https = 1) { return 301 https://$host$request_uri; }
|
||||
proxy_pass http://127.0.0.1:4000;
|
||||
@@ -112,24 +142,7 @@ server {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# Blockscout Explorer endpoint - proxy to Blockscout
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:4000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
|
||||
# Token-aggregation API at /api/v1/ (Chain 138 Snap: market data, swap quote, bridge). Service runs on port 3001.
|
||||
# Token-aggregation API at /api/v1/ — define BEFORE /api/ and / so longest-prefix routing is explicit (Snap + dApps).
|
||||
location /api/v1/ {
|
||||
proxy_pass http://127.0.0.1:3001/api/v1/;
|
||||
proxy_http_version 1.1;
|
||||
@@ -162,7 +175,7 @@ server {
|
||||
alias /var/www/html/config/CHAIN138_RPC_CAPABILITIES.json;
|
||||
}
|
||||
|
||||
# API endpoint (for Blockscout API)
|
||||
# Blockscout API (excludes /api/v1/ which is handled above)
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:4000;
|
||||
proxy_http_version 1.1;
|
||||
@@ -184,6 +197,23 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
add_header Content-Type application/json;
|
||||
}
|
||||
|
||||
# Blockscout Explorer UI (catch-all)
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:4000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
}
|
||||
|
||||
# WebSocket upgrade mapping
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Simple local server for explorer (fallback option)
|
||||
# Usage: ./serve-explorer-local.sh [port]
|
||||
#!/usr/bin/env bash
|
||||
# Local static explorer with SPA path fallback (/institution, /compare, /addresses/… → index.html).
|
||||
# Usage: SERVE_BIND=0.0.0.0 ./scripts/serve-explorer-local.sh [port]
|
||||
# Requires: Python 3.7+
|
||||
|
||||
PORT=${1:-8080}
|
||||
FRONTEND_DIR="$(cd "$(dirname "$0")/../frontend/public" && pwd)"
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PORT="${1:-8080}"
|
||||
BIND="${SERVE_BIND:-127.0.0.1}"
|
||||
|
||||
if [ ! -f "$FRONTEND_DIR/index.html" ]; then
|
||||
echo "❌ Frontend not found at: $FRONTEND_DIR/index.html"
|
||||
exit 1
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "python3 required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Serving explorer on http://localhost:$PORT"
|
||||
echo "Frontend: $FRONTEND_DIR"
|
||||
|
||||
cd "$FRONTEND_DIR"
|
||||
|
||||
# Try Python 3 first, then Python 2
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 -m http.server "$PORT"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
python -m SimpleHTTPServer "$PORT"
|
||||
else
|
||||
echo "❌ Python not found. Install Python to use this script."
|
||||
exit 1
|
||||
fi
|
||||
exec python3 "$SCRIPT_DIR/serve_explorer_spa.py" "$PORT" --bind "$BIND"
|
||||
|
||||
56
scripts/serve_explorer_spa.py
Executable file
56
scripts/serve_explorer_spa.py
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static explorer with SPA fallback: unknown paths serve index.html (client router).
|
||||
API paths (/api/, /explorer-api/) are not rewritten so missing backends still 404 clearly."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description="Serve explorer-monorepo/frontend/public with SPA fallback.")
|
||||
p.add_argument("port", nargs="?", type=int, default=8080, help="Listen port (default 8080)")
|
||||
p.add_argument(
|
||||
"--bind",
|
||||
default="127.0.0.1",
|
||||
help="Bind address (default 127.0.0.1)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root = os.path.normpath(os.path.join(script_dir, "..", "frontend", "public"))
|
||||
if not os.path.isfile(os.path.join(root, "index.html")):
|
||||
print(f"ERROR: index.html not found under {root}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, directory=root, **kw)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
path = urlparse(self.path).path
|
||||
if path.startswith("/api/") or path.startswith("/explorer-api"):
|
||||
return super().do_GET()
|
||||
rel = path.lstrip("/")
|
||||
if rel.startswith(".."):
|
||||
self.send_error(403, "Forbidden")
|
||||
return
|
||||
fs = os.path.join(root, rel) if rel else root
|
||||
if os.path.isfile(fs):
|
||||
return super().do_GET()
|
||||
self.path = "/index.html"
|
||||
return super().do_GET()
|
||||
|
||||
httpd = ThreadingHTTPServer((args.bind, args.port), Handler)
|
||||
print(f"Serving SPA explorer: http://{args.bind}:{args.port}/ (root={root})", flush=True)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user