Compare commits
56 Commits
devin/1778
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b08c18b92b | ||
|
|
206dda1580 | ||
|
|
890ebe3727 | ||
|
|
e0194621a1 | ||
|
|
391e84797f | ||
|
|
0cb31cfa9d | ||
|
|
7e82b917f5 | ||
|
|
4990fc1d34 | ||
|
|
0abccf8e38 | ||
|
|
8a64626eaf | ||
|
|
b87ebee6a1 | ||
|
|
0f02e6e54f | ||
|
|
4d6c2891cd | ||
|
|
2eae47b0d1 | ||
|
|
228fa0eef6 | ||
|
|
763ca75c21 | ||
|
|
ab9c1f9f98 | ||
|
|
efd7c8bbcb | ||
|
|
4fac5e4856 | ||
|
|
b213c6547d | ||
|
|
567b4647c0 | ||
|
|
8a61b1bde2 | ||
|
|
f2ebe824bd | ||
|
|
991d1bb07c | ||
|
|
847cfeb48b | ||
|
|
6a64d2fec6 | ||
|
|
7a7dfca221 | ||
|
|
e3ec87c324 | ||
|
|
0778c18e59 | ||
|
|
4b747f0309 | ||
|
|
ca1394c579 | ||
|
|
e14b43e3fe | ||
|
|
64e78dad47 | ||
|
|
654933cb36 | ||
|
|
d4f922c26e | ||
| e5df7c2ea3 | |||
|
|
e397245ec9 | ||
|
|
8cd8bfa195 | ||
|
|
3b7e24080f | ||
|
|
ba08199051 | ||
|
|
0ba2a70c34 | ||
|
|
ac40184d6b | ||
|
|
7a16ddccf7 | ||
|
|
1f5167aded | ||
|
|
f5eb874210 | ||
|
|
1aa81f454a | ||
|
|
39d2f2482b | ||
|
|
06070e479e | ||
|
|
c01e7c2c4a | ||
|
|
945864af7a | ||
|
|
2e59b9d19c | ||
|
|
b6e74eb5bd | ||
|
|
74b175c2ce | ||
|
|
5ea36e8958 | ||
|
|
6a27d2c0e8 | ||
|
|
cc70283171 |
@@ -37,7 +37,8 @@ jobs:
|
||||
if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
fi
|
||||
curl -sSf -X POST "${{ secrets.PHOENIX_DEPLOY_URL }}" \
|
||||
curl -sSf --connect-timeout 10 --max-time 3600 \
|
||||
-X POST "${{ secrets.PHOENIX_DEPLOY_URL }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.PHOENIX_DEPLOY_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"repo\":\"${{ gitea.repository }}\",\"sha\":\"${SHA}\",\"branch\":\"${BRANCH}\",\"target\":\"explorer-live\"}"
|
||||
|
||||
70
.gitea/workflows/validate-on-pr.yml
Normal file
70
.gitea/workflows/validate-on-pr.yml
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Validate Explorer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- '.gitea/workflows/validate-on-pr.yml'
|
||||
- 'frontend/**'
|
||||
- 'scripts/e2e-*.spec.ts'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'playwright.config.ts'
|
||||
push:
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- '.gitea/workflows/validate-on-pr.yml'
|
||||
- 'frontend/**'
|
||||
- 'scripts/e2e-*.spec.ts'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint, type-check, and unit tests
|
||||
run: npm test
|
||||
|
||||
smoke-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: frontend
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browser
|
||||
run: npx playwright install chromium
|
||||
|
||||
- name: Run live sprint smoke tests
|
||||
env:
|
||||
EXPLORER_URL: https://explorer.d-bis.org
|
||||
run: npm run e2e -- scripts/e2e-sprint-smoke.spec.ts
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -21,8 +21,10 @@ build/
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
# IDE (team settings may be committed)
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
@@ -55,6 +57,10 @@ backend/bin/
|
||||
backend/api/rest/cmd/api-server
|
||||
backend/cmd
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Tooling / scratch directories
|
||||
out/
|
||||
cache/
|
||||
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"go.useLanguageServer": true,
|
||||
"go.toolsEnvVars": {
|
||||
"GO111MODULE": "on"
|
||||
},
|
||||
"gopls": {
|
||||
"build.env": {
|
||||
"GO111MODULE": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
backend/.vscode/settings.json
vendored
Normal file
23
backend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"go.useLanguageServer": true,
|
||||
"go.alternateTools": {
|
||||
"go": "/home/intlc/sdk/go1.23.4/bin/go",
|
||||
"gopls": "/home/intlc/go/bin/gopls"
|
||||
},
|
||||
"go.toolsEnvVars": {
|
||||
"GO111MODULE": "on",
|
||||
"GOWORK": "off",
|
||||
"GOROOT": "/home/intlc/sdk/go1.23.4",
|
||||
"GOPATH": "/home/intlc/go",
|
||||
"GOMODCACHE": "/home/intlc/go/pkg/mod"
|
||||
},
|
||||
"gopls": {
|
||||
"build.env": {
|
||||
"GO111MODULE": "on",
|
||||
"GOWORK": "off",
|
||||
"GOROOT": "/home/intlc/sdk/go1.23.4",
|
||||
"GOPATH": "/home/intlc/go",
|
||||
"GOMODCACHE": "/home/intlc/go/pkg/mod"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func (s *Server) handleGetBlockByNumber(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
// Add query timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `
|
||||
@@ -89,7 +89,7 @@ func (s *Server) handleGetBlockByHash(w http.ResponseWriter, r *http.Request, ha
|
||||
}
|
||||
|
||||
// Add query timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"minor": 1,
|
||||
"patch": 0
|
||||
},
|
||||
"generatedBy": "SolaceScan",
|
||||
"generatedBy": "DBIS Explorer",
|
||||
"timestamp": "2026-03-28T00:00:00Z",
|
||||
"chainId": 138,
|
||||
"chainName": "DeFi Oracle Meta Mainnet",
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
{
|
||||
"name": "MetaMask Multi-Chain Networks (13 chains)",
|
||||
"version": {"major": 1, "minor": 2, "patch": 0},
|
||||
"version": {"major": 1, "minor": 2, "patch": 1},
|
||||
"defaultChainId": 138,
|
||||
"explorerUrl": "https://explorer.d-bis.org",
|
||||
"tokenListUrl": "https://explorer.d-bis.org/api/config/token-list",
|
||||
"generatedBy": "SolaceScan",
|
||||
"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":"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"]},
|
||||
{"chainId":"0x38","chainIdDecimal":56,"chainName":"BNB Smart Chain","rpcUrls":["https://bsc-dataseed.binance.org","https://bsc-dataseed1.defibit.io","https://bsc-dataseed1.ninicoin.io"],"nativeCurrency":{"name":"BNB","symbol":"BNB","decimals":18},"blockExplorerUrls":["https://bscscan.com"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0x64","chainIdDecimal":100,"chainName":"Gnosis Chain","rpcUrls":["https://rpc.gnosischain.com","https://gnosis-rpc.publicnode.com","https://1rpc.io/gnosis"],"nativeCurrency":{"name":"xDAI","symbol":"xDAI","decimals":18},"blockExplorerUrls":["https://gnosisscan.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0x89","chainIdDecimal":137,"chainName":"Polygon","rpcUrls":["https://polygon-rpc.com","https://polygon.llamarpc.com","https://polygon-bor-rpc.publicnode.com"],"nativeCurrency":{"name":"MATIC","symbol":"MATIC","decimals":18},"blockExplorerUrls":["https://polygonscan.com"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0xa","chainIdDecimal":10,"chainName":"Optimism","rpcUrls":["https://mainnet.optimism.io","https://optimism.llamarpc.com","https://optimism-rpc.publicnode.com"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://optimistic.etherscan.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0xa4b1","chainIdDecimal":42161,"chainName":"Arbitrum One","rpcUrls":["https://arb1.arbitrum.io/rpc","https://arbitrum.llamarpc.com","https://arbitrum-one-rpc.publicnode.com"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://arbiscan.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0x2105","chainIdDecimal":8453,"chainName":"Base","rpcUrls":["https://mainnet.base.org","https://base.llamarpc.com","https://base-rpc.publicnode.com"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://basescan.org"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0xa86a","chainIdDecimal":43114,"chainName":"Avalanche C-Chain","rpcUrls":["https://api.avax.network/ext/bc/C/rpc","https://avalanche-c-chain-rpc.publicnode.com","https://1rpc.io/avax/c"],"nativeCurrency":{"name":"AVAX","symbol":"AVAX","decimals":18},"blockExplorerUrls":["https://snowtrace.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0xa4ec","chainIdDecimal":42220,"chainName":"Celo","rpcUrls":["https://forno.celo.org","https://celo-mainnet-rpc.publicnode.com","https://1rpc.io/celo"],"nativeCurrency":{"name":"CELO","symbol":"CELO","decimals":18},"blockExplorerUrls":["https://celoscan.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]},
|
||||
{"chainId":"0x457","chainIdDecimal":1111,"chainName":"Wemix","rpcUrls":["https://api.wemix.com","https://wemix-mainnet-rpc.publicnode.com"],"nativeCurrency":{"name":"WEMIX","symbol":"WEMIX","decimals":18},"blockExplorerUrls":["https://scan.wemix.com"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"]}
|
||||
{
|
||||
"chainId": "0x8a",
|
||||
"chainIdDecimal": 138,
|
||||
"chainName": "DeFi Oracle Meta Mainnet",
|
||||
"shortName": "dbis",
|
||||
"rpcUrls": ["https://rpc-http-pub.d-bis.org"],
|
||||
"nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18},
|
||||
"blockExplorerUrls": ["https://explorer.d-bis.org"],
|
||||
"infoURL": "https://d-bis.org",
|
||||
"explorerApiUrl": "https://explorer.d-bis.org/api/v2",
|
||||
"testnet": false,
|
||||
"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"
|
||||
],
|
||||
"oracles": [
|
||||
{"name": "ETH/USD (proxy)", "address": "0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6", "decimals": 8},
|
||||
{"name": "ETH/USD (aggregator)", "address": "0x99b3511a2d315a497c8112c1fdd8d508d4b1e506", "decimals": 8}
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": "0x1",
|
||||
"chainIdDecimal": 1,
|
||||
"chainName": "Ethereum Mainnet",
|
||||
"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/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"],
|
||||
"oracles": [{"name": "ETH/USD", "address": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", "decimals": 8}]
|
||||
},
|
||||
{"chainId": "0x9f2c4", "chainIdDecimal": 651940, "chainName": "ALL Mainnet", "rpcUrls": ["https://mainnet-rpc.alltra.global"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://alltra.global"], "iconUrls": ["https://alltra.global/favicon.ico"], "oracles": []},
|
||||
{"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"], "oracles": []},
|
||||
{"chainId": "0x38", "chainIdDecimal": 56, "chainName": "BNB Smart Chain", "rpcUrls": ["https://bsc-dataseed.binance.org", "https://bsc-dataseed1.defibit.io", "https://bsc-dataseed1.ninicoin.io"], "nativeCurrency": {"name": "BNB", "symbol": "BNB", "decimals": 18}, "blockExplorerUrls": ["https://bscscan.com"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/smartchain/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x64", "chainIdDecimal": 100, "chainName": "Gnosis Chain", "rpcUrls": ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com", "https://1rpc.io/gnosis"], "nativeCurrency": {"name": "xDAI", "symbol": "xDAI", "decimals": 18}, "blockExplorerUrls": ["https://gnosisscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/xdai/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x89", "chainIdDecimal": 137, "chainName": "Polygon", "rpcUrls": ["https://polygon-rpc.com", "https://polygon.llamarpc.com", "https://polygon-bor-rpc.publicnode.com"], "nativeCurrency": {"name": "MATIC", "symbol": "MATIC", "decimals": 18}, "blockExplorerUrls": ["https://polygonscan.com"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/polygon/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa", "chainIdDecimal": 10, "chainName": "Optimism", "rpcUrls": ["https://mainnet.optimism.io", "https://optimism.llamarpc.com", "https://optimism-rpc.publicnode.com"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://optimistic.etherscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa4b1", "chainIdDecimal": 42161, "chainName": "Arbitrum One", "rpcUrls": ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com", "https://arbitrum-one-rpc.publicnode.com"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://arbiscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrum/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x2105", "chainIdDecimal": 8453, "chainName": "Base", "rpcUrls": ["https://mainnet.base.org", "https://base.llamarpc.com", "https://base-rpc.publicnode.com"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://basescan.org"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/base/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa86a", "chainIdDecimal": 43114, "chainName": "Avalanche C-Chain", "rpcUrls": ["https://api.avax.network/ext/bc/C/rpc", "https://avalanche-c-chain-rpc.publicnode.com", "https://1rpc.io/avax/c"], "nativeCurrency": {"name": "AVAX", "symbol": "AVAX", "decimals": 18}, "blockExplorerUrls": ["https://snowtrace.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/avalanchec/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa4ec", "chainIdDecimal": 42220, "chainName": "Celo", "rpcUrls": ["https://forno.celo.org", "https://celo-mainnet-rpc.publicnode.com", "https://1rpc.io/celo"], "nativeCurrency": {"name": "CELO", "symbol": "CELO", "decimals": 18}, "blockExplorerUrls": ["https://celoscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/celo/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x457", "chainIdDecimal": 1111, "chainName": "Wemix", "rpcUrls": ["https://api.wemix.com", "https://wemix-mainnet-rpc.publicnode.com"], "nativeCurrency": {"name": "WEMIX", "symbol": "WEMIX", "decimals": 18}, "blockExplorerUrls": ["https://scan.wemix.com"], "iconUrls": ["https://scan.wemix.com/favicon.ico"], "oracles": []}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -485,7 +485,7 @@
|
||||
],
|
||||
"blockers": [
|
||||
"Desired public EVM targets still missing cW suites: Wemix.",
|
||||
"Wave 1 transport is still pending for: EUR, JPY, GBP, AUD, CAD, CHF, XAU.",
|
||||
"Wave 1 public-network activation is still pending for: EUR, JPY, GBP, AUD, CAD, CHF, XAU.",
|
||||
"Arbitrum bootstrap remains blocked on the current Mainnet hub leg: tx 0x97df657f0e31341ca852666766e553650531bbcc86621246d041985d7261bb07 reverted before any bridge event was emitted."
|
||||
],
|
||||
"resolutionMatrix": [
|
||||
@@ -540,7 +540,7 @@
|
||||
{
|
||||
"key": "wave1_transport_pending",
|
||||
"state": "open",
|
||||
"blocker": "Wave 1 transport is still pending for: EUR, JPY, GBP, AUD, CAD, CHF, XAU.",
|
||||
"blocker": "Wave 1 public-network activation is still pending for: EUR, JPY, GBP, AUD, CAD, CHF, XAU.",
|
||||
"targets": [
|
||||
{
|
||||
"code": "EUR",
|
||||
@@ -614,7 +614,7 @@
|
||||
],
|
||||
"resolution": [
|
||||
"Enable bridge controls and supervision policy for each Wave 1 canonical asset on Chain 138.",
|
||||
"Set max-outstanding / capacity controls, then promote the canonical symbols into config/gru-transport-active.json.",
|
||||
"Set max-outstanding / capacity controls, then promote the canonical symbols into the GRU public-network overlay.",
|
||||
"Verify the overlay promotion with check-gru-global-priority-rollout.sh and check-gru-v2-chain138-readiness.sh before attaching public liquidity."
|
||||
],
|
||||
"runbooks": [
|
||||
@@ -623,7 +623,7 @@
|
||||
"scripts/verify/check-gru-global-priority-rollout.sh",
|
||||
"scripts/verify/check-gru-v2-chain138-readiness.sh"
|
||||
],
|
||||
"exitCriteria": "Wave 1 transport pending count reaches zero and the overlay reports the seven non-USD assets as live_transport."
|
||||
"exitCriteria": "Wave 1 public-network pending count reaches zero and the overlay reports the seven non-USD assets as live cW public-network representations."
|
||||
},
|
||||
{
|
||||
"key": "first_tier_public_pools_not_live",
|
||||
@@ -801,9 +801,9 @@
|
||||
}
|
||||
],
|
||||
"resolution": [
|
||||
"Complete Wave 1 transport and first-tier public liquidity before promoting the remaining ranked assets.",
|
||||
"Complete Wave 1 public-network activation and first-tier public liquidity before promoting the remaining ranked assets.",
|
||||
"For each backlog asset, add canonical + wrapped symbols to the manifest/rollout plan, deploy contracts, and extend the public pool matrix.",
|
||||
"Promote each new asset through the same transport and public-liquidity gates used for Wave 1."
|
||||
"Promote each new asset through the same public-network and public-liquidity gates used for Wave 1."
|
||||
],
|
||||
"runbooks": [
|
||||
"config/gru-global-priority-currency-rollout.json",
|
||||
@@ -827,7 +827,7 @@
|
||||
"Completed in-repo: 13-asset Chain 138 → SPL target table (WETH + twelve c* → cW* symbols) in config/solana-gru-bridge-lineup.json and docs/03-deployment/CHAIN138_TO_SOLANA_GRU_TOKEN_DEPLOYMENT_LINEUP.md.",
|
||||
"Define and implement SPL mint authority / bridge program wiring; record solanaMint for each asset.",
|
||||
"Replace SolanaRelayService stub with production relay; mainnet-beta E2E both directions.",
|
||||
"Add dedicated verifier coverage and only then promote Solana into active transport inventory and public status surfaces."
|
||||
"Add dedicated verifier coverage and only then promote Solana into active public-network inventory and public status surfaces."
|
||||
],
|
||||
"runbooks": [
|
||||
"config/solana-gru-bridge-lineup.json",
|
||||
@@ -842,7 +842,7 @@
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"This queue is an operator/deployment planning surface. It does not mark queued pools or transports as live.",
|
||||
"This queue is an operator/deployment planning surface. It does not mark queued pools or public-network representations as live.",
|
||||
"Chain 138 canonical venues remain a separate live surface from the public cW mesh."
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
@@ -265,7 +265,7 @@
|
||||
"nextStep": "activate_transport_and_attach_public_liquidity"
|
||||
}
|
||||
],
|
||||
"note": "USD is the only live transport asset today. Wave 1 non-USD assets are deployed canonically on Chain 138 but are not yet promoted into the active transport overlay."
|
||||
"note": "USD is the only live cW public-network asset today. Wave 1 non-USD assets are deployed canonically on Chain 138 but are not yet promoted into the active public-network overlay."
|
||||
},
|
||||
"protocols": {
|
||||
"publicCwMesh": [
|
||||
|
||||
@@ -520,7 +520,7 @@ func (s *Server) HandleMissionControlBridgeTrace(w http.ResponseWriter, r *http.
|
||||
"from_registry": fromLabel,
|
||||
"to": toAddr,
|
||||
"to_registry": toLabel,
|
||||
"blockscout_url": publicBase + "/tx/" + strings.ToLower(tx),
|
||||
"blockscout_url": publicBase + "/transactions/" + strings.ToLower(tx),
|
||||
"source": source,
|
||||
}
|
||||
if registryLoadErr != nil && len(reg) == 0 {
|
||||
|
||||
@@ -177,7 +177,7 @@ func TestHandleMissionControlBridgeTraceLabelsFromRegistry(t *testing.T) {
|
||||
require.Equal(t, strings.ToLower(toAddr), out.Data["to"])
|
||||
require.Equal(t, "CHAIN138_SOURCE_BRIDGE", out.Data["from_registry"])
|
||||
require.Equal(t, "CHAIN138_DEST_BRIDGE", out.Data["to_registry"])
|
||||
require.Equal(t, "https://explorer.example.org/tx/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", out.Data["blockscout_url"])
|
||||
require.Equal(t, "https://explorer.example.org/transactions/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", out.Data["blockscout_url"])
|
||||
}
|
||||
|
||||
func TestHandleMissionControlBridgeTraceFallsBackToAddressInventoryLabels(t *testing.T) {
|
||||
|
||||
@@ -54,6 +54,8 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/auth/wallet", s.handleAuthWallet)
|
||||
mux.HandleFunc("/api/v1/auth/refresh", s.handleAuthRefresh)
|
||||
mux.HandleFunc("/api/v1/auth/logout", s.handleAuthLogout)
|
||||
mux.HandleFunc("/api/v1/walletconnect/", s.handleWalletConnectRoot)
|
||||
mux.HandleFunc("/api/v1/walletconnect", s.handleWalletConnectRoot)
|
||||
mux.HandleFunc("/api/v1/auth/register", s.handleAuthRegister)
|
||||
mux.HandleFunc("/api/v1/auth/login", s.handleAuthLogin)
|
||||
mux.HandleFunc("/api/v1/access/me", s.handleAccessMe)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/explorer/backend/api/freshness"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type explorerStats struct {
|
||||
@@ -22,6 +23,8 @@ type explorerStats struct {
|
||||
GasPrices *explorerGasPrices `json:"gas_prices,omitempty"`
|
||||
NetworkUtilizationPercentage *float64 `json:"network_utilization_percentage,omitempty"`
|
||||
TransactionsToday *int64 `json:"transactions_today,omitempty"`
|
||||
CoinPrice *string `json:"coin_price,omitempty"`
|
||||
CoinImage *string `json:"coin_image,omitempty"`
|
||||
Freshness freshness.Snapshot `json:"freshness"`
|
||||
Completeness freshness.SummaryCompleteness `json:"completeness"`
|
||||
Sampling freshness.Sampling `json:"sampling"`
|
||||
@@ -34,6 +37,14 @@ type explorerGasPrices struct {
|
||||
|
||||
type statsQueryFunc = freshness.QueryRowFunc
|
||||
|
||||
type statsErrorRow struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r statsErrorRow) Scan(dest ...any) error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func queryNullableFloat64(ctx context.Context, queryRow statsQueryFunc, query string, args ...any) (*float64, error) {
|
||||
var value sql.NullFloat64
|
||||
if err := queryRow(ctx, query, args...).Scan(&value); err != nil {
|
||||
@@ -191,25 +202,76 @@ func loadExplorerStats(ctx context.Context, chainID int, queryRow statsQueryFunc
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func loadExplorerStatsFallback(ctx context.Context, chainID int, cause error) explorerStats {
|
||||
rpcURL := strings.TrimSpace(os.Getenv("RPC_URL"))
|
||||
now := time.Now().UTC()
|
||||
queryErr := fmt.Errorf("blockscout database unavailable")
|
||||
if cause != nil {
|
||||
queryErr = cause
|
||||
}
|
||||
queryRow := func(context.Context, string, ...any) pgx.Row {
|
||||
return statsErrorRow{err: queryErr}
|
||||
}
|
||||
|
||||
snapshot, completeness, sampling, diagnostics, err := freshness.BuildSnapshot(
|
||||
ctx,
|
||||
chainID,
|
||||
queryRow,
|
||||
func(ctx context.Context) (*freshness.Reference, error) {
|
||||
return freshness.ProbeChainHead(ctx, rpcURL)
|
||||
},
|
||||
now,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
if sampling.Issues == nil {
|
||||
sampling.Issues = map[string]string{}
|
||||
}
|
||||
sampling.Issues["fallback_freshness"] = err.Error()
|
||||
}
|
||||
if sampling.Issues == nil {
|
||||
sampling.Issues = map[string]string{}
|
||||
}
|
||||
if cause != nil {
|
||||
sampling.Issues["stats_database"] = cause.Error()
|
||||
}
|
||||
|
||||
stats := explorerStats{
|
||||
Freshness: snapshot,
|
||||
Completeness: completeness,
|
||||
Sampling: sampling,
|
||||
Diagnostics: diagnostics,
|
||||
}
|
||||
if snapshot.ChainHead.BlockNumber != nil {
|
||||
stats.LatestBlock = *snapshot.ChainHead.BlockNumber
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
// handleStats handles GET /api/v2/stats
|
||||
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
if !s.requireDB(w) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
stats, err := loadExplorerStats(ctx, s.chainID, s.db.QueryRow)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "explorer stats are temporarily unavailable")
|
||||
return
|
||||
var stats explorerStats
|
||||
if s.db == nil {
|
||||
stats = loadExplorerStatsFallback(ctx, s.chainID, fmt.Errorf("database pool is not configured"))
|
||||
} else {
|
||||
var err error
|
||||
stats, err = loadExplorerStats(ctx, s.chainID, s.db.QueryRow)
|
||||
if err != nil {
|
||||
stats = loadExplorerStatsFallback(ctx, s.chainID, err)
|
||||
}
|
||||
}
|
||||
|
||||
enrichNativeCoinMarket(ctx, &stats)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(stats)
|
||||
}
|
||||
|
||||
150
backend/api/rest/stats_coin_price.go
Normal file
150
backend/api/rest/stats_coin_price.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
chain138WETHAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
||||
)
|
||||
|
||||
type blockscoutCoinStats struct {
|
||||
CoinPrice *string `json:"coin_price"`
|
||||
CoinImage *string `json:"coin_image"`
|
||||
}
|
||||
|
||||
type tokenAggregationTokenResponse struct {
|
||||
Token struct {
|
||||
Market struct {
|
||||
PriceUsd *float64 `json:"priceUsd"`
|
||||
} `json:"market"`
|
||||
} `json:"token"`
|
||||
}
|
||||
|
||||
func enrichNativeCoinMarket(ctx context.Context, stats *explorerStats) {
|
||||
if stats.CoinPrice != nil && strings.TrimSpace(*stats.CoinPrice) != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Prefer on-chain token-aggregation WETH/ETH oracle over Blockscout CoinGecko on Chain 138.
|
||||
if price := fetchTokenAggregationEthUsd(ctx); price != "" {
|
||||
stats.CoinPrice = &price
|
||||
if image := fetchBlockscoutCoinImage(ctx); image != "" {
|
||||
stats.CoinImage = &image
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
bsURL := strings.TrimSpace(os.Getenv("BLOCKSCOUT_STATS_URL"))
|
||||
if bsURL == "" {
|
||||
bsURL = "http://127.0.0.1:4000/api/v2/stats"
|
||||
}
|
||||
if price, image := fetchBlockscoutCoinMarket(ctx, bsURL); price != "" {
|
||||
stats.CoinPrice = &price
|
||||
if image != "" {
|
||||
stats.CoinImage = &image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchBlockscoutCoinImage(ctx context.Context) string {
|
||||
bsURL := strings.TrimSpace(os.Getenv("BLOCKSCOUT_STATS_URL"))
|
||||
if bsURL == "" {
|
||||
bsURL = "http://127.0.0.1:4000/api/v2/stats"
|
||||
}
|
||||
_, image := fetchBlockscoutCoinMarket(ctx, bsURL)
|
||||
return image
|
||||
}
|
||||
|
||||
func fetchBlockscoutCoinMarket(ctx context.Context, url string) (price string, image string) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 4 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
var payload blockscoutCoinStats
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return "", ""
|
||||
}
|
||||
if payload.CoinPrice != nil {
|
||||
price = strings.TrimSpace(*payload.CoinPrice)
|
||||
}
|
||||
if payload.CoinImage != nil {
|
||||
image = strings.TrimSpace(*payload.CoinImage)
|
||||
}
|
||||
return price, image
|
||||
}
|
||||
|
||||
func tokenAggregationAPIV1Base() string {
|
||||
base := strings.TrimRight(strings.TrimSpace(firstNonEmptyEnv(
|
||||
"TOKEN_AGGREGATION_API_BASE",
|
||||
"TOKEN_AGGREGATION_URL",
|
||||
"TOKEN_AGGREGATION_BASE_URL",
|
||||
)), "/")
|
||||
if base == "" {
|
||||
return "http://127.0.0.1:3001/api/v1"
|
||||
}
|
||||
if strings.HasSuffix(base, "/api/v1") {
|
||||
return base
|
||||
}
|
||||
return base + "/api/v1"
|
||||
}
|
||||
|
||||
func fetchTokenAggregationEthUsd(ctx context.Context) string {
|
||||
url := fmt.Sprintf("%s/tokens/%s?chainId=138", tokenAggregationAPIV1Base(), chain138WETHAddress)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ""
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var payload tokenAggregationTokenResponse
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
if payload.Token.Market.PriceUsd == nil || *payload.Token.Market.PriceUsd <= 0 {
|
||||
return ""
|
||||
}
|
||||
return formatUsdPrice(*payload.Token.Market.PriceUsd)
|
||||
}
|
||||
|
||||
func formatUsdPrice(value float64) string {
|
||||
return fmt.Sprintf("%.2f", value)
|
||||
}
|
||||
133
backend/api/rest/stats_coin_price_test.go
Normal file
133
backend/api/rest/stats_coin_price_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEnrichNativeCoinMarketFromBlockscout(t *testing.T) {
|
||||
tokenAgg := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer tokenAgg.Close()
|
||||
t.Setenv("TOKEN_AGGREGATION_BASE_URL", tokenAgg.URL)
|
||||
|
||||
blockscout := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
|
||||
"coin_price": "1701.77",
|
||||
"coin_image": "https://example.com/eth.png",
|
||||
}))
|
||||
}))
|
||||
defer blockscout.Close()
|
||||
|
||||
t.Setenv("BLOCKSCOUT_STATS_URL", blockscout.URL)
|
||||
stats := explorerStats{}
|
||||
enrichNativeCoinMarket(context.Background(), &stats)
|
||||
|
||||
require.NotNil(t, stats.CoinPrice)
|
||||
require.Equal(t, "1701.77", *stats.CoinPrice)
|
||||
require.NotNil(t, stats.CoinImage)
|
||||
require.Equal(t, "https://example.com/eth.png", *stats.CoinImage)
|
||||
}
|
||||
|
||||
func TestEnrichNativeCoinMarketFallsBackToTokenAggregation(t *testing.T) {
|
||||
blockscout := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
|
||||
"coin_price": nil,
|
||||
}))
|
||||
}))
|
||||
defer blockscout.Close()
|
||||
|
||||
tokenAgg := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(t, r.URL.Path, chain138WETHAddress)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
|
||||
"token": map[string]any{
|
||||
"market": map[string]any{
|
||||
"priceUsd": 1678.67,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}))
|
||||
defer tokenAgg.Close()
|
||||
|
||||
t.Setenv("BLOCKSCOUT_STATS_URL", blockscout.URL)
|
||||
t.Setenv("TOKEN_AGGREGATION_BASE_URL", tokenAgg.URL)
|
||||
stats := explorerStats{}
|
||||
enrichNativeCoinMarket(context.Background(), &stats)
|
||||
|
||||
require.NotNil(t, stats.CoinPrice)
|
||||
require.Equal(t, "1678.67", *stats.CoinPrice)
|
||||
}
|
||||
|
||||
func TestEnrichNativeCoinMarketPrefersTokenAggregationWithHostOnlyBaseURL(t *testing.T) {
|
||||
blockscout := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
|
||||
"coin_price": "1740.00",
|
||||
}))
|
||||
}))
|
||||
defer blockscout.Close()
|
||||
|
||||
tokenAgg := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/api/v1/tokens/"+chain138WETHAddress, r.URL.Path)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
|
||||
"token": map[string]any{
|
||||
"market": map[string]any{
|
||||
"priceUsd": 1732.18,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}))
|
||||
defer tokenAgg.Close()
|
||||
|
||||
t.Setenv("BLOCKSCOUT_STATS_URL", blockscout.URL)
|
||||
t.Setenv("TOKEN_AGGREGATION_BASE_URL", tokenAgg.URL)
|
||||
stats := explorerStats{}
|
||||
enrichNativeCoinMarket(context.Background(), &stats)
|
||||
|
||||
require.NotNil(t, stats.CoinPrice)
|
||||
require.Equal(t, "1732.18", *stats.CoinPrice)
|
||||
}
|
||||
|
||||
func TestEnrichNativeCoinMarketPrefersTokenAggregationOverBlockscout(t *testing.T) {
|
||||
blockscout := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
|
||||
"coin_price": "1740.00",
|
||||
"coin_image": "https://example.com/eth.png",
|
||||
}))
|
||||
}))
|
||||
defer blockscout.Close()
|
||||
|
||||
tokenAgg := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(t, r.URL.Path, chain138WETHAddress)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
|
||||
"token": map[string]any{
|
||||
"market": map[string]any{
|
||||
"priceUsd": 1732.18,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}))
|
||||
defer tokenAgg.Close()
|
||||
|
||||
t.Setenv("BLOCKSCOUT_STATS_URL", blockscout.URL)
|
||||
t.Setenv("TOKEN_AGGREGATION_BASE_URL", tokenAgg.URL)
|
||||
stats := explorerStats{}
|
||||
enrichNativeCoinMarket(context.Background(), &stats)
|
||||
|
||||
require.NotNil(t, stats.CoinPrice)
|
||||
require.Equal(t, "1732.18", *stats.CoinPrice)
|
||||
require.NotNil(t, stats.CoinImage)
|
||||
require.Equal(t, "https://example.com/eth.png", *stats.CoinImage)
|
||||
}
|
||||
@@ -136,3 +136,33 @@ func TestLoadExplorerStatsReturnsErrorWhenQueryFails(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "query total transactions")
|
||||
}
|
||||
|
||||
func TestLoadExplorerStatsFallbackUsesRPCHead(t *testing.T) {
|
||||
rpc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch req.Method {
|
||||
case "eth_blockNumber":
|
||||
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x4d2"}`))
|
||||
case "eth_getBlockByNumber":
|
||||
ts := time.Now().Add(-3 * time.Second).Unix()
|
||||
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"timestamp":"0x` + strconv.FormatInt(ts, 16) + `"}}`))
|
||||
default:
|
||||
http.Error(w, `{"jsonrpc":"2.0","id":1,"error":{"message":"unsupported"}}`, http.StatusBadRequest)
|
||||
}
|
||||
}))
|
||||
defer rpc.Close()
|
||||
t.Setenv("RPC_URL", rpc.URL)
|
||||
|
||||
stats := loadExplorerStatsFallback(context.Background(), 138, errors.New("database down"))
|
||||
|
||||
require.Equal(t, int64(1234), stats.LatestBlock)
|
||||
require.NotNil(t, stats.Freshness.ChainHead.BlockNumber)
|
||||
require.Equal(t, int64(1234), *stats.Freshness.ChainHead.BlockNumber)
|
||||
require.Equal(t, freshness.CompletenessUnavailable, stats.Completeness.TransactionsFeed)
|
||||
require.Contains(t, stats.Sampling.Issues, "stats_database")
|
||||
require.Contains(t, stats.Sampling.Issues["latest_indexed_block"], "database down")
|
||||
}
|
||||
|
||||
136
backend/api/rest/walletconnect.go
Normal file
136
backend/api/rest/walletconnect.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/explorer/backend/wallet"
|
||||
)
|
||||
|
||||
func (s *Server) walletConnectHandler() *wallet.WalletConnect {
|
||||
return wallet.NewWalletConnect(s.chainID)
|
||||
}
|
||||
|
||||
// handleWalletConnectConfig handles GET /api/v1/walletconnect/config
|
||||
func (s *Server) handleWalletConnectConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, s.walletConnectHandler().PublicConfig())
|
||||
}
|
||||
|
||||
// handleWalletConnectMetadata handles GET /api/v1/walletconnect/metadata
|
||||
func (s *Server) handleWalletConnectMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"name": "DBIS Explorer",
|
||||
"description": "Chain 138 explorer by DBIS",
|
||||
"url": "https://explorer.d-bis.org",
|
||||
"icons": []string{"https://explorer.d-bis.org/favicon.ico"},
|
||||
})
|
||||
}
|
||||
|
||||
// handleWalletConnectConnect handles POST /api/v1/walletconnect/connect
|
||||
func (s *Server) handleWalletConnectConnect(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := s.walletConnectHandler().Connect(r.Context())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotImplemented, response)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// handleWalletConnectSessionRegister handles POST /api/v1/walletconnect/session
|
||||
func (s *Server) handleWalletConnectSessionRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
Address string `json:"address"`
|
||||
ChainID int `json:"chainId"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
session, err := s.walletConnectHandler().RegisterSession(r.Context(), req.SessionID, req.Address, req.ChainID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, session)
|
||||
}
|
||||
|
||||
// handleWalletConnectSession handles GET /api/v1/walletconnect/session/{id}
|
||||
func (s *Server) handleWalletConnectSession(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := strings.TrimPrefix(r.URL.Path, "/api/v1/walletconnect/session/")
|
||||
sessionID = strings.Trim(sessionID, "/")
|
||||
if sessionID == "" || strings.Contains(sessionID, "/") {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "session id is required")
|
||||
return
|
||||
}
|
||||
|
||||
session, err := s.walletConnectHandler().GetSession(r.Context(), sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, session)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, session)
|
||||
}
|
||||
|
||||
// handleWalletConnectRoot dispatches walletconnect subroutes.
|
||||
func (s *Server) handleWalletConnectRoot(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/walletconnect")
|
||||
path = strings.Trim(path, "/")
|
||||
|
||||
switch path {
|
||||
case "config":
|
||||
s.handleWalletConnectConfig(w, r)
|
||||
case "metadata":
|
||||
s.handleWalletConnectMetadata(w, r)
|
||||
case "connect":
|
||||
s.handleWalletConnectConnect(w, r)
|
||||
case "session":
|
||||
s.handleWalletConnectSessionRegister(w, r)
|
||||
case "":
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"endpoints": []string{
|
||||
"/api/v1/walletconnect/config",
|
||||
"/api/v1/walletconnect/metadata",
|
||||
"/api/v1/walletconnect/connect",
|
||||
"POST /api/v1/walletconnect/session",
|
||||
"/api/v1/walletconnect/session/{sessionId}",
|
||||
},
|
||||
"fallbackAuth": "/api/v1/auth/wallet",
|
||||
})
|
||||
default:
|
||||
if strings.HasPrefix(path, "session/") {
|
||||
s.handleWalletConnectSession(w, r)
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusNotFound, "not_found", "walletconnect route not found")
|
||||
}
|
||||
}
|
||||
113
backend/api/rest/walletconnect_internal_test.go
Normal file
113
backend/api/rest/walletconnect_internal_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleWalletConnectConfig(t *testing.T) {
|
||||
t.Setenv("WALLETCONNECT_PROJECT_ID", "test-project-id")
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect/config", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.handleWalletConnectConfig(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload["projectId"] != "test-project-id" {
|
||||
t.Fatalf("expected project id, got %#v", payload["projectId"])
|
||||
}
|
||||
if payload["fallbackAuth"] != "/api/v1/auth/wallet" {
|
||||
t.Fatalf("expected fallback auth path, got %#v", payload["fallbackAuth"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectConnectDisabled(t *testing.T) {
|
||||
t.Setenv("WALLETCONNECT_PROJECT_ID", "")
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/walletconnect/connect", strings.NewReader("{}"))
|
||||
rec := httptest.NewRecorder()
|
||||
server.handleWalletConnectConnect(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotImplemented {
|
||||
t.Fatalf("expected 501, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectConnectClientMode(t *testing.T) {
|
||||
t.Setenv("WALLETCONNECT_PROJECT_ID", "test-project-id")
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/walletconnect/connect", strings.NewReader("{}"))
|
||||
rec := httptest.NewRecorder()
|
||||
server.handleWalletConnectConnect(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload["status"] != "client" {
|
||||
t.Fatalf("expected client status, got %#v", payload["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectSessionMissing(t *testing.T) {
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect/session/demo-session", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.handleWalletConnectSession(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectSessionRegister(t *testing.T) {
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
body := strings.NewReader(`{"sessionId":"wc-demo","address":"0x4A666F96fC8764181194447A7dFdb7d471b301C8","chainId":138}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/walletconnect/session", body)
|
||||
rec := httptest.NewRecorder()
|
||||
server.handleWalletConnectSessionRegister(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect/session/wc-demo", nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
server.handleWalletConnectSession(getRec, getReq)
|
||||
if getRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected lookup 200, got %d", getRec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectRootIndex(t *testing.T) {
|
||||
_ = os.Setenv("WALLETCONNECT_PROJECT_ID", "")
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.handleWalletConnectRoot(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
255
backend/api/track1/bridge_lanes.go
Normal file
255
backend/api/track1/bridge_lanes.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package track1
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed bridge_lanes_default.json
|
||||
var defaultBridgeLanesJSON []byte
|
||||
|
||||
type bridgeLaneDefinition struct {
|
||||
Key string `json:"key"`
|
||||
ChainName string `json:"chain_name"`
|
||||
ChainID int64 `json:"chain_id"`
|
||||
ConfigReady bool `json:"config_ready"`
|
||||
RPCEnvs []string `json:"rpc_envs"`
|
||||
RPCDefault string `json:"rpc_default"`
|
||||
LinkToken string `json:"link_token"`
|
||||
WETH9Bridge string `json:"weth9_bridge"`
|
||||
WETH10Bridge string `json:"weth10_bridge"`
|
||||
}
|
||||
|
||||
type bridgeLanesConfig struct {
|
||||
Updated string `json:"updated"`
|
||||
MinLinkWei string `json:"min_link_wei"`
|
||||
Lanes []bridgeLaneDefinition `json:"lanes"`
|
||||
}
|
||||
|
||||
func loadBridgeLanesConfig() bridgeLanesConfig {
|
||||
path := strings.TrimSpace(os.Getenv("MISSION_CONTROL_BRIDGE_LANES_JSON"))
|
||||
if path != "" {
|
||||
if b, err := os.ReadFile(path); err == nil && len(b) > 0 {
|
||||
var cfg bridgeLanesConfig
|
||||
if json.Unmarshal(b, &cfg) == nil && len(cfg.Lanes) > 0 {
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
var cfg bridgeLanesConfig
|
||||
_ = json.Unmarshal(defaultBridgeLanesJSON, &cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func resolveLaneRPC(def bridgeLaneDefinition) string {
|
||||
for _, key := range def.RPCEnvs {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
for _, row := range ParseExtraRPCProbes() {
|
||||
chainKey := row[2]
|
||||
if chainKey == strconv.FormatInt(def.ChainID, 10) {
|
||||
return row[1]
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(def.RPCDefault)
|
||||
}
|
||||
|
||||
func erc20BalanceOf(ctx context.Context, rpcURL, tokenAddress, holderAddress string) (string, error) {
|
||||
tokenAddress = strings.ToLower(strings.TrimSpace(tokenAddress))
|
||||
holderAddress = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(holderAddress), "0x"))
|
||||
if len(holderAddress) != 40 {
|
||||
return "0", nil
|
||||
}
|
||||
data := "0x70a08231" + strings.Repeat("0", 24) + holderAddress
|
||||
raw, _, err := postJSONRPC(ctx, bridgeLaneHTTPClient(), rpcURL, "eth_call", []interface{}{
|
||||
map[string]interface{}{
|
||||
"to": tokenAddress,
|
||||
"data": data,
|
||||
},
|
||||
"latest",
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var hex string
|
||||
if err := json.Unmarshal(raw, &hex); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hex = strings.TrimSpace(hex)
|
||||
if hex == "" || hex == "0x" {
|
||||
return "0", nil
|
||||
}
|
||||
value := new(big.Int)
|
||||
if _, ok := value.SetString(strings.TrimPrefix(hex, "0x"), 16); !ok {
|
||||
return "0", nil
|
||||
}
|
||||
return value.String(), nil
|
||||
}
|
||||
|
||||
func bridgeLaneHTTPClient() *http.Client {
|
||||
return &http.Client{Timeout: 6 * time.Second}
|
||||
}
|
||||
|
||||
func bridgeFundingStatus(linkBalanceWei, minLinkWei string) string {
|
||||
balance, okBalance := new(big.Int).SetString(strings.TrimSpace(linkBalanceWei), 10)
|
||||
minimum, okMin := new(big.Int).SetString(strings.TrimSpace(minLinkWei), 10)
|
||||
if !okBalance || !okMin {
|
||||
return "unknown"
|
||||
}
|
||||
if balance.Cmp(minimum) >= 0 {
|
||||
return "funded"
|
||||
}
|
||||
if balance.Sign() > 0 {
|
||||
return "degraded"
|
||||
}
|
||||
return "unfunded"
|
||||
}
|
||||
|
||||
func proofStatusForLane(key string, proofs map[string]interface{}) string {
|
||||
if proofs == nil {
|
||||
return "proof-pending"
|
||||
}
|
||||
laneProofs, ok := proofs[key].([]interface{})
|
||||
if !ok || len(laneProofs) == 0 {
|
||||
return "proof-pending"
|
||||
}
|
||||
for _, item := range laneProofs {
|
||||
row, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if tx, ok := row["tx_hash"].(string); ok && strings.TrimSpace(tx) != "" {
|
||||
return "proof-recorded"
|
||||
}
|
||||
}
|
||||
return "proof-pending"
|
||||
}
|
||||
|
||||
func readProofTransfersJSON() map[string]interface{} {
|
||||
path := strings.TrimSpace(os.Getenv("MISSION_CONTROL_PROOF_TRANSFERS_JSON"))
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil || len(b) == 0 {
|
||||
return map[string]interface{}{"error": "unreadable or empty", "path": path}
|
||||
}
|
||||
if len(b) > 512*1024 {
|
||||
return map[string]interface{}{"error": "file too large", "path": path}
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(b, &payload); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "path": path}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func probeBridgeContract(ctx context.Context, rpcURL, linkToken, bridgeAddress, minLinkWei string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"bridge": strings.TrimSpace(bridgeAddress),
|
||||
}
|
||||
if rpcURL == "" {
|
||||
result["status"] = "unknown"
|
||||
result["error"] = "rpc unavailable"
|
||||
return result
|
||||
}
|
||||
if linkToken == "" || bridgeAddress == "" {
|
||||
result["status"] = "unknown"
|
||||
result["error"] = "missing link token or bridge address"
|
||||
return result
|
||||
}
|
||||
balance, err := erc20BalanceOf(ctx, rpcURL, linkToken, bridgeAddress)
|
||||
if err != nil {
|
||||
result["status"] = "unknown"
|
||||
result["error"] = err.Error()
|
||||
return result
|
||||
}
|
||||
result["link_balance_wei"] = balance
|
||||
result["status"] = bridgeFundingStatus(balance, minLinkWei)
|
||||
return result
|
||||
}
|
||||
|
||||
func aggregateLaneStatus(weth9Status, weth10Status, proofStatus string) string {
|
||||
statuses := []string{weth9Status, weth10Status}
|
||||
hasUnfunded := false
|
||||
hasDegraded := false
|
||||
hasUnknown := false
|
||||
for _, status := range statuses {
|
||||
switch status {
|
||||
case "unfunded":
|
||||
hasUnfunded = true
|
||||
case "degraded":
|
||||
hasDegraded = true
|
||||
case "unknown":
|
||||
hasUnknown = true
|
||||
}
|
||||
}
|
||||
if hasUnfunded {
|
||||
return "unfunded"
|
||||
}
|
||||
if hasDegraded {
|
||||
return "degraded"
|
||||
}
|
||||
if hasUnknown {
|
||||
return "unknown"
|
||||
}
|
||||
if proofStatus == "proof-pending" {
|
||||
return "proof-pending"
|
||||
}
|
||||
return "funded"
|
||||
}
|
||||
|
||||
func BuildBridgeLaneHealth(ctx context.Context) (map[string]interface{}, map[string]interface{}) {
|
||||
cfg := loadBridgeLanesConfig()
|
||||
minLinkWei := strings.TrimSpace(cfg.MinLinkWei)
|
||||
if minLinkWei == "" {
|
||||
minLinkWei = "1000000000000000000"
|
||||
}
|
||||
|
||||
proofPayload := readProofTransfersJSON()
|
||||
proofByLane := map[string]interface{}{}
|
||||
if proofPayload != nil {
|
||||
if lanes, ok := proofPayload["lanes"].(map[string]interface{}); ok {
|
||||
proofByLane = lanes
|
||||
}
|
||||
}
|
||||
|
||||
lanes := make([]map[string]interface{}, 0, len(cfg.Lanes))
|
||||
for _, def := range cfg.Lanes {
|
||||
rpcURL := resolveLaneRPC(def)
|
||||
weth9 := probeBridgeContract(ctx, rpcURL, def.LinkToken, def.WETH9Bridge, minLinkWei)
|
||||
weth10 := probeBridgeContract(ctx, rpcURL, def.LinkToken, def.WETH10Bridge, minLinkWei)
|
||||
weth9Status, _ := weth9["status"].(string)
|
||||
weth10Status, _ := weth10["status"].(string)
|
||||
proofStatus := proofStatusForLane(def.Key, proofByLane)
|
||||
|
||||
lanes = append(lanes, map[string]interface{}{
|
||||
"key": def.Key,
|
||||
"chain_name": def.ChainName,
|
||||
"chain_id": def.ChainID,
|
||||
"config_ready": def.ConfigReady,
|
||||
"link_token": def.LinkToken,
|
||||
"status": aggregateLaneStatus(weth9Status, weth10Status, proofStatus),
|
||||
"proof_status": proofStatus,
|
||||
"weth9": weth9,
|
||||
"weth10": weth10,
|
||||
"rpc_endpoint": redactRPCOrigin(rpcURL),
|
||||
})
|
||||
}
|
||||
|
||||
laneHealth := map[string]interface{}{
|
||||
"updated_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"min_link_wei": minLinkWei,
|
||||
"lanes": lanes,
|
||||
}
|
||||
return laneHealth, proofPayload
|
||||
}
|
||||
61
backend/api/track1/bridge_lanes_default.json
Normal file
61
backend/api/track1/bridge_lanes_default.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"updated": "2026-05-23",
|
||||
"min_link_wei": "1000000000000000000",
|
||||
"lanes": [
|
||||
{
|
||||
"key": "chain138",
|
||||
"chain_name": "Defi Oracle Meta Mainnet (138)",
|
||||
"chain_id": 138,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["RPC_URL", "RPC_URL_138"],
|
||||
"rpc_default": "http://192.168.11.211:8545",
|
||||
"link_token": "0xb7721dd53a8c629d9f1ba31a5819afe250002b03",
|
||||
"weth9_bridge": "0xcacfd227A040002e49e2e01626363071324f820a",
|
||||
"weth10_bridge": "0xe0E93247376aa097dB308B92e6Ba36bA015535D0"
|
||||
},
|
||||
{
|
||||
"key": "gnosis",
|
||||
"chain_name": "Gnosis (100)",
|
||||
"chain_id": 100,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["GNOSIS_RPC", "GNOSIS_MAINNET_RPC", "GNOSIS_RPC_URL"],
|
||||
"rpc_default": "https://rpc.gnosischain.com",
|
||||
"link_token": "0xE2e73A1c69ecF83F464EFCE6A5be353a37cA09b2",
|
||||
"weth9_bridge": "0xc8656F24488cb90c452058da92d1a25BA464eaAE",
|
||||
"weth10_bridge": "0xa846aeAD3071df1b6439d5D813156aCE7C2c1DA1"
|
||||
},
|
||||
{
|
||||
"key": "cronos",
|
||||
"chain_name": "Cronos (25)",
|
||||
"chain_id": 25,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CRONOS_RPC", "CRONOS_RPC_URL", "CRONOS_MAINNET_RPC"],
|
||||
"rpc_default": "https://evm.cronos.org",
|
||||
"link_token": "0x8c80A01F461f297Df7F9DA3A4f740D7297C8Ac85",
|
||||
"weth9_bridge": "0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab",
|
||||
"weth10_bridge": "0x105F8A15b819948a89153505762444Ee9f324684"
|
||||
},
|
||||
{
|
||||
"key": "celo",
|
||||
"chain_name": "Celo (42220)",
|
||||
"chain_id": 42220,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CELO_RPC", "CELO_MAINNET_RPC"],
|
||||
"rpc_default": "https://forno.celo.org",
|
||||
"link_token": "0xd07294e6E917e07dfDcee882dd1e2565085C2ae0",
|
||||
"weth9_bridge": "0xAb57BF30F1354CA0590af22D8974c7f24DB2DbD7",
|
||||
"weth10_bridge": "0xa780ef19A041745d353c9432f2a7f5A241335ffE"
|
||||
},
|
||||
{
|
||||
"key": "wemix",
|
||||
"chain_name": "Wemix (1111)",
|
||||
"chain_id": 1111,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["WEMIX_RPC", "WEMIX_MAINNET_RPC"],
|
||||
"rpc_default": "https://api.wemix.com",
|
||||
"link_token": "0x80f1FcdC96B55e459BF52b998aBBE2c364935d69",
|
||||
"weth9_bridge": "0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04",
|
||||
"weth10_bridge": "0xa4B9DD039565AeD9641D45b57061f99d9cA6Df08"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
backend/api/track1/bridge_lanes_test.go
Normal file
37
backend/api/track1/bridge_lanes_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package track1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridgeFundingStatus(t *testing.T) {
|
||||
require.Equal(t, "funded", bridgeFundingStatus("2000000000000000000", "1000000000000000000"))
|
||||
require.Equal(t, "degraded", bridgeFundingStatus("500000000000000000", "1000000000000000000"))
|
||||
require.Equal(t, "unfunded", bridgeFundingStatus("0", "1000000000000000000"))
|
||||
}
|
||||
|
||||
func TestAggregateLaneStatus(t *testing.T) {
|
||||
require.Equal(t, "unfunded", aggregateLaneStatus("unfunded", "funded", "proof-recorded"))
|
||||
require.Equal(t, "degraded", aggregateLaneStatus("degraded", "funded", "proof-recorded"))
|
||||
require.Equal(t, "proof-pending", aggregateLaneStatus("funded", "funded", "proof-pending"))
|
||||
require.Equal(t, "funded", aggregateLaneStatus("funded", "funded", "proof-recorded"))
|
||||
}
|
||||
|
||||
func TestProofStatusForLane(t *testing.T) {
|
||||
proofs := map[string]interface{}{
|
||||
"gnosis": []interface{}{
|
||||
map[string]interface{}{"tx_hash": "0xabc"},
|
||||
},
|
||||
}
|
||||
require.Equal(t, "proof-recorded", proofStatusForLane("gnosis", proofs))
|
||||
require.Equal(t, "proof-pending", proofStatusForLane("cronos", proofs))
|
||||
}
|
||||
|
||||
func TestLoadBridgeLanesConfigDefault(t *testing.T) {
|
||||
t.Setenv("MISSION_CONTROL_BRIDGE_LANES_JSON", "")
|
||||
cfg := loadBridgeLanesConfig()
|
||||
require.NotEmpty(t, cfg.Lanes)
|
||||
require.NotEmpty(t, cfg.MinLinkWei)
|
||||
}
|
||||
78
backend/api/track1/bridge_mode.go
Normal file
78
backend/api/track1/bridge_mode.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package track1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/explorer/backend/api/freshness"
|
||||
)
|
||||
|
||||
type bridgeDeliveryMode struct {
|
||||
Kind string
|
||||
Reason any
|
||||
Scope any
|
||||
}
|
||||
|
||||
func resolveBridgeDeliveryMode(hasRelays bool, diagnostics *freshness.Diagnostics, txFeed freshness.Completeness) bridgeDeliveryMode {
|
||||
if !hasRelays {
|
||||
if diagnostics != nil && isStaleTransactionVisibility(diagnostics) {
|
||||
return bridgeDeliveryMode{
|
||||
Kind: "mixed",
|
||||
Reason: "partial_observability_inputs",
|
||||
Scope: "homepage_summary_only",
|
||||
}
|
||||
}
|
||||
return bridgeDeliveryMode{
|
||||
Kind: "live",
|
||||
Reason: nil,
|
||||
Scope: nil,
|
||||
}
|
||||
}
|
||||
|
||||
if diagnostics != nil && isStaleTransactionVisibility(diagnostics) {
|
||||
return bridgeDeliveryMode{
|
||||
Kind: "mixed",
|
||||
Reason: "relay_snapshot_only_source",
|
||||
Scope: "bridge_monitoring_and_homepage",
|
||||
}
|
||||
}
|
||||
|
||||
if txFeed == freshness.CompletenessPartial || txFeed == freshness.CompletenessStale {
|
||||
return bridgeDeliveryMode{
|
||||
Kind: "mixed",
|
||||
Reason: "partial_observability_inputs",
|
||||
Scope: "bridge_monitoring_and_homepage",
|
||||
}
|
||||
}
|
||||
|
||||
return bridgeDeliveryMode{
|
||||
Kind: "snapshot",
|
||||
Reason: "live_homepage_stream_not_attached",
|
||||
Scope: "relay_monitoring_homepage_card_only",
|
||||
}
|
||||
}
|
||||
|
||||
func isStaleTransactionVisibility(diagnostics *freshness.Diagnostics) bool {
|
||||
if diagnostics == nil {
|
||||
return false
|
||||
}
|
||||
state := strings.ToLower(strings.TrimSpace(diagnostics.ActivityState))
|
||||
switch state {
|
||||
case "fresh_head_stale_transaction_visibility", "lagging", "stale_transaction_visibility":
|
||||
return true
|
||||
default:
|
||||
return strings.Contains(state, "stale") && strings.Contains(state, "transaction")
|
||||
}
|
||||
}
|
||||
|
||||
func buildBridgeModePayload(now string, resolved bridgeDeliveryMode) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"kind": resolved.Kind,
|
||||
"updated_at": now,
|
||||
"age_seconds": int64(0),
|
||||
"reason": resolved.Reason,
|
||||
"scope": resolved.Scope,
|
||||
"source": freshness.SourceReported,
|
||||
"confidence": freshness.ConfidenceHigh,
|
||||
"provenance": freshness.ProvenanceMissionFeed,
|
||||
}
|
||||
}
|
||||
44
backend/api/track1/bridge_mode_test.go
Normal file
44
backend/api/track1/bridge_mode_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package track1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/explorer/backend/api/freshness"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveBridgeDeliveryModeLiveWithoutRelays(t *testing.T) {
|
||||
got := resolveBridgeDeliveryMode(false, nil, freshness.CompletenessComplete)
|
||||
require.Equal(t, "live", got.Kind)
|
||||
require.Nil(t, got.Reason)
|
||||
}
|
||||
|
||||
func TestResolveBridgeDeliveryModeSnapshotWithRelays(t *testing.T) {
|
||||
got := resolveBridgeDeliveryMode(true, nil, freshness.CompletenessComplete)
|
||||
require.Equal(t, "snapshot", got.Kind)
|
||||
require.Equal(t, "live_homepage_stream_not_attached", got.Reason)
|
||||
require.Equal(t, "relay_monitoring_homepage_card_only", got.Scope)
|
||||
}
|
||||
|
||||
func TestResolveBridgeDeliveryModeMixedWhenTransactionVisibilityStale(t *testing.T) {
|
||||
diagnostics := &freshness.Diagnostics{
|
||||
ActivityState: "fresh_head_stale_transaction_visibility",
|
||||
}
|
||||
got := resolveBridgeDeliveryMode(true, diagnostics, freshness.CompletenessPartial)
|
||||
require.Equal(t, "mixed", got.Kind)
|
||||
require.Equal(t, "relay_snapshot_only_source", got.Reason)
|
||||
require.Equal(t, "bridge_monitoring_and_homepage", got.Scope)
|
||||
}
|
||||
|
||||
func TestResolveBridgeDeliveryModeMixedWhenQuietChain(t *testing.T) {
|
||||
diagnostics := &freshness.Diagnostics{
|
||||
ActivityState: "quiet_chain",
|
||||
}
|
||||
got := resolveBridgeDeliveryMode(false, diagnostics, freshness.CompletenessComplete)
|
||||
require.Equal(t, "live", got.Kind)
|
||||
}
|
||||
|
||||
func TestIsStaleTransactionVisibility(t *testing.T) {
|
||||
require.True(t, isStaleTransactionVisibility(&freshness.Diagnostics{ActivityState: "fresh_head_stale_transaction_visibility"}))
|
||||
require.False(t, isStaleTransactionVisibility(&freshness.Diagnostics{ActivityState: "healthy"}))
|
||||
}
|
||||
@@ -133,6 +133,8 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
|
||||
}
|
||||
if s.freshnessLoader != nil {
|
||||
if snapshot, completeness, sampling, diagnostics, err := s.freshnessLoader(ctx); err == nil && snapshot != nil {
|
||||
txFeed := completeness.TransactionsFeed
|
||||
resolvedMode := resolveBridgeDeliveryMode(false, diagnostics, txFeed)
|
||||
subsystems := map[string]interface{}{
|
||||
"rpc_head": map[string]interface{}{
|
||||
"status": chainStatusFromProbe(p138),
|
||||
@@ -174,39 +176,13 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
|
||||
"issues": sampling.Issues,
|
||||
}
|
||||
}
|
||||
modeKind := "live"
|
||||
modeReason := any(nil)
|
||||
modeScope := any(nil)
|
||||
if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
|
||||
modeKind = "snapshot"
|
||||
modeReason = "live_homepage_stream_not_attached"
|
||||
modeScope = "relay_monitoring_homepage_card_only"
|
||||
subsystems["bridge_relay_monitoring"] = map[string]interface{}{
|
||||
"status": overall,
|
||||
"updated_at": now,
|
||||
"age_seconds": int64(0),
|
||||
"source": freshness.SourceReported,
|
||||
"confidence": freshness.ConfidenceHigh,
|
||||
"provenance": freshness.ProvenanceMissionFeed,
|
||||
"completeness": freshness.CompletenessComplete,
|
||||
}
|
||||
}
|
||||
data["freshness"] = snapshot
|
||||
data["subsystems"] = subsystems
|
||||
data["sampling"] = sampling
|
||||
if diagnostics != nil {
|
||||
data["diagnostics"] = diagnostics
|
||||
}
|
||||
data["mode"] = map[string]interface{}{
|
||||
"kind": modeKind,
|
||||
"updated_at": now,
|
||||
"age_seconds": int64(0),
|
||||
"reason": modeReason,
|
||||
"scope": modeScope,
|
||||
"source": freshness.SourceReported,
|
||||
"confidence": freshness.ConfidenceHigh,
|
||||
"provenance": freshness.ProvenanceMissionFeed,
|
||||
}
|
||||
data["mode"] = buildBridgeModePayload(now, resolvedMode)
|
||||
}
|
||||
}
|
||||
if relays := FetchCCIPRelayHealths(ctx); relays != nil {
|
||||
@@ -222,11 +198,30 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
|
||||
}
|
||||
}
|
||||
}
|
||||
if laneHealth, proofTransfers := BuildBridgeLaneHealth(ctx); laneHealth != nil {
|
||||
data["bridge_lanes"] = laneHealth
|
||||
if proofTransfers != nil {
|
||||
data["proof_transfers"] = proofTransfers
|
||||
}
|
||||
}
|
||||
if mode, ok := data["mode"].(map[string]interface{}); ok {
|
||||
if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
|
||||
mode["kind"] = "snapshot"
|
||||
mode["reason"] = "live_homepage_stream_not_attached"
|
||||
mode["scope"] = "relay_monitoring_homepage_card_only"
|
||||
var diagnostics *freshness.Diagnostics
|
||||
if diag, ok := data["diagnostics"].(*freshness.Diagnostics); ok {
|
||||
diagnostics = diag
|
||||
}
|
||||
txFeed := freshness.CompletenessUnavailable
|
||||
if subsystems, ok := data["subsystems"].(map[string]interface{}); ok {
|
||||
if txIndex, ok := subsystems["tx_index"].(map[string]interface{}); ok {
|
||||
if feed, ok := txIndex["completeness"].(freshness.Completeness); ok {
|
||||
txFeed = feed
|
||||
}
|
||||
}
|
||||
}
|
||||
resolved := resolveBridgeDeliveryMode(true, diagnostics, txFeed)
|
||||
mode["kind"] = resolved.Kind
|
||||
mode["reason"] = resolved.Reason
|
||||
mode["scope"] = resolved.Scope
|
||||
if subsystems, ok := data["subsystems"].(map[string]interface{}); ok {
|
||||
subsystems["bridge_relay_monitoring"] = map[string]interface{}{
|
||||
"status": data["status"],
|
||||
@@ -239,6 +234,9 @@ func (s *Server) BuildBridgeStatusData(ctx context.Context) map[string]interface
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if relays, ok := data["ccip_relays"].(map[string]interface{}); ok && len(relays) > 0 {
|
||||
resolved := resolveBridgeDeliveryMode(true, nil, freshness.CompletenessUnavailable)
|
||||
data["mode"] = buildBridgeModePayload(now, resolved)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -212,6 +212,11 @@ func TestBuildBridgeStatusDataIncludesCCIPRelay(t *testing.T) {
|
||||
require.Contains(t, got, "diagnostics")
|
||||
require.Contains(t, got, "subsystems")
|
||||
require.Contains(t, got, "mode")
|
||||
mode, ok := got["mode"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "mixed", mode["kind"])
|
||||
require.Equal(t, "relay_snapshot_only_source", mode["reason"])
|
||||
require.Equal(t, "bridge_monitoring_and_homepage", mode["scope"])
|
||||
}
|
||||
|
||||
func TestBuildBridgeStatusDataDegradesWhenNamedRelayFails(t *testing.T) {
|
||||
|
||||
@@ -110,6 +110,12 @@ type WalletAuthResponse struct {
|
||||
InstitutionName string `json:"institution_name,omitempty"`
|
||||
}
|
||||
|
||||
// walletAuthSignMessage returns the EIP-191 plaintext users sign during wallet login.
|
||||
// Must stay in sync with frontend buildWalletMessage() in access.ts and explorer-spa.js.
|
||||
func walletAuthSignMessage(nonce string) string {
|
||||
return fmt.Sprintf("Sign this message to authenticate with DBIS Explorer.\n\nNonce: %s", nonce)
|
||||
}
|
||||
|
||||
// GenerateNonce generates a random nonce for wallet authentication
|
||||
func (w *WalletAuth) GenerateNonce(ctx context.Context, address string) (*NonceResponse, error) {
|
||||
// Validate address format
|
||||
@@ -184,7 +190,7 @@ func (w *WalletAuth) AuthenticateWallet(ctx context.Context, req *WalletAuthRequ
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
message := fmt.Sprintf("Sign this message to authenticate with SolaceScan.\n\nNonce: %s", req.Nonce)
|
||||
message := walletAuthSignMessage(req.Nonce)
|
||||
messageHash := accounts.TextHash([]byte(message))
|
||||
|
||||
sigBytes, err := decodeWalletSignature(req.Signature)
|
||||
|
||||
@@ -5,9 +5,42 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWalletAuthSignMessageMatchesFrontend(t *testing.T) {
|
||||
nonce := "abc123def456"
|
||||
require.Equal(
|
||||
t,
|
||||
"Sign this message to authenticate with DBIS Explorer.\n\nNonce: abc123def456",
|
||||
walletAuthSignMessage(nonce),
|
||||
)
|
||||
}
|
||||
|
||||
func TestAuthenticateWalletRecoversSignerFromFrontendMessage(t *testing.T) {
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex()
|
||||
nonce := "test-nonce-001"
|
||||
|
||||
message := walletAuthSignMessage(nonce)
|
||||
messageHash := accounts.TextHash([]byte(message))
|
||||
signature, err := crypto.Sign(messageHash, privateKey)
|
||||
require.NoError(t, err)
|
||||
signature[64] += 27
|
||||
|
||||
sigBytes := make([]byte, len(signature))
|
||||
copy(sigBytes, signature)
|
||||
if sigBytes[64] >= 27 {
|
||||
sigBytes[64] -= 27
|
||||
}
|
||||
pubKey, err := crypto.SigToPub(messageHash, sigBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, address, crypto.PubkeyToAddress(*pubKey).Hex())
|
||||
}
|
||||
|
||||
func TestDecodeWalletSignatureRejectsMalformedValues(t *testing.T) {
|
||||
_, err := decodeWalletSignature("deadbeef")
|
||||
require.ErrorContains(t, err, "signature must start with 0x")
|
||||
|
||||
@@ -1,93 +1,52 @@
|
||||
{
|
||||
"name": "MetaMask Multi-Chain Networks (Chain 138 + Ethereum + ALL Mainnet + Cronos)",
|
||||
"version": {
|
||||
"major": 1,
|
||||
"minor": 1,
|
||||
"patch": 0
|
||||
},
|
||||
"name": "MetaMask Multi-Chain Networks (13 chains)",
|
||||
"version": {"major": 1, "minor": 2, "patch": 1},
|
||||
"defaultChainId": 138,
|
||||
"explorerUrl": "https://explorer.d-bis.org",
|
||||
"tokenListUrl": "https://explorer.d-bis.org/api/config/token-list",
|
||||
"generatedBy": "DBIS Explorer",
|
||||
"chains": [
|
||||
{
|
||||
"chainId": "0x8a",
|
||||
"chainIdDecimal": 138,
|
||||
"chainName": "DeFi Oracle Meta Mainnet",
|
||||
"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"
|
||||
],
|
||||
"shortName": "dbis",
|
||||
"rpcUrls": ["https://rpc-http-pub.d-bis.org"],
|
||||
"nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18},
|
||||
"blockExplorerUrls": ["https://explorer.d-bis.org"],
|
||||
"infoURL": "https://d-bis.org",
|
||||
"explorerApiUrl": "https://explorer.d-bis.org/api/v2",
|
||||
"testnet": false,
|
||||
"iconUrls": [
|
||||
"https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"
|
||||
"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"
|
||||
],
|
||||
"oracles": [
|
||||
{"name": "ETH/USD (proxy)", "address": "0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6", "decimals": 8},
|
||||
{"name": "ETH/USD (aggregator)", "address": "0x99b3511a2d315a497c8112c1fdd8d508d4b1e506", "decimals": 8}
|
||||
]
|
||||
},
|
||||
{
|
||||
"chainId": "0x1",
|
||||
"chainIdDecimal": 1,
|
||||
"chainName": "Ethereum Mainnet",
|
||||
"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"
|
||||
]
|
||||
"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/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"],
|
||||
"oracles": [{"name": "ETH/USD", "address": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", "decimals": 8}]
|
||||
},
|
||||
{
|
||||
"chainId": "0x9f2c4",
|
||||
"chainIdDecimal": 651940,
|
||||
"chainName": "ALL Mainnet",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
{"chainId": "0x9f2c4", "chainIdDecimal": 651940, "chainName": "ALL Mainnet", "rpcUrls": ["https://mainnet-rpc.alltra.global"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://alltra.global"], "iconUrls": ["https://alltra.global/favicon.ico"], "oracles": []},
|
||||
{"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"], "oracles": []},
|
||||
{"chainId": "0x38", "chainIdDecimal": 56, "chainName": "BNB Smart Chain", "rpcUrls": ["https://bsc-dataseed.binance.org", "https://bsc-dataseed1.defibit.io", "https://bsc-dataseed1.ninicoin.io"], "nativeCurrency": {"name": "BNB", "symbol": "BNB", "decimals": 18}, "blockExplorerUrls": ["https://bscscan.com"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/smartchain/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x64", "chainIdDecimal": 100, "chainName": "Gnosis Chain", "rpcUrls": ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com", "https://1rpc.io/gnosis"], "nativeCurrency": {"name": "xDAI", "symbol": "xDAI", "decimals": 18}, "blockExplorerUrls": ["https://gnosisscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/xdai/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x89", "chainIdDecimal": 137, "chainName": "Polygon", "rpcUrls": ["https://polygon-rpc.com", "https://polygon.llamarpc.com", "https://polygon-bor-rpc.publicnode.com"], "nativeCurrency": {"name": "MATIC", "symbol": "MATIC", "decimals": 18}, "blockExplorerUrls": ["https://polygonscan.com"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/polygon/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa", "chainIdDecimal": 10, "chainName": "Optimism", "rpcUrls": ["https://mainnet.optimism.io", "https://optimism.llamarpc.com", "https://optimism-rpc.publicnode.com"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://optimistic.etherscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/optimism/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa4b1", "chainIdDecimal": 42161, "chainName": "Arbitrum One", "rpcUrls": ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com", "https://arbitrum-one-rpc.publicnode.com"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://arbiscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrum/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x2105", "chainIdDecimal": 8453, "chainName": "Base", "rpcUrls": ["https://mainnet.base.org", "https://base.llamarpc.com", "https://base-rpc.publicnode.com"], "nativeCurrency": {"name": "Ether", "symbol": "ETH", "decimals": 18}, "blockExplorerUrls": ["https://basescan.org"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/base/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa86a", "chainIdDecimal": 43114, "chainName": "Avalanche C-Chain", "rpcUrls": ["https://api.avax.network/ext/bc/C/rpc", "https://avalanche-c-chain-rpc.publicnode.com", "https://1rpc.io/avax/c"], "nativeCurrency": {"name": "AVAX", "symbol": "AVAX", "decimals": 18}, "blockExplorerUrls": ["https://snowtrace.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/avalanchec/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0xa4ec", "chainIdDecimal": 42220, "chainName": "Celo", "rpcUrls": ["https://forno.celo.org", "https://celo-mainnet-rpc.publicnode.com", "https://1rpc.io/celo"], "nativeCurrency": {"name": "CELO", "symbol": "CELO", "decimals": 18}, "blockExplorerUrls": ["https://celoscan.io"], "iconUrls": ["https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/celo/info/logo.png"], "oracles": []},
|
||||
{"chainId": "0x457", "chainIdDecimal": 1111, "chainName": "Wemix", "rpcUrls": ["https://api.wemix.com", "https://wemix-mainnet-rpc.publicnode.com"], "nativeCurrency": {"name": "WEMIX", "symbol": "WEMIX", "decimals": 18}, "blockExplorerUrls": ["https://scan.wemix.com"], "iconUrls": ["https://scan.wemix.com/favicon.ico"], "oracles": []}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
DROP INDEX IF EXISTS idx_swap_events_token1_price;
|
||||
DROP INDEX IF EXISTS idx_swap_events_token0_price;
|
||||
DROP INDEX IF EXISTS idx_swap_events_chain_tx_log;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_swap_events_unique_log
|
||||
ON swap_events (
|
||||
chain_id,
|
||||
pool_address,
|
||||
COALESCE(transaction_hash, ''),
|
||||
COALESCE(log_index, -1)
|
||||
);
|
||||
|
||||
ALTER TABLE IF EXISTS swap_events
|
||||
DROP COLUMN IF EXISTS to_address,
|
||||
DROP COLUMN IF EXISTS sender,
|
||||
DROP COLUMN IF EXISTS token1_price_usd,
|
||||
DROP COLUMN IF EXISTS token0_price_usd,
|
||||
DROP COLUMN IF EXISTS price_usd,
|
||||
DROP COLUMN IF EXISTS amount1_out,
|
||||
DROP COLUMN IF EXISTS amount0_out,
|
||||
DROP COLUMN IF EXISTS amount1_in,
|
||||
DROP COLUMN IF EXISTS amount0_in;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Migration: Add per-token USD price columns to swap_events
|
||||
-- Description: Aligns lightweight swap_events schema with token-aggregation writer and
|
||||
-- enables historical OHLCV generation to derive token-specific candles
|
||||
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS amount0_in NUMERIC(78, 0) DEFAULT 0;
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS amount1_in NUMERIC(78, 0) DEFAULT 0;
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS amount0_out NUMERIC(78, 0) DEFAULT 0;
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS amount1_out NUMERIC(78, 0) DEFAULT 0;
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS price_usd NUMERIC(30, 8);
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS token0_price_usd NUMERIC(30, 8);
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS token1_price_usd NUMERIC(30, 8);
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS sender VARCHAR(42);
|
||||
ALTER TABLE IF EXISTS swap_events ADD COLUMN IF NOT EXISTS to_address VARCHAR(42);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_swap_events_token0_price
|
||||
ON swap_events (chain_id, token0_address, timestamp DESC)
|
||||
WHERE token0_price_usd IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_swap_events_token1_price
|
||||
ON swap_events (chain_id, token1_address, timestamp DESC)
|
||||
WHERE token1_price_usd IS NOT NULL;
|
||||
|
||||
DROP INDEX IF EXISTS idx_swap_events_unique_log;
|
||||
DROP INDEX IF EXISTS idx_swap_events_chain_tx_log;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_swap_events_chain_tx_log
|
||||
ON swap_events (chain_id, transaction_hash, log_index);
|
||||
@@ -9,7 +9,7 @@ require (
|
||||
github.com/ethereum/go-ethereum v1.13.5
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/jackc/pgx/v5 v5.5.1
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.36.0
|
||||
@@ -33,6 +33,7 @@ require (
|
||||
github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/holiman/uint256 v1.2.3 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
@@ -40,6 +41,8 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mmcloughlin/addchain v0.4.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||
github.com/supranational/blst v0.3.11 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
@@ -51,6 +54,5 @@ require (
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
rsc.io/tmplfunc v0.0.3 // indirect
|
||||
)
|
||||
|
||||
@@ -75,13 +75,11 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
|
||||
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
|
||||
@@ -98,14 +96,14 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
||||
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -120,8 +118,6 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
|
||||
@@ -129,26 +125,27 @@ github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh
|
||||
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
|
||||
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
|
||||
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
|
||||
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
|
||||
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
@@ -198,8 +195,8 @@ golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -3,35 +3,148 @@ package wallet
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WalletConnect handles WalletConnect v2 integration
|
||||
const (
|
||||
WalletConnectStatusStub = "stub"
|
||||
WalletConnectStatusClient = "client"
|
||||
WalletConnectStatusDisabled = "disabled"
|
||||
)
|
||||
|
||||
// Config describes the public WalletConnect v2 posture exposed to clients.
|
||||
type Config struct {
|
||||
Status string `json:"status"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ProjectID string `json:"projectId"`
|
||||
RelayURL string `json:"relayUrl"`
|
||||
MetadataURL string `json:"metadataUrl"`
|
||||
RequiredNamespaces []string `json:"requiredNamespaces"`
|
||||
SupportedChains []int `json:"supportedChains"`
|
||||
FallbackAuth string `json:"fallbackAuth"`
|
||||
Message string `json:"message"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ConnectResponse is returned while WalletConnect session bridging remains a stub.
|
||||
type ConnectResponse struct {
|
||||
Status string `json:"status"`
|
||||
Enabled bool `json:"enabled"`
|
||||
URI string `json:"uri,omitempty"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
ExpiresAt string `json:"expiresAt,omitempty"`
|
||||
FallbackAuth string `json:"fallbackAuth"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Session represents a wallet session snapshot for future WalletConnect integration.
|
||||
type Session struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
Address string `json:"address,omitempty"`
|
||||
ChainID int `json:"chainId,omitempty"`
|
||||
Connected bool `json:"connected"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// WalletConnect handles WalletConnect v2 integration posture for the explorer API.
|
||||
type WalletConnect struct {
|
||||
projectID string
|
||||
relayURL string
|
||||
chainID int
|
||||
}
|
||||
|
||||
// NewWalletConnect creates a new WalletConnect handler
|
||||
func NewWalletConnect(projectID string) *WalletConnect {
|
||||
return &WalletConnect{projectID: projectID}
|
||||
// NewWalletConnect creates a WalletConnect handler using deployment env vars.
|
||||
func NewWalletConnect(chainID int) *WalletConnect {
|
||||
projectID := strings.TrimSpace(os.Getenv("WALLETCONNECT_PROJECT_ID"))
|
||||
relayURL := strings.TrimSpace(os.Getenv("WALLETCONNECT_RELAY_URL"))
|
||||
if relayURL == "" {
|
||||
relayURL = "wss://relay.walletconnect.org"
|
||||
}
|
||||
return &WalletConnect{
|
||||
projectID: projectID,
|
||||
relayURL: relayURL,
|
||||
chainID: chainID,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect initiates a wallet connection
|
||||
func (wc *WalletConnect) Connect(ctx context.Context) (string, error) {
|
||||
// Implementation would use WalletConnect v2 SDK
|
||||
// Returns connection URI for QR code display
|
||||
return "", fmt.Errorf("not implemented - requires WalletConnect SDK")
|
||||
func (wc *WalletConnect) enabled() bool {
|
||||
return wc.projectID != ""
|
||||
}
|
||||
|
||||
// Session represents a wallet session
|
||||
type Session struct {
|
||||
Address string
|
||||
ChainID int
|
||||
Connected bool
|
||||
// PublicConfig returns the read-only WalletConnect config surface for clients.
|
||||
func (wc *WalletConnect) PublicConfig() Config {
|
||||
status := WalletConnectStatusDisabled
|
||||
if wc.enabled() {
|
||||
status = WalletConnectStatusClient
|
||||
}
|
||||
return Config{
|
||||
Status: status,
|
||||
Enabled: wc.enabled(),
|
||||
ProjectID: wc.projectID,
|
||||
RelayURL: wc.relayURL,
|
||||
MetadataURL: "/api/v1/walletconnect/metadata",
|
||||
RequiredNamespaces: []string{"eip155"},
|
||||
SupportedChains: []int{wc.chainID, 1},
|
||||
FallbackAuth: "/api/v1/auth/wallet",
|
||||
Message: wc.publicMessage(),
|
||||
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSession gets current wallet session
|
||||
func (wc *WalletConnect) GetSession(ctx context.Context, sessionID string) (*Session, error) {
|
||||
// Implementation would retrieve session from WalletConnect
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
func (wc *WalletConnect) publicMessage() string {
|
||||
if wc.enabled() {
|
||||
return "WalletConnect v2 is enabled. Use the WalletConnect button on /wallet for mobile QR pairing; browser extension wallets can continue using /api/v1/auth/wallet."
|
||||
}
|
||||
return "WalletConnect v2 is not configured. Set WALLETCONNECT_PROJECT_ID to publish relay config; browser wallet auth remains available at /api/v1/auth/wallet."
|
||||
}
|
||||
|
||||
// Connect reports client-side WalletConnect posture. Pairing runs in the browser when projectId is published.
|
||||
func (wc *WalletConnect) Connect(_ context.Context) (*ConnectResponse, error) {
|
||||
if !wc.enabled() {
|
||||
return &ConnectResponse{
|
||||
Status: WalletConnectStatusDisabled,
|
||||
Enabled: false,
|
||||
FallbackAuth: "/api/v1/auth/wallet",
|
||||
Message: wc.publicMessage(),
|
||||
}, fmt.Errorf("walletconnect is disabled")
|
||||
}
|
||||
return &ConnectResponse{
|
||||
Status: "client",
|
||||
Enabled: true,
|
||||
FallbackAuth: "/api/v1/auth/wallet",
|
||||
Message: "Initialize WalletConnect in the browser via /wallet using the published projectId; authenticate with /api/v1/auth/wallet after pairing.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSession returns a registered browser-paired WalletConnect session snapshot.
|
||||
func (wc *WalletConnect) GetSession(_ context.Context, sessionID string) (*Session, error) {
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
return nil, fmt.Errorf("session id is required")
|
||||
}
|
||||
if session, ok := lookupWalletConnectSession(sessionID); ok {
|
||||
return session, nil
|
||||
}
|
||||
return &Session{
|
||||
SessionID: sessionID,
|
||||
Connected: false,
|
||||
Status: WalletConnectStatusClient,
|
||||
Message: "Session not registered yet. Pair on /wallet, then POST /api/v1/walletconnect/session with sessionId and address.",
|
||||
}, fmt.Errorf("walletconnect session not found")
|
||||
}
|
||||
|
||||
// RegisterSession stores a client-paired WalletConnect session for operator lookup.
|
||||
func (wc *WalletConnect) RegisterSession(_ context.Context, sessionID, address string, chainID int) (*Session, error) {
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
return nil, fmt.Errorf("session id is required")
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(address), "0x") || len(address) != 42 {
|
||||
return nil, fmt.Errorf("valid address is required")
|
||||
}
|
||||
if chainID <= 0 {
|
||||
chainID = wc.chainID
|
||||
}
|
||||
return RegisterClientSession(sessionID, address, chainID), nil
|
||||
}
|
||||
|
||||
88
backend/wallet/walletconnect_sessions.go
Normal file
88
backend/wallet/walletconnect_sessions.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const walletConnectSessionTTL = 24 * time.Hour
|
||||
|
||||
type storedWalletConnectSession struct {
|
||||
SessionID string
|
||||
Address string
|
||||
ChainID int
|
||||
Connected bool
|
||||
Status string
|
||||
Message string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
walletConnectSessionMu sync.RWMutex
|
||||
walletConnectSessions = map[string]storedWalletConnectSession{}
|
||||
)
|
||||
|
||||
func purgeExpiredWalletConnectSessions(now time.Time) {
|
||||
for id, session := range walletConnectSessions {
|
||||
if now.Sub(session.UpdatedAt) > walletConnectSessionTTL {
|
||||
delete(walletConnectSessions, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterClientSession records a browser-paired WalletConnect session snapshot.
|
||||
func RegisterClientSession(sessionID, address string, chainID int) *Session {
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
address = strings.TrimSpace(address)
|
||||
now := time.Now().UTC()
|
||||
|
||||
walletConnectSessionMu.Lock()
|
||||
defer walletConnectSessionMu.Unlock()
|
||||
purgeExpiredWalletConnectSessions(now)
|
||||
|
||||
record := storedWalletConnectSession{
|
||||
SessionID: sessionID,
|
||||
Address: address,
|
||||
ChainID: chainID,
|
||||
Connected: true,
|
||||
Status: WalletConnectStatusClient,
|
||||
Message: "WalletConnect session registered by browser client after pairing.",
|
||||
UpdatedAt: now,
|
||||
}
|
||||
walletConnectSessions[sessionID] = record
|
||||
return sessionFromStored(record)
|
||||
}
|
||||
|
||||
func lookupWalletConnectSession(sessionID string) (*Session, bool) {
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
if sessionID == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
walletConnectSessionMu.Lock()
|
||||
defer walletConnectSessionMu.Unlock()
|
||||
purgeExpiredWalletConnectSessions(now)
|
||||
|
||||
record, ok := walletConnectSessions[sessionID]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if now.Sub(record.UpdatedAt) > walletConnectSessionTTL {
|
||||
delete(walletConnectSessions, sessionID)
|
||||
return nil, false
|
||||
}
|
||||
return sessionFromStored(record), true
|
||||
}
|
||||
|
||||
func sessionFromStored(record storedWalletConnectSession) *Session {
|
||||
return &Session{
|
||||
SessionID: record.SessionID,
|
||||
Address: record.Address,
|
||||
ChainID: record.ChainID,
|
||||
Connected: record.Connected,
|
||||
Status: record.Status,
|
||||
Message: record.Message,
|
||||
}
|
||||
}
|
||||
25
backend/wallet/walletconnect_sessions_test.go
Normal file
25
backend/wallet/walletconnect_sessions_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegisterAndLookupWalletConnectSession(t *testing.T) {
|
||||
sessionID := "wc-test-topic-123"
|
||||
address := "0x4A666F96fC8764181194447A7dFdb7d471b301C8"
|
||||
|
||||
registered := RegisterClientSession(sessionID, address, 138)
|
||||
if registered == nil || !registered.Connected {
|
||||
t.Fatalf("expected connected session, got %#v", registered)
|
||||
}
|
||||
|
||||
wc := NewWalletConnect(138)
|
||||
session, err := wc.GetSession(context.Background(), sessionID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSession: %v", err)
|
||||
}
|
||||
if session.Address != address {
|
||||
t.Fatalf("expected address %s, got %s", address, session.Address)
|
||||
}
|
||||
}
|
||||
61
config/explorer-bridge-lanes.v1.json
Normal file
61
config/explorer-bridge-lanes.v1.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"updated": "2026-05-23",
|
||||
"min_link_wei": "1000000000000000000",
|
||||
"lanes": [
|
||||
{
|
||||
"key": "chain138",
|
||||
"chain_name": "Defi Oracle Meta Mainnet (138)",
|
||||
"chain_id": 138,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["RPC_URL", "RPC_URL_138"],
|
||||
"rpc_default": "http://192.168.11.211:8545",
|
||||
"link_token": "0xb7721dd53a8c629d9f1ba31a5819afe250002b03",
|
||||
"weth9_bridge": "0xcacfd227A040002e49e2e01626363071324f820a",
|
||||
"weth10_bridge": "0xe0E93247376aa097dB308B92e6Ba36bA015535D0"
|
||||
},
|
||||
{
|
||||
"key": "gnosis",
|
||||
"chain_name": "Gnosis (100)",
|
||||
"chain_id": 100,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["GNOSIS_RPC", "GNOSIS_MAINNET_RPC", "GNOSIS_RPC_URL"],
|
||||
"rpc_default": "https://rpc.gnosischain.com",
|
||||
"link_token": "0xE2e73A1c69ecF83F464EFCE6A5be353a37cA09b2",
|
||||
"weth9_bridge": "0xc8656F24488cb90c452058da92d1a25BA464eaAE",
|
||||
"weth10_bridge": "0xa846aeAD3071df1b6439d5D813156aCE7C2c1DA1"
|
||||
},
|
||||
{
|
||||
"key": "cronos",
|
||||
"chain_name": "Cronos (25)",
|
||||
"chain_id": 25,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CRONOS_RPC", "CRONOS_RPC_URL", "CRONOS_MAINNET_RPC"],
|
||||
"rpc_default": "https://evm.cronos.org",
|
||||
"link_token": "0x8c80A01F461f297Df7F9DA3A4f740D7297C8Ac85",
|
||||
"weth9_bridge": "0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab",
|
||||
"weth10_bridge": "0x105F8A15b819948a89153505762444Ee9f324684"
|
||||
},
|
||||
{
|
||||
"key": "celo",
|
||||
"chain_name": "Celo (42220)",
|
||||
"chain_id": 42220,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["CELO_RPC", "CELO_MAINNET_RPC"],
|
||||
"rpc_default": "https://forno.celo.org",
|
||||
"link_token": "0xd07294e6E917e07dfDcee882dd1e2565085C2ae0",
|
||||
"weth9_bridge": "0xAb57BF30F1354CA0590af22D8974c7f24DB2DbD7",
|
||||
"weth10_bridge": "0xa780ef19A041745d353c9432f2a7f5A241335ffE"
|
||||
},
|
||||
{
|
||||
"key": "wemix",
|
||||
"chain_name": "Wemix (1111)",
|
||||
"chain_id": 1111,
|
||||
"config_ready": true,
|
||||
"rpc_envs": ["WEMIX_RPC", "WEMIX_MAINNET_RPC"],
|
||||
"rpc_default": "https://api.wemix.com",
|
||||
"link_token": "0x80f1FcdC96B55e459BF52b998aBBE2c364935d69",
|
||||
"weth9_bridge": "0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04",
|
||||
"weth10_bridge": "0xa4B9DD039565AeD9641D45b57061f99d9cA6Df08"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
config/explorer-bridge-proof-transfers.example.json
Normal file
21
config/explorer-bridge-proof-transfers.example.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"updated": "2026-05-23T13:07:00Z",
|
||||
"sprint": "tier-a-week-3",
|
||||
"note": "Example shape for MISSION_CONTROL_PROOF_TRANSFERS_JSON. Operator live file: reports/status/bridge-lane-proof-transfers-latest.json (gitignored). Populate with scripts/bridge/run-lane-proof-transfers.sh --execute.",
|
||||
"lanes": {
|
||||
"chain138": [
|
||||
{
|
||||
"tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"amount_eth": "0.001",
|
||||
"dest_chain": "mainnet",
|
||||
"dest_selector": "5009297550715157269",
|
||||
"bridge": "0xcacfd227A040002e49e2e01626363071324f820a",
|
||||
"recorded_at": "2026-05-23T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"gnosis": [],
|
||||
"cronos": [],
|
||||
"celo": [],
|
||||
"wemix": []
|
||||
}
|
||||
}
|
||||
756
config/web3-identity-registry.v1.json
Normal file
756
config/web3-identity-registry.v1.json
Normal file
@@ -0,0 +1,756 @@
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"updated": "2026-06-10",
|
||||
"description": "Unified Web3 identity registry — addresses, ENS, explorer labels, and institutional identifier refs. SSOT for naming across Blockscout sync, explorer UI, and frontend-dapp.",
|
||||
"policyDoc": "docs/04-configuration/WEB3_IDENTITY_AND_NAMING_POLICY.md",
|
||||
"sources": [
|
||||
"smom-dbis-138/config/address-inventory.chain138.json",
|
||||
"config/chain138-official-protocol-contracts.json",
|
||||
"config/compliance/liquidity-vault-gnosis-safe-mainnet.v1.json",
|
||||
"docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json"
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"id": "deployer-defi-oracle",
|
||||
"address": "0x4A666F96fC8764181194447A7dFdb7d471b301C8",
|
||||
"chainIds": [
|
||||
1,
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"deployer",
|
||||
"operator"
|
||||
],
|
||||
"displayName": "DeFi Oracle Deployer",
|
||||
"ens": {
|
||||
"primary": "defi-oracle.eth",
|
||||
"chainId": 1
|
||||
},
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "DEPLOYER_ADMIN_138"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DeFi Oracle Deployer (defi-oracle.eth)",
|
||||
"tagTypes": [
|
||||
"operator",
|
||||
"deployer"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "liquidity-vault-safe-mainnet",
|
||||
"address": "0x93a42Bdc51BecE00a2F31C744Fceb4956C4B6f76",
|
||||
"chainIds": [
|
||||
1
|
||||
],
|
||||
"roles": [
|
||||
"treasury",
|
||||
"multisig"
|
||||
],
|
||||
"displayName": "LiquidityVault Safe",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "LIQUIDITY_VAULT_SAFE_MAINNET"
|
||||
},
|
||||
{
|
||||
"type": "safe",
|
||||
"value": "2-of-4",
|
||||
"note": "Created by defi-oracle.eth"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "LiquidityVault Safe (2-of-4)",
|
||||
"tagTypes": [
|
||||
"treasury",
|
||||
"multisig"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "dodo-pmm-integration-stack-a",
|
||||
"address": "0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"protocol",
|
||||
"dex",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "DODO PMM Integration (Stack A)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "DODO_PMM_INTEGRATION"
|
||||
},
|
||||
{
|
||||
"type": "protocol",
|
||||
"value": "dodo_v2"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM Integration (live Stack A)",
|
||||
"tagTypes": [
|
||||
"protocol",
|
||||
"dex",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "dodo-pmm-provider",
|
||||
"address": "0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"protocol",
|
||||
"dex",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "DODO PMM Provider",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "DODO_PMM_PROVIDER"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM Provider (ILiquidityProvider)",
|
||||
"tagTypes": [
|
||||
"protocol",
|
||||
"dex"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "dodo-v2-proxy",
|
||||
"address": "0xEF6E6F41A522896a9EE1C580C87C05E409193F8d",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"protocol",
|
||||
"dex",
|
||||
"router"
|
||||
],
|
||||
"displayName": "DODOV2Proxy02",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "DODO_V2_PROXY"
|
||||
},
|
||||
{
|
||||
"type": "protocol",
|
||||
"value": "dodo_v2"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODOV2Proxy02",
|
||||
"tagTypes": [
|
||||
"protocol",
|
||||
"router"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "canonical-cusdt-138",
|
||||
"address": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"token",
|
||||
"stablecoin"
|
||||
],
|
||||
"displayName": "cUSDT (canonical)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "symbol",
|
||||
"value": "cUSDT"
|
||||
},
|
||||
{
|
||||
"type": "decimals",
|
||||
"value": "6"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "cUSDT (Compliant USDT)",
|
||||
"tagTypes": [
|
||||
"token",
|
||||
"stablecoin",
|
||||
"gru"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "canonical-cusdc-138",
|
||||
"address": "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"token",
|
||||
"stablecoin"
|
||||
],
|
||||
"displayName": "cUSDC (canonical)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "symbol",
|
||||
"value": "cUSDC"
|
||||
},
|
||||
{
|
||||
"type": "decimals",
|
||||
"value": "6"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "cUSDC (Compliant USDC)",
|
||||
"tagTypes": [
|
||||
"token",
|
||||
"stablecoin",
|
||||
"gru"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "canonical-cbtc-138",
|
||||
"address": "0xe94260c555ac1d9d3cc9e1632883452ebdf0082e",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"token"
|
||||
],
|
||||
"displayName": "cBTC (canonical)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "symbol",
|
||||
"value": "cBTC"
|
||||
},
|
||||
{
|
||||
"type": "decimals",
|
||||
"value": "8"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "cBTC (Compliant BTC)",
|
||||
"tagTypes": [
|
||||
"token",
|
||||
"gru"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-cusdt-cusdc",
|
||||
"address": "0x9e89bAe009adf128782E19e8341996c596ac40dC",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool cUSDT/cUSDC",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "cUSDT/cUSDC"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM cUSDT/cUSDC (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm",
|
||||
"dex"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "ccip-router-138",
|
||||
"address": "0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"bridge",
|
||||
"ccip"
|
||||
],
|
||||
"displayName": "CCIP Router (Chain 138)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "CCIP_ROUTER_138"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "Chainlink CCIP Router",
|
||||
"tagTypes": [
|
||||
"bridge",
|
||||
"ccip"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "oracle-aggregator-138",
|
||||
"address": "0x99b3511a2d315a497c8112c1fdd8d508d4b1e506",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"oracle"
|
||||
],
|
||||
"displayName": "Oracle Aggregator",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "ORACLE_AGGREGATOR_ADDRESS"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "Oracle Aggregator",
|
||||
"tagTypes": [
|
||||
"oracle"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "multicall3-138",
|
||||
"address": "0xcA11bde05977b3631167028862bE2a173976CA11",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"utility"
|
||||
],
|
||||
"displayName": "Multicall3",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "MULTICALL3"
|
||||
},
|
||||
{
|
||||
"type": "protocol",
|
||||
"value": "multicall3"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "Multicall3",
|
||||
"tagTypes": [
|
||||
"utility"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "liquidity-owner-adam",
|
||||
"address": "0x348775A05CF5b6fC9d18830CfBd63DAE0Fb3c668",
|
||||
"chainIds": [
|
||||
1
|
||||
],
|
||||
"roles": [
|
||||
"multisig-owner"
|
||||
],
|
||||
"displayName": "AdamMultiSig (LiquidityVault owner)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "safeOwner",
|
||||
"value": "LiquidityVault"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "LiquidityVault Owner — AdamMultiSig",
|
||||
"tagTypes": [
|
||||
"multisig-owner"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "liquidity-owner-benard",
|
||||
"address": "0x3F5BD2e9DA51Dc76f7F3308daa904C1B9E90460f",
|
||||
"chainIds": [
|
||||
1
|
||||
],
|
||||
"roles": [
|
||||
"multisig-owner"
|
||||
],
|
||||
"displayName": "BenardMultiSig (LiquidityVault owner)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "safeOwner",
|
||||
"value": "LiquidityVault"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "LiquidityVault Owner — BenardMultiSig",
|
||||
"tagTypes": [
|
||||
"multisig-owner"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "liquidity-owner-nathan",
|
||||
"address": "0x8c906Bd27ba9Ea828B32DaE484eea5982b20CDb9",
|
||||
"chainIds": [
|
||||
1
|
||||
],
|
||||
"roles": [
|
||||
"multisig-owner"
|
||||
],
|
||||
"displayName": "NathanMultiSig (LiquidityVault owner)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "safeOwner",
|
||||
"value": "LiquidityVault"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "LiquidityVault Owner — NathanMultiSig",
|
||||
"tagTypes": [
|
||||
"multisig-owner"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "liquidity-owner-target",
|
||||
"address": "0x16285b235b413bFd376ECBe66F2F9f5F6EA5313C",
|
||||
"chainIds": [
|
||||
1
|
||||
],
|
||||
"roles": [
|
||||
"multisig-owner"
|
||||
],
|
||||
"displayName": "0xLiquidity (planned Safe owner)",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "safeOwner",
|
||||
"value": "LiquidityVault",
|
||||
"note": "planned owner swap target"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "LiquidityVault Owner — 0xLiquidity (planned)",
|
||||
"tagTypes": [
|
||||
"multisig-owner"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "enhanced-swap-router-v2",
|
||||
"address": "0xa421706768aeb7fafa2d912c5e10824ef3437ad4",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"protocol",
|
||||
"router"
|
||||
],
|
||||
"displayName": "EnhancedSwapRouter V2",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "inventoryKey",
|
||||
"value": "ENHANCED_SWAP_ROUTER_V2_ADDRESS"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "EnhancedSwapRouter V2",
|
||||
"tagTypes": [
|
||||
"protocol",
|
||||
"router"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-cusdt-usdt",
|
||||
"address": "0x866Cb44b59303d8dc5f4F9E3E7A8e8b0bf238d66",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool cUSDT/USDT",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "cUSDT/USDT"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM cUSDT/USDT (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-cusdc-usdc",
|
||||
"address": "0xc39B7D0F40838cbFb54649d327f49a6DAC964062",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool cUSDC/USDC",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "cUSDC/USDC"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM cUSDC/USDC (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-cbtc-cusdt",
|
||||
"address": "0x67049e7333481e2cac91af61403ac7bddfab7bcd",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool cBTC/cUSDT",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "cBTC/cUSDT"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM cBTC/cUSDT (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-cbtc-cusdc",
|
||||
"address": "0x72f1a0794153c3b8a1e8a731f1d8e1a52cb10dc5",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool cBTC/cUSDC",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "cBTC/cUSDC"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM cBTC/cUSDC (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-weth-usdc",
|
||||
"address": "0xb53a0508940b1ff90f1aad4f6cb50a7012fe5593",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool WETH/USDC",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "WETH/USDC"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM WETH/USDC (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-weth-usdt",
|
||||
"address": "0xe227f6c0520c0c6e8786fe56fa76c4914f861533",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool WETH/USDT",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "WETH/USDT"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM WETH/USDT (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
},
|
||||
{
|
||||
"id": "pmm-pool-cbtc-cxauc",
|
||||
"address": "0xf3e8a07d419b61f002114e64d79f7cf8f7989433",
|
||||
"chainIds": [
|
||||
138
|
||||
],
|
||||
"roles": [
|
||||
"pool",
|
||||
"pmm"
|
||||
],
|
||||
"displayName": "PMM Pool cBTC/cXAUC",
|
||||
"ens": null,
|
||||
"web3Domains": [],
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "pair",
|
||||
"value": "cBTC/cXAUC"
|
||||
}
|
||||
],
|
||||
"explorer": {
|
||||
"blockscoutLabel": "DODO PMM cBTC/cXAUC (live)",
|
||||
"tagTypes": [
|
||||
"pool",
|
||||
"pmm"
|
||||
]
|
||||
},
|
||||
"eiLabel": null
|
||||
}
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
"id": "omnl-head-office",
|
||||
"displayName": "OMNL Head Office (DBIS) – Central Bank",
|
||||
"lei": "98450070C57395F6B906",
|
||||
"entityRef": "omnl-entity-master:1",
|
||||
"roles": [
|
||||
"entity",
|
||||
"central-bank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "omnl-nrb",
|
||||
"displayName": "Nepal Rastra Bank",
|
||||
"lei": "25490000MX377HHPSR96",
|
||||
"entityRef": "omnl-entity-master:18",
|
||||
"roles": [
|
||||
"entity",
|
||||
"central-bank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "omnl-sanima",
|
||||
"displayName": "Sanima Bank Limited",
|
||||
"lei": "25490043FER1B108XE95",
|
||||
"entityRef": "omnl-entity-master:19",
|
||||
"roles": [
|
||||
"entity",
|
||||
"bank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "omnl-cbuae",
|
||||
"displayName": "Central Bank of the UAE",
|
||||
"lei": "5493006P6LOOFH8TB150",
|
||||
"entityRef": "omnl-entity-master:23",
|
||||
"roles": [
|
||||
"entity",
|
||||
"central-bank"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -479,7 +479,7 @@ EOF
|
||||
```bash
|
||||
cat > /etc/systemd/system/solacescanscout-frontend.service << 'EOF'
|
||||
[Unit]
|
||||
Description=SolaceScan Next Frontend Service
|
||||
Description=DBIS Explorer Next Frontend Service
|
||||
After=network.target explorer-api.service
|
||||
Requires=explorer-api.service
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Live Deployment Map
|
||||
|
||||
Current production deployment map for the SolaceScan public explorer surface.
|
||||
Current production deployment map for the DBIS Explorer public explorer surface.
|
||||
|
||||
This file is the authoritative reference for the live explorer stack as of `2026-04-05`. It supersedes the older monolithic deployment notes in this directory when the question is "what is running in production right now?"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Next.js frontend proxy locations for SolaceScan.
|
||||
# Next.js frontend proxy locations for DBIS Explorer.
|
||||
# Keep the existing higher-priority locations for:
|
||||
# - /api/
|
||||
# - /api/config/token-list
|
||||
@@ -12,6 +12,12 @@
|
||||
# Include these locations after those API/static locations and before any legacy
|
||||
# catch-all that serves /var/www/html/index.html directly.
|
||||
|
||||
location ^~ /legacy/ {
|
||||
alias /var/www/html/legacy/;
|
||||
try_files $uri $uri/ /legacy/index.html;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
}
|
||||
|
||||
location ^~ /_next/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=SolaceScan Next Frontend Service
|
||||
Description=DBIS Explorer Next Frontend Service
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
|
||||
35
docs/CHAIN138_VISUAL_TOPOLOGY_SOURCE.md
Normal file
35
docs/CHAIN138_VISUAL_TOPOLOGY_SOURCE.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Chain 138 visual topology — command center source
|
||||
|
||||
Canonical maintainer doc for `frontend/public/chain138-command-center.html` (live at `/chain138-command-center.html` and `/topology`).
|
||||
|
||||
Diagrams are **informational**; live addresses and guardrails are served by the explorer:
|
||||
|
||||
- `/protocols` and `/token-aggregation/api/v1/report/official-protocols`
|
||||
- `/pools`, `/liquidity`, `/routes`
|
||||
- `/bridge` (managed relay fleet + CCIP lane health)
|
||||
- `/docs/gru`, `/docs/posture-glossary`, `/docs/transaction-review`
|
||||
|
||||
## PMM production canon (Chain 138)
|
||||
|
||||
| Role | Address |
|
||||
|------|---------|
|
||||
| DODOPMMIntegration Stack A (live) | `0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895` |
|
||||
| DODOPMMProvider Stack A (live) | `0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e` |
|
||||
| EnhancedSwapRouter (live) | `0xE6Cc7643ae2A4C720A28D8263BC4972905d7DE0f` |
|
||||
| Stack B integration (do not wire) | `0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d` |
|
||||
|
||||
EnhancedSwapRouterV2 remains a planned Phase 4b deploy; it is not the default production router.
|
||||
|
||||
## Managed relay lanes (explorer `/bridge`)
|
||||
|
||||
`mainnet_weth`, `mainnet_cw`, `bsc`, `avax`, `avax_cw`, `avax_to_138`
|
||||
|
||||
## External references (proxmox monorepo)
|
||||
|
||||
When editing diagrams, cross-check:
|
||||
|
||||
- `docs/11-references/PMM_DEX_ROUTING_STATUS.md`
|
||||
- `docs/11-references/CONTRACT_ADDRESSES_REFERENCE.md`
|
||||
- `docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md` (§5 canonical tokens)
|
||||
|
||||
Bundle metadata: `frontend/public/chain138-command-center.meta.json` (refresh via `scripts/refresh-chain138-command-center-meta.sh`).
|
||||
@@ -224,7 +224,7 @@ User → chainlist.org → Search "DBIS" → Click "Add to MetaMask"
|
||||
|
||||
```
|
||||
User → MetaMask → Click "View on Explorer"
|
||||
→ MetaMask opens: https://explorer.d-bis.org/tx/{hash}
|
||||
→ MetaMask opens: https://explorer.d-bis.org/transactions/{hash}
|
||||
→ Blockscout displays transaction details
|
||||
→ Blockscout API provides the data
|
||||
```
|
||||
@@ -285,4 +285,3 @@ User → MetaMask → View Token Balance
|
||||
|
||||
**Last Updated**: 2025-12-24
|
||||
**Status**: Analysis Complete
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ The script checks:
|
||||
- HTTP 200 on `/api/v2/stats`, `/api/v2/blocks`, `/api/v2/transactions`.
|
||||
- Explorer frontend at `/` returns 200.
|
||||
- Chain 138 Snap companion site at `/snap/` returns 200 or 301 and contains expected content when 200.
|
||||
- The static Visual Command Center at `/chain138-command-center.html` returns 200 and contains expected architecture text.
|
||||
- The static Visual Command Center at `/chain138-command-center.html` (alias `/topology`) returns 200, includes Stack A PMM canon text, and `/chain138-command-center.meta.json` exposes bundle metadata.
|
||||
- Mission Control endpoints return healthy responses:
|
||||
- `/explorer-api/v1/mission-control/stream`
|
||||
- `/explorer-api/v1/mission-control/bridge/trace`
|
||||
|
||||
28
docs/EXPLORER_PUBLIC_API_ACCESS.md
Normal file
28
docs/EXPLORER_PUBLIC_API_ACCESS.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Explorer public API access (decision record)
|
||||
|
||||
**Date:** 2026-05-23
|
||||
**Live page:** `/docs/public-api-access`
|
||||
|
||||
## Summary
|
||||
|
||||
| Surface | Auth today | Notes |
|
||||
|---------|------------|-------|
|
||||
| Blockscout read API (`/api/v2/*`) | None | Same-origin proxy to Blockscout |
|
||||
| Public JSON (stats, bridge routes, token lists, etc.) | None | Listed in footer **Public APIs** |
|
||||
| Managed RPC keys | Wallet session on `/access` | `POST /api/v1/access/api-keys` after `/api/v1/auth/wallet` |
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Keep Blockscout and public JSON unauthenticated** for integrators on the public explorer domain.
|
||||
2. **Managed RPC keys** remain the wallet-authenticated product on `/access` — not a Blockscout API-key layer.
|
||||
3. **Future path (Option B):** nginx/API-gateway throttling with optional `X-API-Key` for higher quotas if abuse appears. Full external developer portal remains optional.
|
||||
|
||||
## Integrator flow
|
||||
|
||||
- Read-only: use footer links or `/docs/public-api-access` endpoint list.
|
||||
- Higher limits / RPC: connect wallet on `/wallet`, open `/access`, create scoped keys (tier, product, expiry, quota).
|
||||
|
||||
## Operator
|
||||
|
||||
- No nginx key gate required until rate-limit policy changes.
|
||||
- Support contact: `support@d-bis.org`
|
||||
19
docs/TOKEN_LIST_SURFACES.md
Normal file
19
docs/TOKEN_LIST_SURFACES.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Token list surfaces
|
||||
|
||||
The explorer uses two public token-list endpoints. Application code should pick the list through `getTokenListForSurface()` / `tokensApi.listForSurface()` rather than hard-coding `/api/config/token-list`.
|
||||
|
||||
| Surface | Endpoint | Use when |
|
||||
|---------|----------|----------|
|
||||
| `wallet` | `/api/v1/report/token-list?chainId=138` (fallback: config) | Wallet SSR, MetaMask watch list, featured-token dedup inputs |
|
||||
| `catalog` | report (fallback: config) | `/tokens`, search token inference, homepage price feed curation |
|
||||
| `extended` | `/api/config/token-list` | Full Metamask dual-chain catalog, provenance lookup merge, operations/liquidity/system/pools inventory |
|
||||
|
||||
Report list is the canonical Chain 138 trading set (31 tokens live). Config list is the extended catalog (190+ entries across chains).
|
||||
|
||||
## Page mapping
|
||||
|
||||
| Page / surface | Surface | Notes |
|
||||
|----------------|---------|-------|
|
||||
| `/wallet` | `wallet` | SSR + MetaMask watch list |
|
||||
| `/tokens`, `/search`, homepage price feed | `catalog` | Canonical trading set with config fallback |
|
||||
| `/liquidity`, `/operations`, `/system`, `/pools` | `extended` | Full catalog with `TokenListSurfaceNote` label |
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,6 +11,15 @@ describe('resolveExplorerApiBase', () => {
|
||||
).toBe('https://blockscout.defi-oracle.io')
|
||||
})
|
||||
|
||||
it('uses browser HTTPS origin when an explicit same-host HTTP value is present', () => {
|
||||
expect(
|
||||
resolveExplorerApiBase({
|
||||
envValue: 'http://explorer.d-bis.org',
|
||||
browserOrigin: 'https://explorer.d-bis.org',
|
||||
})
|
||||
).toBe('https://explorer.d-bis.org')
|
||||
})
|
||||
|
||||
it('falls back to same-origin in the browser when env is empty', () => {
|
||||
expect(
|
||||
resolveExplorerApiBase({
|
||||
|
||||
@@ -4,19 +4,35 @@ function normalizeApiBase(value: string | null | undefined): string {
|
||||
return (value || '').trim().replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function preferBrowserOriginForSameHost(explicitBase: string, browserOrigin: string): string {
|
||||
if (!explicitBase || !browserOrigin) return explicitBase
|
||||
|
||||
try {
|
||||
const explicitUrl = new URL(explicitBase)
|
||||
const browserUrl = new URL(browserOrigin)
|
||||
if (explicitUrl.hostname === browserUrl.hostname && explicitUrl.protocol !== browserUrl.protocol) {
|
||||
return browserOrigin
|
||||
}
|
||||
} catch {
|
||||
return explicitBase
|
||||
}
|
||||
|
||||
return explicitBase
|
||||
}
|
||||
|
||||
export function resolveExplorerApiBase(options: {
|
||||
envValue?: string | null
|
||||
browserOrigin?: string | null
|
||||
serverFallback?: string
|
||||
} = {}): string {
|
||||
const explicitBase = normalizeApiBase(options.envValue ?? process.env.NEXT_PUBLIC_API_URL ?? '')
|
||||
if (explicitBase) {
|
||||
return explicitBase
|
||||
}
|
||||
|
||||
const browserOrigin = normalizeApiBase(
|
||||
options.browserOrigin ?? (typeof window !== 'undefined' ? window.location.origin : '')
|
||||
)
|
||||
if (explicitBase) {
|
||||
return preferBrowserOriginForSameHost(explicitBase, browserOrigin)
|
||||
}
|
||||
|
||||
if (browserOrigin) {
|
||||
return browserOrigin
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { getKnownDisplayName } from '@/utils/web3IdentityRegistry'
|
||||
import { resolveEnsName } from '@/utils/ens'
|
||||
|
||||
interface AddressProps {
|
||||
address: string
|
||||
chainId?: number
|
||||
/** Blockscout or caller-provided label (highest precedence). */
|
||||
label?: string | null
|
||||
showCopy?: boolean
|
||||
showENS?: boolean
|
||||
truncate?: boolean
|
||||
@@ -12,14 +16,30 @@ interface AddressProps {
|
||||
|
||||
export function Address({
|
||||
address,
|
||||
chainId,
|
||||
chainId: _chainId,
|
||||
label,
|
||||
showCopy = true,
|
||||
showENS = false,
|
||||
truncate = false,
|
||||
className,
|
||||
}: AddressProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [ensName, setEnsName] = useState<string | null>(null)
|
||||
|
||||
const registryLabel = getKnownDisplayName(address)
|
||||
|
||||
useEffect(() => {
|
||||
if (!showENS || !address) return
|
||||
let active = true
|
||||
void resolveEnsName(address).then((name) => {
|
||||
if (active) setEnsName(name)
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [address, showENS])
|
||||
|
||||
const primaryLabel = label || registryLabel || (showENS ? ensName : null) || null
|
||||
const displayAddress = truncate
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
: address
|
||||
@@ -42,14 +62,20 @@ export function Address({
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
'min-w-0 font-mono text-sm leading-6 text-gray-900 dark:text-gray-100',
|
||||
truncate ? 'truncate' : 'break-all'
|
||||
)}
|
||||
>
|
||||
{displayAddress}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
{primaryLabel ? (
|
||||
<div className="truncate text-sm font-medium text-gray-900 dark:text-gray-100">{primaryLabel}</div>
|
||||
) : null}
|
||||
<span
|
||||
className={clsx(
|
||||
'min-w-0 font-mono text-sm leading-6 text-gray-900 dark:text-gray-100',
|
||||
truncate ? 'truncate' : 'break-all',
|
||||
primaryLabel ? 'text-xs text-gray-500 dark:text-gray-400' : '',
|
||||
)}
|
||||
>
|
||||
{displayAddress}
|
||||
</span>
|
||||
</div>
|
||||
{showCopy && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -11,12 +11,12 @@ export function Card({ children, className, title }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-xl bg-white p-4 shadow-md dark:bg-gray-800 sm:p-6',
|
||||
'rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70 sm:p-5',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white sm:mb-4 sm:text-xl">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-900 dark:text-white sm:text-lg">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,11 @@ interface TableProps<T> {
|
||||
data: T[]
|
||||
className?: string
|
||||
emptyMessage?: string
|
||||
/**
|
||||
* responsive: stacked cards below `md`, table at md+.
|
||||
* tabular: always use columnar HTML table (holder lists, dense numeric tables).
|
||||
*/
|
||||
layout?: 'responsive' | 'tabular'
|
||||
/** Stable key for each row (e.g. row => row.id or row => row.hash). Falls back to index if not provided. */
|
||||
keyExtractor?: (row: T) => string | number
|
||||
}
|
||||
@@ -21,6 +26,7 @@ export function Table<T>({
|
||||
data,
|
||||
className,
|
||||
emptyMessage = 'No data available right now.',
|
||||
layout = 'responsive',
|
||||
keyExtractor,
|
||||
}: TableProps<T>) {
|
||||
if (data.length === 0) {
|
||||
@@ -36,9 +42,12 @@ export function Table<T>({
|
||||
)
|
||||
}
|
||||
|
||||
const stackedClass = layout === 'tabular' ? 'hidden' : 'grid gap-3 md:hidden'
|
||||
const tableWrapperClass = layout === 'tabular' ? 'overflow-x-auto' : 'hidden overflow-x-auto md:block'
|
||||
|
||||
return (
|
||||
<div className={clsx('space-y-3', className)}>
|
||||
<div className="grid gap-3 md:hidden">
|
||||
<div className={stackedClass}>
|
||||
{data.map((row, rowIndex) => (
|
||||
<div
|
||||
key={keyExtractor ? keyExtractor(row) : rowIndex}
|
||||
@@ -60,7 +69,7 @@ export function Table<T>({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden overflow-x-auto md:block">
|
||||
<div className={tableWrapperClass}>
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
|
||||
3
frontend/next-env.d.ts
vendored
3
frontend/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
const path = require('path')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
outputFileTracingRoot: path.resolve(__dirname, '..', '..'),
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/address/:address',
|
||||
destination: '/addresses/:address',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/tx/:hash',
|
||||
destination: '/transactions/:hash',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/more',
|
||||
destination: '/operations',
|
||||
@@ -19,6 +32,16 @@ const nextConfig = {
|
||||
destination: '/docs/transaction-review',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/topology',
|
||||
destination: '/chain138-command-center.html',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/command-center',
|
||||
destination: '/chain138-command-center.html',
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
// If you see a workspace lockfile warning: align on one package manager (npm or pnpm) in frontend, or ignore for dev/build.
|
||||
|
||||
19151
frontend/package-lock.json
generated
19151
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,22 +12,24 @@
|
||||
"build": "next build",
|
||||
"build:check": "npm run lint && npm run type-check && npm run build",
|
||||
"smoke:routes": "node ./scripts/smoke-routes.mjs",
|
||||
"smoke:scroll": "node ./scripts/smoke-scroll-height.mjs",
|
||||
"start": "PORT=${PORT:-3000} node ./scripts/start-standalone.mjs",
|
||||
"start:next": "next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint src libs next.config.js --ext .js,.jsx,.ts,.tsx",
|
||||
"type-check": "tsc --noEmit -p tsconfig.check.json",
|
||||
"test": "npm run lint && npm run type-check",
|
||||
"test": "npm run lint && npm run type-check && npm run test:unit",
|
||||
"test:unit": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.14.2",
|
||||
"@walletconnect/ethereum-provider": "^2.21.10",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.15.2",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^3.0.6",
|
||||
"js-sha3": "^0.9.3",
|
||||
"next": "^14.0.4",
|
||||
"postcss": "^8.4.32",
|
||||
"next": "^15.5.15",
|
||||
"postcss": "^8.5.10",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.3.6",
|
||||
@@ -35,11 +37,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"eslint-config-next": "^15.5.15",
|
||||
"playwright": "1.51.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.6.1"
|
||||
"vitest": "^4.1.5"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.28.0",
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Acknowledgments | SolaceScan</title>
|
||||
<meta name="description" content="Acknowledgments for the SolaceScan Chain 138 explorer.">
|
||||
<title>Acknowledgments | DBIS Explorer</title>
|
||||
<meta name="description" content="Acknowledgments for the DBIS Explorer Chain 138 explorer.">
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; }
|
||||
.shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
|
||||
@@ -19,7 +19,7 @@
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScan Acknowledgments</div>
|
||||
<div class="brand">DBIS Explorer Acknowledgments</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -28,10 +28,10 @@
|
||||
<ul>
|
||||
<li><strong>Blockscout</strong> for explorer indexing and API compatibility.</li>
|
||||
<li><strong>MetaMask</strong> for wallet connectivity and Snap support.</li>
|
||||
<li><strong>Chainlink CCIP</strong> for bridge-related routing, transport, and companion operational surfaces where applicable.</li>
|
||||
<li><strong>Chainlink CCIP</strong> for bridge-related routing, cW public-network representations, and companion operational surfaces where applicable.</li>
|
||||
<li><strong>ethers.js</strong> for wallet and Ethereum interaction support.</li>
|
||||
<li><strong>Font Awesome</strong> for iconography.</li>
|
||||
<li><strong>Next.js</strong> and the frontend contributors supporting the DBIS / Defi Oracle explorer experience.</li>
|
||||
<li><strong>Next.js</strong> and the frontend contributors supporting the DBIS explorer experience.</li>
|
||||
</ul>
|
||||
<p class="muted">If we have missed a contributor or dependency, please let us know at <a href="mailto:support@d-bis.org">support@d-bis.org</a>.</p>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Chain 138 — Visual Command Center</title>
|
||||
<!-- Mermaid: local copy preferred; runtime fallback loader below -->
|
||||
<script src="/thirdparty/mermaid.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
@@ -16,6 +15,9 @@
|
||||
--muted: #94a3b8;
|
||||
--accent: #2563eb;
|
||||
--accent-hover: #1d4ed8;
|
||||
--callout: #1e293b;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
@@ -25,7 +27,32 @@
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
.explorer-chrome {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 1rem;
|
||||
padding: 0.55rem 1rem;
|
||||
background: #0a0e17;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.explorer-chrome a {
|
||||
color: #93c5fd;
|
||||
text-decoration: none;
|
||||
}
|
||||
.explorer-chrome a:hover { text-decoration: underline; }
|
||||
.chrome-brand {
|
||||
font-weight: 700;
|
||||
color: var(--text) !important;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.chrome-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem 0.85rem;
|
||||
}
|
||||
header {
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--header);
|
||||
border-bottom: 1px solid var(--border);
|
||||
@@ -39,7 +66,7 @@
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--muted);
|
||||
max-width: 52rem;
|
||||
max-width: 56rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.toolbar {
|
||||
@@ -54,10 +81,18 @@
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.tabs-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.25rem;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.tab {
|
||||
padding: 0.5rem 0.85rem;
|
||||
@@ -68,6 +103,8 @@
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab:hover {
|
||||
color: var(--text);
|
||||
@@ -79,10 +116,10 @@
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
.toolbar a.back {
|
||||
margin-left: auto;
|
||||
font-size: 0.8125rem;
|
||||
color: #93c5fd;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toolbar a.back:hover { text-decoration: underline; }
|
||||
.content {
|
||||
@@ -99,6 +136,20 @@
|
||||
margin-bottom: 1rem;
|
||||
max-width: 56rem;
|
||||
}
|
||||
.callout {
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.1rem;
|
||||
background: var(--callout);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.55;
|
||||
max-width: 56rem;
|
||||
}
|
||||
.callout strong { color: #e2e8f0; }
|
||||
.callout a { color: #93c5fd; text-decoration: none; }
|
||||
.callout a:hover { text-decoration: underline; }
|
||||
.callout-warn { border-color: #b45309; background: rgba(180, 83, 9, 0.12); }
|
||||
.mermaid-wrap {
|
||||
background: var(--panel);
|
||||
padding: 1.25rem;
|
||||
@@ -114,14 +165,47 @@
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.mermaid-wrap + .mermaid-wrap { margin-top: 0.5rem; }
|
||||
.link-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.link-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
.link-card:hover { border-color: #475569; }
|
||||
.link-card-title { font-weight: 700; margin-bottom: 0.3rem; }
|
||||
.link-card-desc { color: var(--muted); line-height: 1.5; font-size: 0.875rem; }
|
||||
.pool-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8125rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.pool-table th, .pool-table td {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.45rem 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
.pool-table th { color: var(--muted); font-weight: 600; }
|
||||
.pool-table a { color: #93c5fd; text-decoration: none; }
|
||||
.pool-table a:hover { text-decoration: underline; }
|
||||
footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
footer code { color: #a5b4fc; }
|
||||
footer a { color: #93c5fd; }
|
||||
.status-note {
|
||||
margin: 0.75rem 1.25rem 0;
|
||||
padding: 0.85rem 1rem;
|
||||
@@ -132,51 +216,84 @@
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status-note a {
|
||||
color: #93c5fd;
|
||||
text-decoration: none;
|
||||
}
|
||||
.status-note a { color: #93c5fd; text-decoration: none; }
|
||||
.status-note a:hover { text-decoration: underline; }
|
||||
.status-note summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="explorer-chrome" aria-label="Explorer navigation">
|
||||
<a class="chrome-brand" href="/">DBIS Explorer</a>
|
||||
<div class="chrome-links">
|
||||
<a href="/operations">Operations</a>
|
||||
<a href="/bridge">Bridge</a>
|
||||
<a href="/protocols">Protocols</a>
|
||||
<a href="/liquidity">Liquidity</a>
|
||||
<a href="/docs">Docs</a>
|
||||
<a href="/topology">Topology</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header>
|
||||
<h1>Chain 138 — deployment and liquidity topology</h1>
|
||||
<p>Operator-style view of the architecture in <code>docs/02-architecture/SMOM_DBIS_138_FULL_DEPLOYMENT_FLOW_MAP.md</code>. Diagrams are informational only; contract addresses live in explorer config and repo references. The main explorer remains the canonical live operational surface. Deep links: <code>?tab=mission-control</code> or numeric <code>?tab=0</code>–<code>8</code> (slug per tab).</p>
|
||||
<p>
|
||||
Static architecture companion maintained in
|
||||
<code>explorer-monorepo/docs/CHAIN138_VISUAL_TOPOLOGY_SOURCE.md</code>.
|
||||
Diagrams are informational only; live contract addresses and guardrails are on
|
||||
<a href="/protocols">/protocols</a> and the main explorer SPA.
|
||||
Deep links: <code>?tab=mission-control</code> or slugs <code>master</code> … <code>mission-control</code>
|
||||
(numeric <code>?tab=0</code>–<code>8</code>).
|
||||
Bundle: <span id="meta-version">—</span>.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="status-note" id="mermaid-status">
|
||||
Loading local diagram assets. If the local Mermaid bundle is unavailable, the page will try a trusted CDN fallback automatically.
|
||||
</div>
|
||||
|
||||
<div class="status-note" id="command-center-fallback">
|
||||
If diagram rendering is unavailable, use the main explorer operational surfaces directly:
|
||||
<a href="/operations">Operations Hub</a>,
|
||||
<a href="/bridge">Bridge Monitoring</a>,
|
||||
<a href="/routes">Routes</a>,
|
||||
<a href="/system">System</a>,
|
||||
and <a href="/operator">Operator</a>.
|
||||
</div>
|
||||
<details class="status-note" id="page-about">
|
||||
<summary>About this page (live explorer is canonical)</summary>
|
||||
<p id="mermaid-status" style="margin:0.65rem 0 0;">
|
||||
Loading diagram assets from <code>/thirdparty/mermaid.min.js</code>; CDN fallback runs automatically if local Mermaid is unavailable.
|
||||
</p>
|
||||
<p style="margin:0.65rem 0 0;">
|
||||
Operational surfaces:
|
||||
<a href="/operations">Operations hub</a>,
|
||||
<a href="/bridge">Bridge</a>,
|
||||
<a href="/routes">Routes</a>,
|
||||
<a href="/liquidity">Liquidity</a>,
|
||||
<a href="/pools">Pools</a>,
|
||||
<a href="/protocols">Protocols</a>,
|
||||
<a href="/analytics">Analytics</a>,
|
||||
<a href="/system">System</a>,
|
||||
<a href="/operator">Operator</a>,
|
||||
<a href="/access">Access</a>,
|
||||
<a href="/docs">Docs</a>,
|
||||
<a href="/snap/">MetaMask Snap</a>.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="tabs" role="tablist" aria-label="Topology panels">
|
||||
<button type="button" id="tab-0" class="tab active" role="tab" aria-selected="true" aria-controls="panel-0" data-tab="0" tabindex="0">Master map</button>
|
||||
<button type="button" id="tab-1" class="tab" role="tab" aria-selected="false" aria-controls="panel-1" data-tab="1" tabindex="-1">Network</button>
|
||||
<button type="button" id="tab-2" class="tab" role="tab" aria-selected="false" aria-controls="panel-2" data-tab="2" tabindex="-1">Stack</button>
|
||||
<button type="button" id="tab-3" class="tab" role="tab" aria-selected="false" aria-controls="panel-3" data-tab="3" tabindex="-1">Flows</button>
|
||||
<button type="button" id="tab-4" class="tab" role="tab" aria-selected="false" aria-controls="panel-4" data-tab="4" tabindex="-1">Cross-chain</button>
|
||||
<button type="button" id="tab-5" class="tab" role="tab" aria-selected="false" aria-controls="panel-5" data-tab="5" tabindex="-1">Public cW</button>
|
||||
<button type="button" id="tab-6" class="tab" role="tab" aria-selected="false" aria-controls="panel-6" data-tab="6" tabindex="-1">Off-chain</button>
|
||||
<button type="button" id="tab-7" class="tab" role="tab" aria-selected="false" aria-controls="panel-7" data-tab="7" tabindex="-1">Integrations</button>
|
||||
<button type="button" id="tab-8" class="tab" role="tab" aria-selected="false" aria-controls="panel-8" data-tab="8" tabindex="-1">Mission Control</button>
|
||||
<div class="tabs-wrap">
|
||||
<div class="tabs" role="tablist" aria-label="Topology panels">
|
||||
<button type="button" id="tab-0" class="tab active" role="tab" aria-selected="true" aria-controls="panel-0" data-tab="0" tabindex="0">Master map</button>
|
||||
<button type="button" id="tab-1" class="tab" role="tab" aria-selected="false" aria-controls="panel-1" data-tab="1" tabindex="-1">Network</button>
|
||||
<button type="button" id="tab-2" class="tab" role="tab" aria-selected="false" aria-controls="panel-2" data-tab="2" tabindex="-1">Stack</button>
|
||||
<button type="button" id="tab-3" class="tab" role="tab" aria-selected="false" aria-controls="panel-3" data-tab="3" tabindex="-1">Flows</button>
|
||||
<button type="button" id="tab-4" class="tab" role="tab" aria-selected="false" aria-controls="panel-4" data-tab="4" tabindex="-1">Cross-chain</button>
|
||||
<button type="button" id="tab-5" class="tab" role="tab" aria-selected="false" aria-controls="panel-5" data-tab="5" tabindex="-1">Public cW</button>
|
||||
<button type="button" id="tab-6" class="tab" role="tab" aria-selected="false" aria-controls="panel-6" data-tab="6" tabindex="-1">Off-chain</button>
|
||||
<button type="button" id="tab-7" class="tab" role="tab" aria-selected="false" aria-controls="panel-7" data-tab="7" tabindex="-1">Integrations</button>
|
||||
<button type="button" id="tab-8" class="tab" role="tab" aria-selected="false" aria-controls="panel-8" data-tab="8" tabindex="-1">Mission Control</button>
|
||||
</div>
|
||||
</div>
|
||||
<a class="back" href="/operations">Back to Operations</a>
|
||||
<a class="back" href="/operations">Operations hub</a>
|
||||
</div>
|
||||
|
||||
<!-- 0 Master -->
|
||||
<div class="content active" id="panel-0" role="tabpanel" aria-labelledby="tab-0">
|
||||
<p class="panel-desc">Hub, leaf endings, CCIP destinations, Alltra, the dedicated Avalanche cW corridor, the public cW mesh, and pending programs. Mainnet cW mint corridors and the optional TRUU rail are summarized under the Ethereum anchor.</p>
|
||||
<p class="panel-desc">Hub, leaf endings, CCIP destinations, Alltra, Avalanche cW corridor, public cW mesh, and pending programs. Mainnet cW mint corridors and optional TRUU sit under the Ethereum anchor.</p>
|
||||
<div class="mermaid-wrap"><div class="mermaid" id="g-master">
|
||||
flowchart TB
|
||||
subgraph LEAF_INGRESS["Leaves — access to 138"]
|
||||
@@ -187,44 +304,44 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph LEAF_EDGE["Leaves — services that index or front 138"]
|
||||
EXP[Explorer · Blockscout · token-aggregation]
|
||||
EXP[explorer.d-bis.org · blockscout.defi-oracle.io · token-aggregation]
|
||||
INFO[info.defi-oracle.io]
|
||||
DAPP[dapp.d-bis.org bridge UI]
|
||||
DAPP[dapp.d-bis.org bridge UI · atomic-swap.defi-oracle.io]
|
||||
DBIS[dbis-api Core hosts]
|
||||
X402[x402 payment API]
|
||||
MCP[MCP PMM controller]
|
||||
end
|
||||
|
||||
subgraph HUB["CHAIN 138 — origin hub"]
|
||||
C138["Besu EVM · tokens core · DODO PMM V2/V3 · RouterV2 · UniV3 / Balancer / Curve / 1inch pilots · CCIP bridges + router · AlltraAdapter · BridgeVault · ISO channels · mirror reserve vault settlement · Lockbox · Truth / Tron / Solana adapters"]
|
||||
C138["Besu EVM · tokens · DODO PMM Stack A · EnhancedSwapRouter live · UniV3 / Balancer / Curve / 1inch pilots · CCIP · AlltraAdapter · BridgeVault · ISO · reserve vault · Lockbox · Truth / Tron / Solana adapters"]
|
||||
end
|
||||
|
||||
subgraph CCIP_ETH["Ethereum 1 — CCIP anchor"]
|
||||
ETH1["WETH9 / WETH10 bridges · CCIPRelayRouter · RelayBridge · Logger · optional trustless stack"]
|
||||
LEAF_ETH["Leaf — Mainnet native DEX venues · Li.Fi touchpoints on other chains · first-wave cW DODO pools · optional TRUU PMM rail"]
|
||||
LEAF_ETH["Mainnet DEX · Li.Fi touchpoints · first-wave cW DODO pools · optional TRUU PMM rail"]
|
||||
end
|
||||
|
||||
subgraph CCIP_L2["Other live CCIP EVM destinations"]
|
||||
L2CLU["OP 10 · Base 8453 · Arb 42161 · Polygon 137 · BSC 56 · Avax 43114 · Gnosis 100 · Celo 42220 · Cronos 25"]
|
||||
LEAF_L2["Leaf — per-chain native DEX · cW token transport · partial edge pools"]
|
||||
LEAF_L2["Per-chain native DEX · cW representation · partial edge pools"]
|
||||
end
|
||||
|
||||
subgraph ALLTRA["ALL Mainnet 651940"]
|
||||
A651["AlltraAdapter peer · AUSDT · WETH · WALL · HYDX · DEX env placeholders"]
|
||||
LEAF_651["Leaf — ALL native venues when configured"]
|
||||
LEAF_651["ALL native venues when configured"]
|
||||
end
|
||||
|
||||
subgraph SPECIAL["Dedicated corridor from 138"]
|
||||
AVAXCW["138 cUSDT to Avax cWUSDT mint path"]
|
||||
LEAF_AVAX["Leaf — recipient on 43114"]
|
||||
LEAF_AVAX["Recipient on 43114"]
|
||||
end
|
||||
|
||||
subgraph CW_MESH["Public cW GRU mesh"]
|
||||
CW["Cross-public-EVM token matrix · pool design · Mainnet DODO concentration"]
|
||||
end
|
||||
|
||||
subgraph PENDING["Pending separate scaffold"]
|
||||
WEMIX[Wemix 1111 CCIP pending]
|
||||
subgraph PENDING["Pending or degraded scaffold"]
|
||||
WEMIX[Wemix 1111 CCIP lane degraded]
|
||||
XDC[XDC Zero parallel program]
|
||||
SCAFF[Etherlink Tezos OP L2 design]
|
||||
PNON[Truth pointer · Tron adapter · Solana partial]
|
||||
@@ -265,13 +382,14 @@ flowchart TB
|
||||
|
||||
<!-- 1 Network -->
|
||||
<div class="content" id="panel-1" role="tabpanel" aria-labelledby="tab-1" hidden>
|
||||
<p class="panel-desc">Chain 138 to the public EVM mesh, Alltra, pending or scaffold targets, Avalanche cW minting, and the separate Mainnet cW mint corridor that sits alongside the standard WETH-class CCIP rail.</p>
|
||||
<p class="panel-desc">Chain 138 to the public EVM mesh, Alltra, scaffold targets, Avalanche cW minting, and the Mainnet cW mint corridor alongside the WETH-class CCIP rail.</p>
|
||||
<div class="mermaid-wrap"><div class="mermaid">
|
||||
flowchart TB
|
||||
subgraph C138["Chain 138 — primary"]
|
||||
CORE[Core registry vault oracle ISO router]
|
||||
PMM[DODO PMM V2 DVM + pools]
|
||||
R2[EnhancedSwapRouterV2]
|
||||
PMM[DODO PMM Stack A V2 DVM + pools]
|
||||
R1[EnhancedSwapRouter live]
|
||||
R2P[EnhancedSwapRouterV2 planned]
|
||||
D3[D3MM pilot]
|
||||
CCIPB[CCIP WETH9 WETH10 bridges]
|
||||
ALLA[AlltraAdapter]
|
||||
@@ -292,9 +410,9 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph PEND["Pending or separate"]
|
||||
WEMIX[Wemix 1111 CCIP pending]
|
||||
WEMIX[Wemix 1111 lane degraded]
|
||||
XDC[XDC Zero parallel program]
|
||||
SCAFF[Etherlink Tezos OP L2 scaffold design]
|
||||
SCAFF[Etherlink Tezos OP L2 scaffold]
|
||||
end
|
||||
|
||||
subgraph A651["ALL Mainnet 651940"]
|
||||
@@ -312,13 +430,32 @@ flowchart TB
|
||||
C138 -.->|future gated| SCAFF
|
||||
|
||||
C138 -->|avax cw corridor| E43114
|
||||
R1 -.->|Phase 4b| R2P
|
||||
</div></div>
|
||||
<p class="panel-desc">Topology note: Mainnet now represents two Ethereum-facing patterns in production, the standard WETH-class CCIP rail and the dedicated <code>cUSDC/cUSDT -> cWUSDC/cWUSDT</code> mint corridor.</p>
|
||||
<p class="panel-desc">Topology note: Mainnet has two Ethereum-facing patterns — standard WETH-class CCIP and the dedicated <code>cUSDC/cUSDT → cWUSDC/cWUSDT</code> mint corridor.</p>
|
||||
</div>
|
||||
|
||||
<!-- 2 Stack -->
|
||||
<div class="content" id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
|
||||
<p class="panel-desc">On-chain layers: tokens, core, liquidity, cross-domain, reserve and settlement.</p>
|
||||
|
||||
<div class="callout callout-warn">
|
||||
<strong>PMM Stack A (production).</strong>
|
||||
Wire dApps and routers to
|
||||
<code>DODOPMMIntegration</code> <code>0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895</code>
|
||||
and <code>DODOPMMProvider</code> <code>0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e</code>.
|
||||
</div>
|
||||
<div class="callout callout-warn">
|
||||
<strong>Do not wire Stack B</strong> parallel integration
|
||||
<code>0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d</code> — seeded but un-traded pools.
|
||||
See <a href="/protocols">official protocol registry</a>.
|
||||
</div>
|
||||
<div class="callout">
|
||||
<strong>EnhancedSwapRouter (live)</strong> <code>0xE6Cc7643ae2A4C720A28D8263BC4972905d7DE0f</code> —
|
||||
DODO Stack A active; UniV3/Balancer/1inch slots wired per-pair.
|
||||
<strong>EnhancedSwapRouterV2</strong> is planned (Phase 4b), not the default production router.
|
||||
</div>
|
||||
|
||||
<div class="mermaid-wrap"><div class="mermaid">
|
||||
flowchart TB
|
||||
subgraph L1["Tokens and compliance"]
|
||||
@@ -333,10 +470,11 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph L3["Liquidity and execution"]
|
||||
DVM[DVMFactory VendingMachine DODOPMMIntegration]
|
||||
DVM[Stack A DVMFactory VendingMachine DODOPMMIntegration]
|
||||
PRV[DODOPMMProvider PrivatePoolRegistry]
|
||||
R2[EnhancedSwapRouterV2]
|
||||
VEN[Uniswap v3 lane Balancer Curve 1inch pilots]
|
||||
R1[EnhancedSwapRouter live]
|
||||
R2P[EnhancedSwapRouterV2 planned]
|
||||
VEN[Uniswap v3 Balancer Curve 1inch pilots]
|
||||
D3[D3Oracle D3Vault D3Proxy D3MMFactory]
|
||||
end
|
||||
|
||||
@@ -355,17 +493,48 @@ flowchart TB
|
||||
|
||||
L1 --> L2
|
||||
L2 --> L3
|
||||
L3 --> R2
|
||||
R2 --> VEN
|
||||
L3 --> R1
|
||||
R1 --> VEN
|
||||
R1 -.-> R2P
|
||||
L2 --> L4
|
||||
L2 --> L5
|
||||
DVM --> PRV
|
||||
</div></div>
|
||||
|
||||
<div class="callout">
|
||||
<strong>Compliance & evidence (explorer surfaces).</strong>
|
||||
<code>ISO20022Router</code> on-chain aligns with ISO-20022 posture badges —
|
||||
<a href="/docs/posture-glossary">posture glossary</a>.
|
||||
x402 readiness (EIP-712, ERC-5267, permit/3009) —
|
||||
<a href="/docs/gru">GRU guide</a> and <a href="/search?q=cUSDT">token/search filters</a>.
|
||||
Transaction evidence scoring —
|
||||
<a href="/docs/transaction-review">transaction review matrix</a>.
|
||||
Li* RWA factory is separate from c*→cW* transport —
|
||||
<a href="/protocols/rwa_token_factory">RWA token factory registry</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3 Flows -->
|
||||
<div class="content" id="panel-3" role="tabpanel" aria-labelledby="tab-3" hidden>
|
||||
<p class="panel-desc">Same-chain 138: PMM pools, RouterV2 venues, D3 pilot.</p>
|
||||
<p class="panel-desc">Same-chain 138: Stack A PMM pools, EnhancedSwapRouter (live), D3 pilot. Live pool inventory: <a href="/pools">/pools</a>.</p>
|
||||
|
||||
<div class="mermaid-wrap">
|
||||
<h3>Stack A PMM pools (live, traded)</h3>
|
||||
<table class="pool-table">
|
||||
<thead><tr><th>Pair</th><th>Pool</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>cUSDT/cUSDC</td><td><a href="/pools/0x9e89bAe009adf128782E19e8341996c596ac40dC">0x9e89…40dC</a></td></tr>
|
||||
<tr><td>cUSDT/USDT</td><td><a href="/pools/0x866Cb44b59303d8dc5f4F9E3E7A8e8b0bf238d66">0x866C…8d66</a></td></tr>
|
||||
<tr><td>cUSDC/USDC</td><td><a href="/pools/0xc39B7D0F40838cbFb54649d327f49a6DAC964062">0xc39B…4062</a></td></tr>
|
||||
<tr><td>cBTC/cUSDT</td><td><a href="/pools/0x67049e7333481e2cac91af61403ac7bddfab7bcd">0x6704…7bcd</a></td></tr>
|
||||
<tr><td>cBTC/cUSDC</td><td><a href="/pools/0x72f1a0794153c3b8a1e8a731f1d8e1a52cb10dc5">0x72f1…0dc5</a></td></tr>
|
||||
<tr><td>WETH/USDC</td><td><a href="/pools/0xb53a0508940b1ff90f1aad4f6cb50a7012fe5593">0xb53a…5593</a></td></tr>
|
||||
<tr><td>WETH/USDT</td><td><a href="/pools/0xe227f6c0520c0c6e8786fe56fa76c4914f861533">0xe227…1533</a></td></tr>
|
||||
<tr><td>cBTC/cXAUC</td><td><a href="/pools/0xf3e8a07d419b61f002114e64d79f7cf8f7989433">0xf3e8…9433</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mermaid-wrap"><div class="mermaid">
|
||||
flowchart LR
|
||||
subgraph inputs["Typical inputs"]
|
||||
@@ -377,13 +546,13 @@ flowchart LR
|
||||
U6[cXAUC]
|
||||
end
|
||||
|
||||
subgraph path_pmm["DODO PMM"]
|
||||
INT[DODOPMMIntegration]
|
||||
subgraph path_pmm["DODO PMM Stack A"]
|
||||
INT[DODOPMMIntegration 0x86ADA6Ef]
|
||||
POOL[Stable pools XAU public pools Private XAU registry]
|
||||
end
|
||||
|
||||
subgraph path_r2["Router v2"]
|
||||
R2[EnhancedSwapRouterV2]
|
||||
subgraph path_r1["EnhancedSwapRouter live"]
|
||||
R1[EnhancedSwapRouter 0xE6Cc7643]
|
||||
UV3[Uniswap v3 WETH stable]
|
||||
PILOT[Balancer Curve 1inch]
|
||||
end
|
||||
@@ -394,19 +563,45 @@ flowchart LR
|
||||
|
||||
inputs --> INT
|
||||
INT --> POOL
|
||||
inputs --> R2
|
||||
R2 --> UV3
|
||||
R2 --> PILOT
|
||||
GEN2[WETH WETH10] --> R2
|
||||
inputs --> R1
|
||||
R1 --> UV3
|
||||
R1 --> PILOT
|
||||
GEN2[WETH WETH10] --> R1
|
||||
GEN2 --> D3
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 4 Cross-chain -->
|
||||
<div class="content" id="panel-4" role="tabpanel" aria-labelledby="tab-4" hidden>
|
||||
<p class="panel-desc">CCIP transport, Alltra round-trip, the dedicated c-to-cW mint corridors, and the orchestrated swap-bridge-swap target.</p>
|
||||
<p class="panel-desc">CCIP routing, managed relay fleet, Alltra round-trip, c→cW mint corridors, and swap-bridge-swap orchestration. Live relay posture: <a href="/bridge">/bridge</a>.</p>
|
||||
|
||||
<div class="mermaid-wrap">
|
||||
<h3>CCIP — WETH primary transport</h3>
|
||||
<h3>Managed CCIP relay fleet (explorer /bridge)</h3>
|
||||
<div class="mermaid">
|
||||
flowchart TB
|
||||
C138[Chain 138 hub]
|
||||
|
||||
subgraph RELAYS["Managed relay lanes"]
|
||||
MW[mainnet_weth]
|
||||
MC[mainnet_cw]
|
||||
BSC[bsc]
|
||||
AV[avax]
|
||||
AVC[avax_cw]
|
||||
A138[avax_to_138]
|
||||
end
|
||||
|
||||
C138 --> MW
|
||||
C138 --> MC
|
||||
C138 --> BSC
|
||||
C138 --> AV
|
||||
C138 --> AVC
|
||||
AVC --> A138
|
||||
A138 --> C138
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mermaid-wrap">
|
||||
<h3>CCIP — WETH primary routing lane</h3>
|
||||
<div class="mermaid">
|
||||
sequenceDiagram
|
||||
participant U as User or bot
|
||||
@@ -454,7 +649,17 @@ flowchart LR
|
||||
|
||||
<!-- 5 Public cW -->
|
||||
<div class="content" id="panel-5" role="tabpanel" aria-labelledby="tab-5" hidden>
|
||||
<p class="panel-desc">Ethereum Mainnet first-wave cW DODO mesh, plus the separate optional TRUU PMM rail. See PMM_DEX_ROUTING_STATUS and cross-chain-pmm-lps deployment-status for live detail.</p>
|
||||
<p class="panel-desc">
|
||||
Ethereum Mainnet first-wave cW DODO mesh plus optional TRUU PMM rail.
|
||||
<strong>c*</strong> on Chain 138 is the compliant canonical instrument;
|
||||
<strong>cW*</strong> on public chains is the wrapped transport representation (not Li* RWA).
|
||||
Guide: <a href="/docs/gru">GRU guide</a> · mesh search <a href="/search?q=cWUSDC">cWUSDC</a>.
|
||||
</p>
|
||||
<div class="callout callout-warn">
|
||||
<strong>Mainnet cWUSDC/USDC guardrail.</strong>
|
||||
Do not route cWUSDC→USDC through skewed legacy UniV2 pair <code>0xC28706F8…</code> when peg deviation is high.
|
||||
Prefer DODO / UniV3 repair lanes and explorer <a href="/liquidity">liquidity policy</a> (bridge underlying, not LP).
|
||||
</div>
|
||||
<div class="mermaid-wrap"><div class="mermaid">
|
||||
flowchart TB
|
||||
subgraph ETH["Ethereum Mainnet"]
|
||||
@@ -466,7 +671,7 @@ flowchart TB
|
||||
CW <--> DODO
|
||||
HUB <--> DODO
|
||||
</div></div>
|
||||
<p class="panel-desc">TRUU note: the optional Mainnet Truth rail is a separate volatile PMM lane and is not part of the default cW stable mesh.</p>
|
||||
<p class="panel-desc">TRUU: optional Mainnet Truth volatile PMM lane — not part of the default cW stable mesh.</p>
|
||||
<div class="mermaid-wrap">
|
||||
<h3>Mainnet TRUU PMM (volatile, optional)</h3>
|
||||
<div class="mermaid">
|
||||
@@ -485,20 +690,20 @@ flowchart LR
|
||||
|
||||
<!-- 6 Off-chain -->
|
||||
<div class="content" id="panel-6" role="tabpanel" aria-labelledby="tab-6" hidden>
|
||||
<p class="panel-desc">Wallets, edge FQDNs, APIs, operators feeding Chain 138 RPC, plus the explorer-hosted Mission Control visual surfaces.</p>
|
||||
<p class="panel-desc">Wallets, edge FQDNs, APIs, operators, and explorer-hosted Mission Control surfaces.</p>
|
||||
<div class="mermaid-wrap"><div class="mermaid">
|
||||
flowchart TB
|
||||
subgraph users["Wallets and tools"]
|
||||
MM[MetaMask custom network Snaps]
|
||||
MM[MetaMask custom network · /snap/]
|
||||
MCP[MCP PMM controller allowlist 138]
|
||||
end
|
||||
|
||||
subgraph edge["Public edge"]
|
||||
EXP[explorer.d-bis.org Blockscout token-aggregation]
|
||||
MC[Mission Control visual panels]
|
||||
EXP[explorer.d-bis.org · blockscout.defi-oracle.io]
|
||||
MC[Mission Control panels · /bridge · homepage]
|
||||
INFO[info.defi-oracle.io]
|
||||
DAPP[dapp.d-bis.org bridge UI]
|
||||
RPC[rpc-http-pub.d-bis.org public RPC]
|
||||
DAPP[dapp.d-bis.org · atomic-swap.defi-oracle.io]
|
||||
RPC[rpc-http-pub.d-bis.org]
|
||||
end
|
||||
|
||||
subgraph api["APIs"]
|
||||
@@ -508,7 +713,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph ops["Operator"]
|
||||
REL[CCIP relay systemd]
|
||||
REL[CCIP relay systemd · mainnet_cw bsc avax lanes]
|
||||
SCR[smom-dbis-138 forge scripts]
|
||||
end
|
||||
|
||||
@@ -519,18 +724,21 @@ flowchart TB
|
||||
api --> C138[Chain 138 RPC]
|
||||
ops --> C138
|
||||
</div></div>
|
||||
<p class="panel-desc">Mission Control note: the live visual display lives in the main explorer SPA, especially the bridge-monitoring and operator surfaces. This command center stays focused on the static architecture view.</p>
|
||||
<p class="panel-desc">Live Mission Control UI: <a href="/bridge">bridge monitoring</a>, <a href="/operations">operations hub</a>, homepage Mission Control card. This page stays the static topology reference.</p>
|
||||
</div>
|
||||
|
||||
<!-- 7 Integrations -->
|
||||
<div class="content" id="panel-7" role="tabpanel" aria-labelledby="tab-7" hidden>
|
||||
<p class="panel-desc">Contract families vs wallet/client integrations not spelled out in every zoom diagram. Wormhole remains docs/MCP scope, not canonical 138 addresses.</p>
|
||||
<p class="panel-desc">
|
||||
Contract families vs wallet/client integrations.
|
||||
<strong>Wormhole</strong> appears in the <a href="/protocols">official protocol registry</a> as integration scaffold / docs scope — not a live Chain 138 liquidity rail unless listed as production on <a href="/protocols/wormhole">protocol detail</a>.
|
||||
</p>
|
||||
<div class="mermaid-wrap"><div class="mermaid">
|
||||
flowchart LR
|
||||
subgraph chain138_tech["Chain 138 contract families"]
|
||||
A[Besu EVM]
|
||||
B[ERC-20 core registries]
|
||||
C[DODO V2 V3]
|
||||
C[DODO V2 V3 Stack A]
|
||||
D[UniV3 Bal Curve 1inch pilots]
|
||||
E[CCIP bridges router]
|
||||
F[Alltra Vault ISO channels]
|
||||
@@ -541,7 +749,7 @@ flowchart LR
|
||||
CL[Chainlist]
|
||||
TW[thirdweb RPC]
|
||||
ETH[ethers.js]
|
||||
MM[MetaMask Snaps]
|
||||
MM[MetaMask Snaps /snap/]
|
||||
end
|
||||
|
||||
chain138_tech --> public_integrations
|
||||
@@ -550,7 +758,7 @@ flowchart LR
|
||||
|
||||
<!-- 8 Mission Control -->
|
||||
<div class="content" id="panel-8" role="tabpanel" aria-labelledby="tab-8" hidden>
|
||||
<p class="panel-desc">Mission Control is the live explorer surface for SSE health, labeled bridge traces, cached liquidity proxy results, and operator-facing API references. The interactive controls live in the main explorer SPA; this tab is the architecture companion with direct entry points.</p>
|
||||
<p class="panel-desc">Mission Control: SSE health, bridge traces, liquidity proxy, and operator APIs. Interactive controls live in the explorer SPA; this tab lists entry points.</p>
|
||||
<div class="mermaid-wrap">
|
||||
<h3>Mission Control visual flow</h3>
|
||||
<div class="mermaid">
|
||||
@@ -575,22 +783,54 @@ flowchart LR
|
||||
LIQ --> UP
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mermaid-wrap">
|
||||
<h3>Live entry points</h3>
|
||||
<p class="panel-desc">Use the main explorer UI for the visual Mission Control experience, then open the raw APIs when you need direct payloads or verification.</p>
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem;">
|
||||
<a href="/operator" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--panel);"><div style="font-weight:700; margin-bottom:0.3rem;">Operator hub</div><div style="color:var(--muted); line-height:1.5;">Explorer SPA surface with Mission Control and operator-facing API references.</div></a>
|
||||
<a href="/bridge" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--panel);"><div style="font-weight:700; margin-bottom:0.3rem;">Bridge monitoring</div><div style="color:var(--muted); line-height:1.5;">Includes the visible Mission Control bridge-trace card and SSE stream entry point.</div></a>
|
||||
<a href="/explorer-api/v1/mission-control/stream" target="_blank" rel="noopener noreferrer" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--panel);"><div style="font-weight:700; margin-bottom:0.3rem;">SSE stream</div><div style="color:var(--muted); line-height:1.5;"><code>GET /explorer-api/v1/mission-control/stream</code></div></a>
|
||||
<a href="/explorer-api/v1/mission-control/bridge/trace?tx=0x2f31d4f9a97be754b800f4af1a9eedf3b107d353bfa1a19e81417497a76c05c2" target="_blank" rel="noopener noreferrer" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--panel);"><div style="font-weight:700; margin-bottom:0.3rem;">Bridge trace example</div><div style="color:var(--muted); line-height:1.5;"><code>GET /explorer-api/v1/mission-control/bridge/trace</code></div></a>
|
||||
<a href="/explorer-api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools" target="_blank" rel="noopener noreferrer" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--panel);"><div style="font-weight:700; margin-bottom:0.3rem;">Liquidity example</div><div style="color:var(--muted); line-height:1.5;"><code>GET /explorer-api/v1/mission-control/liquidity/token/{address}/pools</code></div></a>
|
||||
<div style="border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--panel);"><div style="font-weight:700; margin-bottom:0.3rem;">Track 4 script API</div><div style="color:var(--muted); line-height:1.5;"><code>POST /explorer-api/v1/track4/operator/run-script</code><br>Requires wallet auth, IP allowlisting, and backend allowlist config.</div></div>
|
||||
<h3>Explorer operational surfaces</h3>
|
||||
<div class="link-grid">
|
||||
<a class="link-card" href="/operations"><div class="link-card-title">Operations hub</div><div class="link-card-desc">Consolidated monitoring, routes, and topology shortcuts.</div></a>
|
||||
<a class="link-card" href="/bridge"><div class="link-card-title">Bridge monitoring</div><div class="link-card-desc">Relay fleet, CCIP lane health, bridge trace card, SSE.</div></a>
|
||||
<a class="link-card" href="/routes"><div class="link-card-title">Routes</div><div class="link-card-desc">Live route matrix and execution paths.</div></a>
|
||||
<a class="link-card" href="/liquidity"><div class="link-card-title">Liquidity</div><div class="link-card-desc">PMM access, planner capabilities, pool inventory.</div></a>
|
||||
<a class="link-card" href="/pools"><div class="link-card-title">Pools</div><div class="link-card-desc">Mission-control pool inventory snapshot.</div></a>
|
||||
<a class="link-card" href="/protocols"><div class="link-card-title">Protocols</div><div class="link-card-desc">Official DODO, Uni, CCIP, Multicall3 registry.</div></a>
|
||||
<a class="link-card" href="/weth"><div class="link-card-title">WETH utilities</div><div class="link-card-desc">Wrapped-asset references and bridge context.</div></a>
|
||||
<a class="link-card" href="/analytics"><div class="link-card-title">Analytics</div><div class="link-card-desc">Track 3 blocks, transactions, activity summaries.</div></a>
|
||||
<a class="link-card" href="/system"><div class="link-card-title">System</div><div class="link-card-desc">Networks, RPC capabilities, topology inventory.</div></a>
|
||||
<a class="link-card" href="/operator"><div class="link-card-title">Operator</div><div class="link-card-desc">Track 4 relay, route, and planner shortcuts.</div></a>
|
||||
<a class="link-card" href="/access"><div class="link-card-title">Access</div><div class="link-card-desc">Wallet-authenticated Track 3/4 features.</div></a>
|
||||
<a class="link-card" href="/docs"><div class="link-card-title">Documentation</div><div class="link-card-desc">GRU, posture glossary, transaction review, APIs.</div></a>
|
||||
<a class="link-card" href="/snap/"><div class="link-card-title">MetaMask Snap</div><div class="link-card-desc">Snap install path on this explorer domain.</div></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mermaid-wrap">
|
||||
<h3>Public JSON APIs</h3>
|
||||
<div class="link-grid">
|
||||
<a class="link-card" href="/explorer-api/v1/mission-control/stream" target="_blank" rel="noopener noreferrer"><div class="link-card-title">SSE stream</div><div class="link-card-desc"><code>GET /explorer-api/v1/mission-control/stream</code></div></a>
|
||||
<a class="link-card" href="/explorer-api/v1/track1/bridge/status" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Bridge status JSON</div><div class="link-card-desc"><code>GET /explorer-api/v1/track1/bridge/status</code></div></a>
|
||||
<a class="link-card" href="/explorer-api/v1/mission-control/bridge/trace?tx=0x2f31d4f9a97be754b800f4af1a9eedf3b107d353bfa1a19e81417497a76c05c2" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Bridge trace example</div><div class="link-card-desc"><code>GET …/mission-control/bridge/trace</code></div></a>
|
||||
<a class="link-card" href="/explorer-api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Liquidity pools (cUSDT)</div><div class="link-card-desc"><code>GET …/liquidity/token/{address}/pools</code></div></a>
|
||||
<a class="link-card" href="/token-aggregation/api/v1/routes/matrix?includeNonLive=true" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Route matrix</div><div class="link-card-desc">Token-aggregation live and planned routes.</div></a>
|
||||
<a class="link-card" href="/api/v1/report/external-indexer-readiness?chainId=138" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Indexer readiness</div><div class="link-card-desc">External indexer readiness report (chain 138).</div></a>
|
||||
<a class="link-card" href="/token-aggregation/api/v1/report/official-protocols" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Official protocols JSON</div><div class="link-card-desc">Production guardrails and contract list.</div></a>
|
||||
<a class="link-card" href="/api/v2/stats" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Blockscout stats</div><div class="link-card-desc">Chain head, gas, indexer summary.</div></a>
|
||||
<a class="link-card" href="/api/config/networks" target="_blank" rel="noopener noreferrer"><div class="link-card-title">Wallet networks</div><div class="link-card-desc">Published chain metadata for onboarding.</div></a>
|
||||
<a class="link-card" href="/explorer-api/v1/walletconnect/config" target="_blank" rel="noopener noreferrer"><div class="link-card-title">WalletConnect config</div><div class="link-card-desc">WalletConnect v2 posture.</div></a>
|
||||
</div>
|
||||
<p class="panel-desc" style="margin-top:1rem;">
|
||||
<strong>Track 4 script API:</strong>
|
||||
<code>POST /explorer-api/v1/track4/operator/run-script</code> —
|
||||
wallet auth, IP allowlist, and backend allowlist required.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
Source: <code>proxmox/docs/02-architecture/SMOM_DBIS_138_FULL_DEPLOYMENT_FLOW_MAP.md</code> — addresses: <code>config/smart-contracts-master.json</code> and CONTRACT_ADDRESSES_REFERENCE.
|
||||
Source: <code>explorer-monorepo/docs/CHAIN138_VISUAL_TOPOLOGY_SOURCE.md</code> ·
|
||||
Live addresses: <a href="/protocols">/protocols</a>,
|
||||
<a href="/token-aggregation/api/v1/report/official-protocols">official-protocols JSON</a> ·
|
||||
Meta: <a href="/chain138-command-center.meta.json">bundle metadata</a> ·
|
||||
Alias: <a href="/topology">/topology</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -612,6 +852,17 @@ flowchart LR
|
||||
var tablist = document.querySelector('[role="tablist"]');
|
||||
var done = {};
|
||||
|
||||
fetch('/chain138-command-center.meta.json', { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (meta) {
|
||||
var el = document.getElementById('meta-version');
|
||||
if (!el || !meta) return;
|
||||
var parts = [meta.bundleVersion];
|
||||
if (meta.routeMatrixUpdated) parts.push('routes ' + meta.routeMatrixUpdated);
|
||||
el.textContent = parts.join(' · ');
|
||||
})
|
||||
.catch(function () {});
|
||||
|
||||
function parseInitialTab() {
|
||||
var q = new URLSearchParams(window.location.search).get('tab');
|
||||
if (q == null || q === '') return 0;
|
||||
@@ -647,7 +898,7 @@ flowchart LR
|
||||
var u = new URL(window.location.href);
|
||||
u.searchParams.set('tab', slug);
|
||||
history.replaceState(null, '', u.pathname + u.search + u.hash);
|
||||
} catch (e) { /* file:// or restricted */ }
|
||||
} catch (e) { /* file:// */ }
|
||||
}
|
||||
|
||||
function setActive(index) {
|
||||
@@ -677,11 +928,11 @@ flowchart LR
|
||||
await ensureMermaid();
|
||||
await mermaid.run({ nodes: nodes });
|
||||
var status = document.getElementById('mermaid-status');
|
||||
if (status) status.textContent = 'Diagram assets loaded. This page is a public reference surface; the main explorer remains the canonical live operational view.';
|
||||
if (status) status.textContent = 'Diagram assets loaded. Use the main explorer for live operational data.';
|
||||
} catch (e) {
|
||||
console.error('Mermaid render failed for panel', index, e);
|
||||
var statusError = document.getElementById('mermaid-status');
|
||||
if (statusError) statusError.textContent = 'Diagram rendering failed. Use the Operations Hub or the main explorer for live operational surfaces.';
|
||||
if (statusError) statusError.textContent = 'Diagram rendering failed. Open /operations or /bridge for live surfaces.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
frontend/public/chain138-command-center.meta.json
Normal file
6
frontend/public/chain138-command-center.meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "2026-06-26",
|
||||
"updatedAt": "2026-06-26T08:40:23Z",
|
||||
"sourceDoc": "explorer-monorepo/docs/CHAIN138_VISUAL_TOPOLOGY_SOURCE.md",
|
||||
"routeMatrixUpdated": null
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Documentation Redirect | SolaceScan</title>
|
||||
<meta name="description" content="Redirecting to the canonical SolaceScan documentation hub.">
|
||||
<title>Documentation Redirect | DBIS Explorer</title>
|
||||
<meta name="description" content="Redirecting to the canonical DBIS Explorer documentation hub.">
|
||||
<meta http-equiv="refresh" content="0; url=/docs">
|
||||
<link rel="canonical" href="https://blockscout.defi-oracle.io/docs">
|
||||
<style>
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScan Documentation</div>
|
||||
<div class="brand">DBIS Explorer Documentation</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1985
frontend/public/legacy/index.html
Normal file
1985
frontend/public/legacy/index.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Privacy Policy | SolaceScan</title>
|
||||
<meta name="description" content="Privacy policy for the SolaceScan Chain 138 explorer operated by DBIS / Defi Oracle.">
|
||||
<title>Privacy Policy | DBIS Explorer</title>
|
||||
<meta name="description" content="Privacy policy for the DBIS Explorer Chain 138 explorer operated by DBIS.">
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; }
|
||||
.shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
|
||||
@@ -19,13 +19,13 @@
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScan Privacy Policy</div>
|
||||
<div class="brand">DBIS Explorer Privacy Policy</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0;">Privacy Policy</h1>
|
||||
<p class="muted">Last updated: 2026-03-25</p>
|
||||
<p>SolaceScan is the public Chain 138 explorer surface operated by DBIS / Defi Oracle. Most content you view comes from public blockchain data, explorer indexers, route services, and public configuration endpoints. We do not ask for personal information to browse the public explorer.</p>
|
||||
<p>DBIS Explorer is the public Chain 138 explorer surface operated by DBIS. Most content you view comes from public blockchain data, explorer indexers, route services, and public configuration endpoints. We do not ask for personal information to browse the public explorer.</p>
|
||||
<h2>What we store locally</h2>
|
||||
<ul>
|
||||
<li>We may store theme preference, locale, recent searches, and similar local UI settings in your browser.</li>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Terms of Service | SolaceScan</title>
|
||||
<meta name="description" content="Terms of service for the SolaceScan Chain 138 explorer operated by DBIS / Defi Oracle.">
|
||||
<title>Terms of Service | DBIS Explorer</title>
|
||||
<meta name="description" content="Terms of service for the DBIS Explorer Chain 138 explorer operated by DBIS.">
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; }
|
||||
.shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
|
||||
@@ -19,13 +19,13 @@
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScan Terms of Service</div>
|
||||
<div class="brand">DBIS Explorer Terms of Service</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0;">Terms of Service</h1>
|
||||
<p class="muted">Last updated: 2026-03-25</p>
|
||||
<p>SolaceScan is provided for informational and operational purposes by DBIS / Defi Oracle. By using the public explorer, wallet tools, docs, and linked companion resources, you agree that:</p>
|
||||
<p>DBIS Explorer is provided for informational and operational purposes by DBIS. By using the public explorer, wallet tools, docs, and linked companion resources, you agree that:</p>
|
||||
<h2>Service scope</h2>
|
||||
<ul>
|
||||
<li>Blockchain data may be delayed, incomplete, or temporarily unavailable.</li>
|
||||
@@ -55,7 +55,7 @@
|
||||
<li>Bridge, route, liquidity, and operational surfaces are investigative and informational unless a page explicitly presents an authenticated management workflow.</li>
|
||||
</ul>
|
||||
<h2>Operator identity</h2>
|
||||
<p>SolaceScan is operated by DBIS / Defi Oracle. Public explorer access may appear under <code>blockscout.defi-oracle.io</code>, while companion resources may appear under <code>explorer.d-bis.org</code> and related DBIS domains.</p>
|
||||
<p>DBIS Explorer is operated by DBIS. Public explorer access may appear under <code>blockscout.defi-oracle.io</code>, while companion resources may appear under <code>explorer.d-bis.org</code> and related DBIS domains.</p>
|
||||
<h2>Support and notices</h2>
|
||||
<p>For service questions, operational issues, or policy notices, contact <a href="mailto:support@d-bis.org">support@d-bis.org</a>.</p>
|
||||
<h2>Disputes and interpretation</h2>
|
||||
|
||||
18
frontend/public/thirdparty/README.md
vendored
18
frontend/public/thirdparty/README.md
vendored
@@ -2,16 +2,20 @@
|
||||
|
||||
## Mermaid (Visual Command Center)
|
||||
|
||||
`chain138-command-center.html` loads Mermaid from jsDelivr by default. If your explorer host blocks external script origins (CSP) or you need a fully offline doc path:
|
||||
`chain138-command-center.html` loads Mermaid from **`/thirdparty/mermaid.min.js` first**. If the local bundle is missing, the page runtime loads jsDelivr as a fallback.
|
||||
|
||||
To vendor Mermaid for offline / CSP-locked hosts:
|
||||
|
||||
1. From repo root:
|
||||
```bash
|
||||
bash explorer-monorepo/scripts/vendor-mermaid-for-command-center.sh
|
||||
```
|
||||
2. Edit `chain138-command-center.html` and change the Mermaid `<script src="...">` line to:
|
||||
```html
|
||||
<script src="/thirdparty/mermaid.min.js"></script>
|
||||
```
|
||||
3. Deploy with `deploy-frontend-to-vmid5000.sh` — it copies `thirdparty/mermaid.min.js` when the file exists.
|
||||
2. Deploy with `deploy-next-frontend-to-vmid5000.sh` — it copies `thirdparty/mermaid.min.js` when the file exists.
|
||||
|
||||
The minified file is gitignored (~3.3 MB); do not commit it.
|
||||
Before deploy, refresh bundle metadata:
|
||||
|
||||
```bash
|
||||
bash explorer-monorepo/scripts/refresh-chain138-command-center-meta.sh
|
||||
```
|
||||
|
||||
The minified Mermaid file is gitignored (~3.3 MB); do not commit it.
|
||||
|
||||
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 |
@@ -4,15 +4,22 @@ const baseUrl = (process.env.BASE_URL || 'https://explorer.d-bis.org').replace(/
|
||||
const addressUnderTest = process.env.SMOKE_ADDRESS || '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506'
|
||||
|
||||
const checks = [
|
||||
{ path: '/', expectTexts: ['SolaceScan', 'Recent Blocks', 'Open wallet tools'] },
|
||||
{ path: '/', expectTexts: ['DBIS Explorer', 'Institutional quick paths'] },
|
||||
{ path: '/bridge', expectTexts: ['Bridge'] },
|
||||
{ path: '/routes', expectTexts: ['Routes'] },
|
||||
{ path: '/operations', expectTexts: ['Operations'] },
|
||||
{ path: '/system', expectTexts: ['System'] },
|
||||
{ path: '/operator', expectTexts: ['Operator'] },
|
||||
{ path: '/analytics', expectTexts: ['Analytics'] },
|
||||
{ path: '/liquidity', expectTexts: ['Liquidity'] },
|
||||
{ path: '/blocks', expectTexts: ['Blocks'] },
|
||||
{ path: '/transactions', expectTexts: ['Transactions'] },
|
||||
{ path: '/addresses', expectTexts: ['Addresses', 'Open An Address'] },
|
||||
{ path: '/watchlist', expectTexts: ['Watchlist', 'Saved Addresses'] },
|
||||
{ path: '/pools', expectTexts: ['Pools', 'Pool operation shortcuts'] },
|
||||
{ path: '/liquidity', expectTexts: ['Chain 138 Liquidity Access', 'Explorer Access Points'] },
|
||||
{ path: '/wallet', expectTexts: ['Wallet & MetaMask', 'Install Open Snap'] },
|
||||
{ path: '/tokens', expectTexts: ['Tokens', 'Find A Token'] },
|
||||
{ path: '/addresses', expectTexts: ['Addresses', 'Open address'] },
|
||||
{ path: '/watchlist', expectTexts: ['Watchlist', 'Saved addresses'] },
|
||||
{ path: '/pools', expectTexts: ['Pools'] },
|
||||
{ path: '/wallet', expectTexts: ['Wallet Tools', 'WalletConnect'] },
|
||||
{ path: '/access', expectTexts: ['Wallet Login', 'RPC Access'] },
|
||||
{ path: '/tokens', expectTexts: ['Tokens'] },
|
||||
{ path: '/search', expectTexts: ['Search'], placeholder: 'Search by address, transaction hash, block number...' },
|
||||
{ path: `/addresses/${addressUnderTest}`, expectTexts: [], anyOfTexts: ['Back to addresses', 'Address not found'] },
|
||||
]
|
||||
@@ -22,12 +29,19 @@ async function bodyText(page) {
|
||||
}
|
||||
|
||||
async function hasShell(page) {
|
||||
const homeLink = await page.getByRole('link', { name: /Go to explorer home/i }).isVisible().catch(() => false)
|
||||
const supportText = await page.getByText(/Support:/i).isVisible().catch(() => false)
|
||||
return homeLink && supportText
|
||||
const homeLink = await page
|
||||
.getByRole('link', { name: /Go to DBIS Explorer home|Go to explorer home/i })
|
||||
.isVisible()
|
||||
.catch(() => false)
|
||||
const dbisTitle = await page
|
||||
.getByText(/DBIS Explorer/i)
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false)
|
||||
return homeLink || dbisTitle
|
||||
}
|
||||
|
||||
async function waitForBodyText(page, snippets, timeoutMs = 15000) {
|
||||
async function waitForBodyText(page, snippets, timeoutMs = 20000) {
|
||||
if (!snippets || snippets.length === 0) {
|
||||
return bodyText(page)
|
||||
}
|
||||
@@ -56,7 +70,7 @@ async function main() {
|
||||
|
||||
try {
|
||||
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {})
|
||||
|
||||
if (!response || !response.ok()) {
|
||||
console.error(`FAIL ${check.path}: HTTP ${response ? response.status() : 'no-response'}`)
|
||||
|
||||
90
frontend/scripts/smoke-scroll-height.mjs
Normal file
90
frontend/scripts/smoke-scroll-height.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const baseUrl = (process.env.BASE_URL || 'https://explorer.d-bis.org').replace(/\/$/, '')
|
||||
const viewportHeight = Number(process.env.SMOKE_VIEWPORT_HEIGHT || 720)
|
||||
const maxViewportHeights = Number(process.env.SMOKE_MAX_VIEWPORT_HEIGHTS || 2.5)
|
||||
|
||||
const routeLimits = new Map(
|
||||
(process.env.SMOKE_SCROLL_ROUTE_LIMITS || ' /=4,/operator=4,/transactions=3')
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => {
|
||||
const [path, limit] = entry.split('=')
|
||||
return [path, Number(limit)]
|
||||
}),
|
||||
)
|
||||
|
||||
function limitForRoute(path) {
|
||||
return routeLimits.get(path) ?? maxViewportHeights
|
||||
}
|
||||
|
||||
const routes = [
|
||||
'/',
|
||||
'/bridge',
|
||||
'/routes',
|
||||
'/operations',
|
||||
'/system',
|
||||
'/operator',
|
||||
'/analytics',
|
||||
'/liquidity',
|
||||
'/blocks',
|
||||
'/transactions',
|
||||
'/addresses',
|
||||
'/wallet',
|
||||
'/access',
|
||||
]
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1280, height: viewportHeight },
|
||||
})
|
||||
let failures = 0
|
||||
|
||||
for (const path of routes) {
|
||||
const url = `${baseUrl}${path}`
|
||||
|
||||
try {
|
||||
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
if (!response || !response.ok()) {
|
||||
console.error(`FAIL ${path}: HTTP ${response ? response.status() : 'no-response'}`)
|
||||
failures += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight)
|
||||
const routeLimit = limitForRoute(path)
|
||||
const limit = viewportHeight * routeLimit
|
||||
|
||||
if (scrollHeight > limit) {
|
||||
console.error(
|
||||
`FAIL ${path}: scrollHeight ${scrollHeight}px exceeds ${routeLimit}× viewport (${Math.round(limit)}px)`,
|
||||
)
|
||||
failures += 1
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(`OK ${path} (${scrollHeight}px)`)
|
||||
} catch (error) {
|
||||
console.error(`FAIL ${path}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
failures += 1
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
|
||||
if (failures > 0) {
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Scroll-height smoke passed for ${routes.length} routes at ${viewportHeight}px viewport`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error))
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -7,7 +7,20 @@ import process from 'node:process'
|
||||
const projectRoot = process.cwd()
|
||||
const standaloneRoot = path.join(projectRoot, '.next', 'standalone')
|
||||
const standaloneNextRoot = path.join(standaloneRoot, '.next')
|
||||
const standaloneServer = path.join(standaloneRoot, 'server.js')
|
||||
|
||||
function resolveStandaloneServer() {
|
||||
const directServer = path.join(standaloneRoot, 'server.js')
|
||||
if (existsSync(directServer)) {
|
||||
return { serverPath: directServer, appRoot: standaloneRoot }
|
||||
}
|
||||
|
||||
const nestedServer = path.join(standaloneRoot, 'explorer-monorepo', 'frontend', 'server.js')
|
||||
if (existsSync(nestedServer)) {
|
||||
return { serverPath: nestedServer, appRoot: path.dirname(nestedServer) }
|
||||
}
|
||||
|
||||
return { serverPath: directServer, appRoot: standaloneRoot }
|
||||
}
|
||||
|
||||
async function copyIfPresent(sourcePath, destinationPath) {
|
||||
if (!existsSync(sourcePath)) {
|
||||
@@ -19,15 +32,16 @@ async function copyIfPresent(sourcePath, destinationPath) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!existsSync(standaloneServer)) {
|
||||
const { serverPath, appRoot } = resolveStandaloneServer()
|
||||
if (!existsSync(serverPath)) {
|
||||
console.error('Standalone server build is missing. Run `npm run build` first.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await copyIfPresent(path.join(projectRoot, '.next', 'static'), path.join(standaloneNextRoot, 'static'))
|
||||
await copyIfPresent(path.join(projectRoot, 'public'), path.join(standaloneRoot, 'public'))
|
||||
await copyIfPresent(path.join(projectRoot, 'public'), path.join(appRoot, 'public'))
|
||||
|
||||
const child = spawn(process.execPath, [standaloneServer], {
|
||||
const child = spawn(process.execPath, [serverPath], {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { FormEvent, useCallback, useEffect, useState } from 'react'
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import PageIntro from '@/components/common/PageIntro'
|
||||
import SectionTabs, { TabPanel, type SectionTab } from '@/components/common/SectionTabs'
|
||||
import DisclosureSection from '@/components/common/DisclosureSection'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import {
|
||||
accessApi,
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
type AccessUser,
|
||||
type WalletAccessSession,
|
||||
} from '@/services/api/access'
|
||||
import { formatDate, formatInteger, formatTimestamp } from '@/utils/format'
|
||||
|
||||
const ACCESS_SCOPE_OPTIONS = ['rpc:read', 'rpc:write', 'rpc:admin'] as const
|
||||
const OPERATOR_IDENTITIES = [
|
||||
@@ -88,6 +91,18 @@ export default function AccessManagementPage() {
|
||||
const [createdKey, setCreatedKey] = useState('')
|
||||
const [message, setMessage] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [accessTab, setAccessTab] = useState<'products' | 'account' | 'admin'>('products')
|
||||
|
||||
const accessTabs = useMemo((): SectionTab<'products' | 'account' | 'admin'>[] => {
|
||||
const tabs: SectionTab<'products' | 'account' | 'admin'>[] = [
|
||||
{ id: 'products', label: 'Products', count: products.length },
|
||||
{ id: 'account', label: 'Account & keys' },
|
||||
]
|
||||
if (user?.is_admin) {
|
||||
tabs.push({ id: 'admin', label: 'Admin' })
|
||||
}
|
||||
return tabs
|
||||
}, [products.length, user?.is_admin])
|
||||
|
||||
const clearSessionState = useCallback(() => {
|
||||
setUser(null)
|
||||
@@ -349,15 +364,15 @@ export default function AccessManagementPage() {
|
||||
OPERATOR_IDENTITIES.find((entry) => entry.slug === productSlug)
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-6 sm:py-8">
|
||||
<main className="container mx-auto px-4 py-4 sm:py-6">
|
||||
<PageIntro
|
||||
compact
|
||||
eyebrow="Access Control"
|
||||
title="Wallet Login, RPC Access & API Tokens"
|
||||
description="Connect a wallet for standard account sign-in, manage authenticated access, issue API keys, and prepare subscription-gated RPC products for DBIS, ThirdWeb, and Alltra."
|
||||
description="Wallet sign-in, API keys, and subscription-gated RPC products."
|
||||
actions={[
|
||||
{ href: '/wallet', label: 'Wallet tools' },
|
||||
{ href: '/system', label: 'System status' },
|
||||
{ href: '/search', label: 'Search explorer' },
|
||||
{ href: '/wallet', label: 'Wallet' },
|
||||
{ href: '/system', label: 'System' },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -372,7 +387,17 @@ export default function AccessManagementPage() {
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="mb-8 grid gap-6 lg:grid-cols-3">
|
||||
<SectionTabs
|
||||
idPrefix="access"
|
||||
ariaLabel="Access console sections"
|
||||
tabs={accessTabs}
|
||||
activeTab={accessTab}
|
||||
onChange={setAccessTab}
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<TabPanel idPrefix="access" tabId="products" activeTab={accessTab}>
|
||||
<div className="mb-4 grid gap-4 lg:grid-cols-3">
|
||||
{products.map((product) => (
|
||||
<Card key={product.slug} title={product.name}>
|
||||
<div className="space-y-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
@@ -446,10 +471,13 @@ export default function AccessManagementPage() {
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_1.2fr]">
|
||||
<div className="space-y-6">
|
||||
<Card title="Wallet Authentication">
|
||||
<TabPanel idPrefix="access" tabId="account" activeTab={accessTab}>
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_1.2fr]">
|
||||
<div className="space-y-4">
|
||||
<DisclosureSection title="Wallet authentication" defaultOpen={Boolean(walletSession)} alwaysCollapsible>
|
||||
<Card>
|
||||
<div className="space-y-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<p>
|
||||
Use a connected wallet for standard account sign-in, then access subscriptions, API keys, and managed RPC controls with the same authenticated session.
|
||||
@@ -467,7 +495,7 @@ export default function AccessManagementPage() {
|
||||
Your wallet address remains private within the access console. This session is treated as account sign-in, not a public identifier.
|
||||
</p>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Session expires {new Date(walletSession.expiresAt).toLocaleString()}
|
||||
Session expires {formatTimestamp(walletSession.expiresAt)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
@@ -498,21 +526,24 @@ export default function AccessManagementPage() {
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</DisclosureSection>
|
||||
|
||||
<Card title="Operator Identities">
|
||||
<div className="space-y-4">
|
||||
<DisclosureSection title="Operator identities" defaultOpen={false} alwaysCollapsible>
|
||||
<Card>
|
||||
<div className="space-y-3">
|
||||
{OPERATOR_IDENTITIES.map((identity) => (
|
||||
<div key={identity.slug} className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div key={identity.slug} className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<EntityBadge label={identity.label} tone="info" />
|
||||
<EntityBadge label={identity.slug} />
|
||||
<EntityBadge label={`vmid ${identity.vmid}`} tone="warning" />
|
||||
</div>
|
||||
<code className="mt-3 block break-all text-xs text-gray-700 dark:text-gray-300">{identity.address}</code>
|
||||
<code className="mt-2 block break-all text-xs text-gray-700 dark:text-gray-300">{identity.address}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</DisclosureSection>
|
||||
|
||||
<Card title={user ? `Signed in as ${user.username}` : 'Create or Access Account'}>
|
||||
{user ? (
|
||||
@@ -607,138 +638,6 @@ export default function AccessManagementPage() {
|
||||
) : null}
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{user?.is_admin ? (
|
||||
<Card title="Pending Access Review">
|
||||
<div className="mb-4 flex flex-wrap items-end gap-3">
|
||||
<label className="block min-w-[12rem]">
|
||||
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Subscription status</span>
|
||||
<select
|
||||
value={adminSubscriptionStatus}
|
||||
onChange={(event) => setAdminSubscriptionStatus(event.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="revoked">Revoked</option>
|
||||
<option value="">All statuses</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{adminSubscriptions.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{adminSubscriptions.map((subscription) => (
|
||||
<div key={subscription.id} className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-white">{subscription.productSlug}</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<EntityBadge label={subscription.status} tone={subscription.status === 'active' ? 'success' : 'warning'} />
|
||||
<EntityBadge label={subscription.tier} />
|
||||
<EntityBadge label={`${subscription.monthlyQuota.toLocaleString()} quota`} tone="info" />
|
||||
{subscription.requiresApproval ? <EntityBadge label="restricted product" tone="warning" /> : null}
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
Requested {new Date(subscription.createdAt).toLocaleString()}
|
||||
{subscription.notes ? ` · ${subscription.notes}` : ''}
|
||||
</div>
|
||||
<label className="mt-3 block">
|
||||
<span className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-300">Admin note</span>
|
||||
<input
|
||||
type="text"
|
||||
value={adminActionNotes[subscription.id] || ''}
|
||||
onChange={(event) =>
|
||||
setAdminActionNotes((current) => ({
|
||||
...current,
|
||||
[subscription.id]: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Reason, approval scope, or operator note"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleAdminSubscriptionAction(subscription.id, 'active')}
|
||||
className="rounded-lg bg-emerald-600 px-3 py-2 text-sm font-medium text-white hover:bg-emerald-700"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleAdminSubscriptionAction(subscription.id, 'suspended')}
|
||||
className="rounded-lg border border-amber-300 px-3 py-2 text-sm font-medium text-amber-800 hover:bg-amber-50 dark:border-amber-800 dark:text-amber-300 dark:hover:bg-amber-950/20"
|
||||
>
|
||||
Suspend
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleAdminSubscriptionAction(subscription.id, 'revoked')}
|
||||
className="rounded-lg border border-red-300 px-3 py-2 text-sm font-medium text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-300 dark:hover:bg-red-950/20"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">No subscriptions match the current review filter.</p>
|
||||
)}
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{user?.is_admin ? (
|
||||
<Card title="Platform Audit Feed">
|
||||
<div className="mb-4 grid gap-3 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Filter by product</span>
|
||||
<select value={adminAuditProduct} onChange={(event) => void handleAdminAuditProductChange(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
|
||||
<option value="">All products</option>
|
||||
{products.map((product) => (
|
||||
<option key={`audit-${product.slug}`} value={product.slug}>{product.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Entries shown</span>
|
||||
<select
|
||||
value={adminAuditLimit}
|
||||
onChange={(event) => setAdminAuditLimit(event.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{adminAuditEntries.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{adminAuditEntries.map((entry) => (
|
||||
<div key={`admin-audit-${entry.id}`} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EntityBadge label={entry.productSlug || 'unscoped'} tone="info" />
|
||||
<EntityBadge label={entry.methodName || 'unknown method'} />
|
||||
<EntityBadge label={`${entry.requestCount} req`} />
|
||||
</div>
|
||||
<div className="mt-2 font-medium text-gray-900 dark:text-white">{entry.keyName || entry.apiKeyId}</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{new Date(entry.createdAt).toLocaleString()}
|
||||
{entry.lastIp ? ` · ${entry.lastIp}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">No recent validated RPC traffic matches the current filter.</p>
|
||||
)}
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Card title="Issued API Keys">
|
||||
@@ -758,11 +657,11 @@ export default function AccessManagementPage() {
|
||||
<EntityBadge label={`${key.requestsUsed}/${key.monthlyQuota || 0}`} />
|
||||
{key.approved ? <EntityBadge label="approved" tone="success" /> : <EntityBadge label="pending" tone="warning" />}
|
||||
{key.revoked ? <EntityBadge label="revoked" tone="warning" /> : <EntityBadge label="active" tone="success" />}
|
||||
{key.expiresAt ? <EntityBadge label={`expires ${new Date(key.expiresAt).toLocaleDateString()}`} /> : <EntityBadge label="no expiry" />}
|
||||
{key.expiresAt ? <EntityBadge label={`expires ${formatDate(key.expiresAt)}`} /> : <EntityBadge label="no expiry" />}
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
Created {new Date(key.createdAt).toLocaleString()}
|
||||
{key.lastUsedAt ? ` · Last used ${new Date(key.lastUsedAt).toLocaleString()}` : ' · Not used yet'}
|
||||
Created {formatTimestamp(key.createdAt)}
|
||||
{key.lastUsedAt ? ` · Last used ${formatTimestamp(key.lastUsedAt)}` : ' · Not used yet'}
|
||||
</div>
|
||||
</div>
|
||||
{!key.revoked ? (
|
||||
@@ -816,7 +715,7 @@ export default function AccessManagementPage() {
|
||||
<EntityBadge label={`${item.active_keys} active keys`} />
|
||||
</div>
|
||||
<div className="mt-2 text-gray-600 dark:text-gray-400">
|
||||
{item.requests_used.toLocaleString()} requests used / {item.monthly_quota.toLocaleString()} monthly quota
|
||||
{formatInteger(item.requests_used)} requests used / {formatInteger(item.monthly_quota)} monthly quota
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -851,7 +750,7 @@ export default function AccessManagementPage() {
|
||||
</div>
|
||||
<div className="mt-2 font-medium text-gray-900 dark:text-white">{entry.keyName || entry.apiKeyId}</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{new Date(entry.createdAt).toLocaleString()}
|
||||
{formatTimestamp(entry.createdAt)}
|
||||
{entry.lastIp ? ` · ${entry.lastIp}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -868,6 +767,114 @@ export default function AccessManagementPage() {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
{user?.is_admin ? (
|
||||
<TabPanel idPrefix="access" tabId="admin" activeTab={accessTab}>
|
||||
<div className="space-y-4">
|
||||
<Card title="Pending access review">
|
||||
<div className="mb-4 flex flex-wrap items-end gap-3">
|
||||
<label className="block min-w-[12rem]">
|
||||
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Subscription status</span>
|
||||
<select
|
||||
value={adminSubscriptionStatus}
|
||||
onChange={(event) => setAdminSubscriptionStatus(event.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="revoked">Revoked</option>
|
||||
<option value="">All statuses</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{adminSubscriptions.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{adminSubscriptions.map((subscription) => (
|
||||
<div key={subscription.id} className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-white">{subscription.productSlug}</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<EntityBadge label={subscription.status} tone={subscription.status === 'active' ? 'success' : 'warning'} />
|
||||
<EntityBadge label={subscription.tier} />
|
||||
<EntityBadge label={`${formatInteger(subscription.monthlyQuota)} quota`} tone="info" />
|
||||
{subscription.requiresApproval ? <EntityBadge label="restricted product" tone="warning" /> : null}
|
||||
</div>
|
||||
<label className="mt-3 block">
|
||||
<span className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-300">Admin note</span>
|
||||
<input
|
||||
type="text"
|
||||
value={adminActionNotes[subscription.id] || ''}
|
||||
onChange={(event) =>
|
||||
setAdminActionNotes((current) => ({
|
||||
...current,
|
||||
[subscription.id]: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Reason, approval scope, or operator note"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button type="button" onClick={() => void handleAdminSubscriptionAction(subscription.id, 'active')} className="rounded-lg bg-emerald-600 px-3 py-2 text-sm font-medium text-white hover:bg-emerald-700">Approve</button>
|
||||
<button type="button" onClick={() => void handleAdminSubscriptionAction(subscription.id, 'suspended')} className="rounded-lg border border-amber-300 px-3 py-2 text-sm font-medium text-amber-800 hover:bg-amber-50 dark:border-amber-800 dark:text-amber-300">Suspend</button>
|
||||
<button type="button" onClick={() => void handleAdminSubscriptionAction(subscription.id, 'revoked')} className="rounded-lg border border-red-300 px-3 py-2 text-sm font-medium text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-300">Revoke</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">No subscriptions match the current review filter.</p>
|
||||
)}
|
||||
</Card>
|
||||
<Card title="Platform audit feed">
|
||||
<div className="mb-4 grid gap-3 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Filter by product</span>
|
||||
<select value={adminAuditProduct} onChange={(event) => void handleAdminAuditProductChange(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
|
||||
<option value="">All products</option>
|
||||
{products.map((product) => (
|
||||
<option key={`audit-${product.slug}`} value={product.slug}>{product.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Entries shown</span>
|
||||
<select value={adminAuditLimit} onChange={(event) => setAdminAuditLimit(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{adminAuditEntries.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{adminAuditEntries.map((entry) => (
|
||||
<div key={`admin-audit-${entry.id}`} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EntityBadge label={entry.productSlug || 'unscoped'} tone="info" />
|
||||
<EntityBadge label={entry.methodName || 'unknown method'} />
|
||||
<EntityBadge label={`${entry.requestCount} req`} />
|
||||
</div>
|
||||
<div className="mt-2 font-medium text-gray-900 dark:text-white">{entry.keyName || entry.apiKeyId}</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatTimestamp(entry.createdAt)}
|
||||
{entry.lastIp ? ` · ${entry.lastIp}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">No recent validated RPC traffic matches the current filter.</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</TabPanel>
|
||||
) : null}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import type { CheckpointMainnetAttestationLink } from '@/services/api/tokenAggregation'
|
||||
|
||||
function shortHash(hash: string): string {
|
||||
if (hash.length < 20) return hash
|
||||
return `${hash.slice(0, 10)}…${hash.slice(-8)}`
|
||||
}
|
||||
|
||||
export interface MainnetAttestationPanelProps {
|
||||
batchId: string
|
||||
batchTotalUsd?: string
|
||||
links: CheckpointMainnetAttestationLink[]
|
||||
}
|
||||
|
||||
export default function MainnetAttestationPanel({
|
||||
batchId,
|
||||
batchTotalUsd,
|
||||
links,
|
||||
}: MainnetAttestationPanelProps) {
|
||||
return (
|
||||
<Card title="Ethereum mainnet attestation">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
This Chain 138 transaction is included in checkpoint batch #{batchId}
|
||||
{batchTotalUsd ? ` · USD ref ${batchTotalUsd}` : ''}.
|
||||
</p>
|
||||
|
||||
{links.length === 0 ? (
|
||||
<p className="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
Attested on mainnet; Etherscan transaction links are indexing. Refresh in a minute or open the
|
||||
checkpoint contracts on Etherscan.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mt-4 space-y-3">
|
||||
{links.map((link) => (
|
||||
<li
|
||||
key={`${link.layer}:${link.mainnetTxHash}`}
|
||||
className="rounded-2xl border border-primary-200/60 bg-primary-50/40 p-4 dark:border-primary-500/20 dark:bg-primary-950/20"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{link.label || link.layer}
|
||||
</span>
|
||||
<Link
|
||||
href={link.mainnetExplorerUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-mono text-sm text-primary-600 hover:underline dark:text-primary-400"
|
||||
>
|
||||
{shortHash(link.mainnetTxHash)} ↗
|
||||
</Link>
|
||||
</div>
|
||||
{(link.meta?.role || link.meta?.uetr) && (
|
||||
<div className="mt-2 flex flex-wrap gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{link.meta.role ? <span>role: {link.meta.role}</span> : null}
|
||||
{link.meta.uetr ? <span>UETR: {link.meta.uetr}</span> : null}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
|
||||
import type { ChainActivityContext } from '@/utils/activityContext'
|
||||
import { formatRelativeAge, formatTimestamp } from '@/utils/format'
|
||||
import { formatInteger, formatTimestamp } from '@/utils/format'
|
||||
import { Explain, useUiMode } from './UiModeContext'
|
||||
|
||||
function resolveTone(state: ChainActivityContext['state']): 'success' | 'warning' | 'neutral' {
|
||||
@@ -58,9 +59,14 @@ export default function ActivityContextPanel({
|
||||
const { mode } = useUiMode()
|
||||
const tone = resolveTone(context.state)
|
||||
const dualTimelineLabel =
|
||||
context.latest_block_timestamp && context.latest_transaction_timestamp
|
||||
? `${formatRelativeAge(context.latest_block_timestamp)} head · ${formatRelativeAge(context.latest_transaction_timestamp)} latest tx`
|
||||
: 'Dual timeline unavailable'
|
||||
context.latest_block_timestamp && context.latest_transaction_timestamp ? (
|
||||
<>
|
||||
<ClientRelativeTime value={context.latest_block_timestamp} suffix=" head" /> ·{' '}
|
||||
<ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" latest tx" />
|
||||
</>
|
||||
) : (
|
||||
'Dual timeline unavailable'
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="border border-sky-200 bg-sky-50/60 dark:border-sky-900/40 dark:bg-sky-950/20" title={title}>
|
||||
@@ -75,6 +81,9 @@ export default function ActivityContextPanel({
|
||||
</Explain>
|
||||
</div>
|
||||
<EntityBadge label={resolveLabel(context.state)} tone={tone} />
|
||||
{context.head_is_idle && context.state === 'low' ? (
|
||||
<EntityBadge label="quiet chain" tone="info" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{compact ? (
|
||||
@@ -83,11 +92,11 @@ export default function ActivityContextPanel({
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
{context.latest_block_number != null ? `#${context.latest_block_number}` : 'Unknown'}
|
||||
</span>{' '}
|
||||
was {formatRelativeAge(context.latest_block_timestamp)}. Latest visible transaction was{' '}
|
||||
was <ClientRelativeTime value={context.latest_block_timestamp} suffix=" ago" />. Latest visible transaction was{' '}
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
{context.latest_transaction_block_number != null ? `#${context.latest_transaction_block_number}` : 'Unknown'}
|
||||
</span>{' '}
|
||||
{formatRelativeAge(context.latest_transaction_timestamp)}.{' '}
|
||||
<ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />.{' '}
|
||||
{mode === 'guided'
|
||||
? 'Use the block gap and last non-empty block above to tell low activity apart from an indexing issue.'
|
||||
: dualTimelineLabel}
|
||||
@@ -101,11 +110,11 @@ export default function ActivityContextPanel({
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
{context.latest_block_number != null ? `#${context.latest_block_number}` : 'Unknown'}
|
||||
</span>{' '}
|
||||
was {formatRelativeAge(context.latest_block_timestamp)}. Latest visible transaction was{' '}
|
||||
was <ClientRelativeTime value={context.latest_block_timestamp} suffix=" ago" />. Latest visible transaction was{' '}
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
{context.latest_transaction_block_number != null ? `#${context.latest_transaction_block_number}` : 'Unknown'}
|
||||
</span>{' '}
|
||||
{formatRelativeAge(context.latest_transaction_timestamp)}.
|
||||
<ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />.
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
|
||||
@@ -130,6 +139,11 @@ export default function ActivityContextPanel({
|
||||
Open last non-empty block →
|
||||
</Link>
|
||||
) : null}
|
||||
{context.block_gap_to_latest_transaction != null ? (
|
||||
<span>
|
||||
Block gap to latest visible transaction: {formatInteger(context.block_gap_to_latest_transaction)}
|
||||
</span>
|
||||
) : null}
|
||||
{context.latest_transaction_timestamp ? (
|
||||
<span>Latest visible transaction time: {formatTimestamp(context.latest_transaction_timestamp)}</span>
|
||||
) : null}
|
||||
|
||||
@@ -8,15 +8,15 @@ export default function BrandLockup({ compact = false }: { compact?: boolean })
|
||||
<span
|
||||
className={[
|
||||
'block truncate font-semibold tracking-[-0.02em] text-gray-950 dark:text-white',
|
||||
compact ? 'text-[1.45rem]' : 'text-[1.65rem]',
|
||||
compact ? 'text-[1.2rem]' : 'text-[1.35rem]',
|
||||
].join(' ')}
|
||||
>
|
||||
SolaceScan
|
||||
DBIS Explorer
|
||||
</span>
|
||||
<span
|
||||
className={[
|
||||
'block truncate font-medium uppercase text-gray-500 dark:text-gray-400',
|
||||
compact ? 'text-[0.72rem] tracking-[0.14em]' : 'text-[0.8rem] tracking-[0.12em]',
|
||||
'block truncate font-medium uppercase text-gray-500 dark:text-gray-400 max-sm:hidden',
|
||||
compact ? 'text-[0.64rem] tracking-[0.13em]' : 'text-[0.68rem] tracking-[0.12em]',
|
||||
].join(' ')}
|
||||
>
|
||||
Chain 138 Explorer by DBIS
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export default function BrandMark({ size = 'default' }: { size?: 'default' | 'compact' }) {
|
||||
const containerClassName =
|
||||
size === 'compact'
|
||||
? 'h-10 w-10 rounded-xl'
|
||||
: 'h-11 w-11 rounded-2xl'
|
||||
const iconClassName = size === 'compact' ? 'h-6 w-6' : 'h-7 w-7'
|
||||
? 'h-9 w-9 rounded-lg'
|
||||
: 'h-10 w-10 rounded-lg'
|
||||
const iconClassName = size === 'compact' ? 'h-5 w-5' : 'h-6 w-6'
|
||||
|
||||
return (
|
||||
<span
|
||||
|
||||
57
frontend/src/components/common/ClientRelativeTime.tsx
Normal file
57
frontend/src/components/common/ClientRelativeTime.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { formatRelativeAgeAt } from '@/utils/format'
|
||||
|
||||
const PLACEHOLDER = '…'
|
||||
|
||||
export default function ClientRelativeTime({
|
||||
value,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
fallback = 'Unknown',
|
||||
className,
|
||||
}: {
|
||||
value?: string | null
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
fallback?: string
|
||||
className?: string
|
||||
}) {
|
||||
const [label, setLabel] = useState(PLACEHOLDER)
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setLabel(fallback)
|
||||
return
|
||||
}
|
||||
const parsed = Date.parse(value)
|
||||
if (!Number.isFinite(parsed)) {
|
||||
setLabel(fallback)
|
||||
return
|
||||
}
|
||||
const update = () => setLabel(formatRelativeAgeAt(Date.now(), value))
|
||||
update()
|
||||
const id = window.setInterval(update, 30_000)
|
||||
return () => window.clearInterval(id)
|
||||
}, [value, fallback])
|
||||
|
||||
const content = label === PLACEHOLDER ? PLACEHOLDER : label
|
||||
if (className) {
|
||||
return (
|
||||
<span className={className} suppressHydrationWarning>
|
||||
{prefix}
|
||||
{content}
|
||||
{suffix}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span suppressHydrationWarning>
|
||||
{prefix}
|
||||
{content}
|
||||
{suffix}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
29
frontend/src/components/common/CompactMetricBar.tsx
Normal file
29
frontend/src/components/common/CompactMetricBar.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
export type CompactMetric = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export default function CompactMetricBar({
|
||||
metrics,
|
||||
className = 'mb-4',
|
||||
}: {
|
||||
metrics: CompactMetric[]
|
||||
className?: string
|
||||
}) {
|
||||
if (metrics.length === 0) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-wrap items-center gap-x-4 gap-y-1 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-300 ${className}`}
|
||||
>
|
||||
{metrics.map((metric) => (
|
||||
<span key={metric.label}>
|
||||
<span className="text-gray-500 dark:text-gray-400">{metric.label}:</span>{' '}
|
||||
<span className="font-medium text-gray-900 dark:text-white">{metric.value}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
70
frontend/src/components/common/DisclosureSection.tsx
Normal file
70
frontend/src/components/common/DisclosureSection.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import { useId, useState, type ReactNode } from 'react'
|
||||
|
||||
interface DisclosureSectionProps {
|
||||
title: string
|
||||
children: ReactNode
|
||||
defaultOpen?: boolean
|
||||
className?: string
|
||||
headingClassName?: string
|
||||
/** When true, content is always visible and the toggle is hidden (desktop layout). */
|
||||
forceOpen?: boolean
|
||||
/** When true, keep accordion behavior on all breakpoints (no md:auto-expand). */
|
||||
alwaysCollapsible?: boolean
|
||||
}
|
||||
|
||||
export default function DisclosureSection({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
className = '',
|
||||
headingClassName = '',
|
||||
forceOpen = false,
|
||||
alwaysCollapsible = false,
|
||||
}: DisclosureSectionProps) {
|
||||
const [open, setOpen] = useState(defaultOpen || forceOpen)
|
||||
const panelId = useId().replace(/:/g, '')
|
||||
const isOpen = forceOpen || open
|
||||
|
||||
return (
|
||||
<section className={className}>
|
||||
{forceOpen ? (
|
||||
<div className={`mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 ${headingClassName}`}>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={panelId}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
className={`flex w-full items-center justify-between gap-3 rounded-lg border border-gray-200 bg-gray-50/80 px-3 py-2.5 text-left text-sm font-semibold uppercase tracking-wide text-gray-700 transition hover:border-primary-300 dark:border-gray-800 dark:bg-gray-900/40 dark:text-gray-200 dark:hover:border-primary-700 ${alwaysCollapsible ? '' : 'md:hidden'} ${headingClassName}`}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<span aria-hidden="true" className="text-base text-gray-500 dark:text-gray-400">
|
||||
{isOpen ? '−' : '+'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
id={panelId}
|
||||
className={
|
||||
forceOpen
|
||||
? ''
|
||||
: alwaysCollapsible
|
||||
? `${isOpen ? 'block' : 'hidden'} mt-3`
|
||||
: `${isOpen ? 'block' : 'hidden'} mt-3 md:mt-0 md:block`
|
||||
}
|
||||
>
|
||||
{!forceOpen && !alwaysCollapsible ? (
|
||||
<div className="mb-3 hidden text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 md:block">
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
49
frontend/src/components/common/DisplayCurrencyContext.tsx
Normal file
49
frontend/src/components/common/DisplayCurrencyContext.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
export type DisplayCurrency = 'native' | 'usd'
|
||||
|
||||
const DISPLAY_CURRENCY_STORAGE_KEY = 'explorer_display_currency'
|
||||
|
||||
const DisplayCurrencyContext = createContext<{
|
||||
currency: DisplayCurrency
|
||||
setCurrency: (currency: DisplayCurrency) => void
|
||||
} | null>(null)
|
||||
|
||||
export function DisplayCurrencyProvider({ children }: { children: ReactNode }) {
|
||||
const [currency, setCurrencyState] = useState<DisplayCurrency>('native')
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
const stored = window.localStorage.getItem(DISPLAY_CURRENCY_STORAGE_KEY)
|
||||
if (stored === 'native' || stored === 'usd') {
|
||||
setCurrencyState(stored)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setCurrency = (nextCurrency: DisplayCurrency) => {
|
||||
setCurrencyState(nextCurrency)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(DISPLAY_CURRENCY_STORAGE_KEY, nextCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
currency,
|
||||
setCurrency,
|
||||
}),
|
||||
[currency],
|
||||
)
|
||||
|
||||
return <DisplayCurrencyContext.Provider value={value}>{children}</DisplayCurrencyContext.Provider>
|
||||
}
|
||||
|
||||
export function useDisplayCurrency() {
|
||||
const context = useContext(DisplayCurrencyContext)
|
||||
if (!context) {
|
||||
throw new Error('useDisplayCurrency must be used within a DisplayCurrencyProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -13,26 +13,44 @@ function toneClasses(tone: 'neutral' | 'success' | 'warning' | 'info') {
|
||||
}
|
||||
}
|
||||
|
||||
export function getEntityBadgeTone(tag: string): 'neutral' | 'success' | 'warning' | 'info' {
|
||||
const normalized = tag.toLowerCase()
|
||||
if (normalized === 'compliant' || normalized === 'listed' || normalized === 'verified') {
|
||||
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'
|
||||
}
|
||||
if (normalized === 'wrapped') {
|
||||
if (normalized === 'wrapped' || normalized === 'treasury-bond') {
|
||||
return 'warning'
|
||||
}
|
||||
if (normalized === 'bridge' || normalized === 'canonical' || normalized === 'official') {
|
||||
if (normalized === 'bridge' || normalized === 'canonical' || normalized === 'official' || normalized === 'electronic-money' || normalized === 'commodity') {
|
||||
return 'info'
|
||||
}
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
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] || resolvedLabel
|
||||
}
|
||||
|
||||
export default function EntityBadge({
|
||||
label,
|
||||
tone,
|
||||
className,
|
||||
}: {
|
||||
label: string
|
||||
label: unknown
|
||||
tone?: 'neutral' | 'success' | 'warning' | 'info'
|
||||
className?: string
|
||||
}) {
|
||||
@@ -46,7 +64,7 @@ export default function EntityBadge({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{formatEntityBadgeLabel(label)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function ExplorerAgentTool() {
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
'Explorer AI Agent Tool is ready. I can explain this page, summarize what you are looking at, and help investigate transactions, contracts, routes, and system surfaces.',
|
||||
'DBIS Explorer AI Assist is ready. I can explain this page, summarize what you are looking at, and help investigate transactions, contracts, routes, and system surfaces.',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function ExplorerAgentTool() {
|
||||
<section className="w-[min(24rem,calc(100vw-1.5rem))] overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">Explorer AI Agent Tool</h2>
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">DBIS Explorer AI Assist</h2>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Page-aware guidance for the explorer. Helpful, read-only, and designed for quick investigation.
|
||||
</p>
|
||||
@@ -163,15 +163,16 @@ export default function ExplorerAgentTool() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary-600 px-4 py-3 text-sm font-semibold text-white shadow-lg transition hover:bg-primary-700"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary-600 p-3 text-sm font-semibold text-white shadow-lg transition hover:bg-primary-700 lg:px-4 lg:py-3"
|
||||
aria-expanded={open}
|
||||
aria-label="Open DBIS Explorer AI Assist"
|
||||
>
|
||||
<span className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-white/15">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-4l-4 4v-4Z" />
|
||||
</svg>
|
||||
</span>
|
||||
Agent Tool
|
||||
<span className="hidden lg:inline">AI Assist</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,25 +2,33 @@ import type { ReactNode } from 'react'
|
||||
import Navbar from './Navbar'
|
||||
import Footer from './Footer'
|
||||
import ExplorerAgentTool from './ExplorerAgentTool'
|
||||
import ExplorerDocumentHead from './ExplorerDocumentHead'
|
||||
import { DisplayCurrencyProvider } from './DisplayCurrencyContext'
|
||||
import { UiModeProvider } from './UiModeContext'
|
||||
import { PostureGlossaryProvider } from './PostureGlossaryProvider'
|
||||
|
||||
export default function ExplorerChrome({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<UiModeProvider>
|
||||
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[100] focus:rounded-md focus:bg-primary-600 focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
<Navbar />
|
||||
<div id="main-content" className="flex-1">
|
||||
{children}
|
||||
</div>
|
||||
<ExplorerAgentTool />
|
||||
<Footer />
|
||||
</div>
|
||||
<DisplayCurrencyProvider>
|
||||
<PostureGlossaryProvider>
|
||||
<ExplorerDocumentHead />
|
||||
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[100] focus:rounded-md focus:bg-primary-600 focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
<Navbar />
|
||||
<div id="main-content" className="flex-1">
|
||||
{children}
|
||||
</div>
|
||||
<ExplorerAgentTool />
|
||||
<Footer />
|
||||
</div>
|
||||
</PostureGlossaryProvider>
|
||||
</DisplayCurrencyProvider>
|
||||
</UiModeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
56
frontend/src/components/common/ExplorerDocumentHead.tsx
Normal file
56
frontend/src/components/common/ExplorerDocumentHead.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const BASE_TITLE = 'DBIS Explorer'
|
||||
|
||||
function resolveDocumentTitle(pathname: string, query: Record<string, string | string[] | undefined>): string {
|
||||
if (pathname === '/') return `${BASE_TITLE} — Chain 138`
|
||||
if (pathname === '/search') return `Search — ${BASE_TITLE}`
|
||||
if (pathname === '/wallet') return `Wallet Tools — ${BASE_TITLE}`
|
||||
if (pathname === '/protocols') return `Official Protocol Contracts — ${BASE_TITLE}`
|
||||
if (pathname.startsWith('/protocols/')) {
|
||||
const id = typeof query.id === 'string' ? query.id : pathname.split('/').pop() || 'Protocol'
|
||||
return `${id} — Protocols — ${BASE_TITLE}`
|
||||
}
|
||||
if (pathname === '/pools') return `Pool Registry — ${BASE_TITLE}`
|
||||
if (pathname.startsWith('/pools/')) return `Pool Detail — ${BASE_TITLE}`
|
||||
if (pathname === '/liquidity') return `Liquidity — ${BASE_TITLE}`
|
||||
if (pathname === '/operations') return `Operations — ${BASE_TITLE}`
|
||||
if (pathname === '/docs') return `Documentation — ${BASE_TITLE}`
|
||||
if (pathname === '/tokens') return `Tokens — ${BASE_TITLE}`
|
||||
if (pathname.startsWith('/tokens/')) return `Token — ${BASE_TITLE}`
|
||||
if (pathname === '/addresses') return `Addresses — ${BASE_TITLE}`
|
||||
if (pathname.startsWith('/addresses/')) return `Address — ${BASE_TITLE}`
|
||||
if (pathname === '/blocks') return `Blocks — ${BASE_TITLE}`
|
||||
if (pathname === '/transactions') return `Transactions — ${BASE_TITLE}`
|
||||
if (pathname.startsWith('/transactions/')) return `Transaction — ${BASE_TITLE}`
|
||||
if (pathname === '/bridge') return `Bridge — ${BASE_TITLE}`
|
||||
if (pathname === '/routes') return `Routes — ${BASE_TITLE}`
|
||||
if (pathname === '/analytics') return `Analytics — ${BASE_TITLE}`
|
||||
if (pathname === '/operator') return `Operator — ${BASE_TITLE}`
|
||||
if (pathname === '/system') return `System — ${BASE_TITLE}`
|
||||
if (pathname === '/weth') return `WETH — ${BASE_TITLE}`
|
||||
if (pathname === '/watchlist') return `Watchlist — ${BASE_TITLE}`
|
||||
if (pathname === '/access') return `Account Access — ${BASE_TITLE}`
|
||||
|
||||
const segment = pathname.split('/').filter(Boolean)[0]
|
||||
if (segment) {
|
||||
const label = segment.charAt(0).toUpperCase() + segment.slice(1)
|
||||
return `${label} — ${BASE_TITLE}`
|
||||
}
|
||||
return BASE_TITLE
|
||||
}
|
||||
|
||||
export default function ExplorerDocumentHead() {
|
||||
const router = useRouter()
|
||||
const pathname = router.pathname || '/'
|
||||
const title = resolveDocumentTitle(pathname, router.query)
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import type { MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
||||
import type { ExplorerStats } from '@/services/api/stats'
|
||||
import type { ChainActivityContext } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import DisclosureSection from '@/components/common/DisclosureSection'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
|
||||
export default function ExplorerFreshnessDisclosure({
|
||||
context,
|
||||
stats,
|
||||
bridgeStatus,
|
||||
scopeLabel,
|
||||
activityTitle = 'Recency',
|
||||
title = 'Index freshness',
|
||||
className = 'mb-4',
|
||||
children,
|
||||
}: {
|
||||
context: ChainActivityContext
|
||||
stats?: ExplorerStats | null
|
||||
bridgeStatus?: MissionControlBridgeStatusResponse | null
|
||||
scopeLabel: string
|
||||
activityTitle?: string
|
||||
title?: string
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<DisclosureSection title={title} defaultOpen={false} alwaysCollapsible className={className}>
|
||||
<ActivityContextPanel compact context={context} title={activityTitle} />
|
||||
<FreshnessTrustNote
|
||||
className="mt-2"
|
||||
context={context}
|
||||
stats={stats}
|
||||
bridgeStatus={bridgeStatus}
|
||||
scopeLabel={scopeLabel}
|
||||
/>
|
||||
{children}
|
||||
</DisclosureSection>
|
||||
)
|
||||
}
|
||||
34
frontend/src/components/common/ExplorerRetryAlert.tsx
Normal file
34
frontend/src/components/common/ExplorerRetryAlert.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
interface ExplorerRetryAlertProps {
|
||||
message: string
|
||||
onRetry?: () => void
|
||||
retryLabel?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function ExplorerRetryAlert({
|
||||
message,
|
||||
onRetry,
|
||||
retryLabel = 'Retry',
|
||||
className = '',
|
||||
}: ExplorerRetryAlertProps) {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={[
|
||||
'flex flex-col gap-3 rounded-xl border border-red-200 bg-red-50/70 px-4 py-3 dark:border-red-900/50 dark:bg-red-950/20 sm:flex-row sm:items-center sm:justify-between',
|
||||
className,
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{message}</p>
|
||||
{onRetry ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRetry}
|
||||
className="shrink-0 rounded-lg border border-red-300 bg-white px-3 py-1.5 text-sm font-semibold text-red-800 transition-colors hover:bg-red-50 dark:border-red-800 dark:bg-red-950 dark:text-red-100 dark:hover:bg-red-900/40"
|
||||
>
|
||||
{retryLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import DisclosureSection from '@/components/common/DisclosureSection'
|
||||
import FooterLinkGroups from '@/components/common/FooterLinkGroups'
|
||||
import FooterPublicApiLinks from '@/components/common/FooterPublicApiLinks'
|
||||
|
||||
const footerLinkClass =
|
||||
'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors'
|
||||
@@ -7,74 +10,53 @@ export default function Footer() {
|
||||
const year = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
<footer className="mt-auto border-t border-gray-200 dark:border-gray-700 bg-white/90 dark:bg-gray-900/90 backdrop-blur">
|
||||
<div className="container mx-auto px-4 py-6 sm:py-8">
|
||||
<div className="grid gap-4 sm:gap-6 md:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<div className="space-y-3 rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white sm:text-lg">
|
||||
SolaceScan
|
||||
</div>
|
||||
<p className="max-w-xl text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
Built on Blockscout for the DBIS / Defi Oracle Chain 138 explorer surface.
|
||||
Explorer data is powered by Blockscout, Chain 138 RPC, and the companion MetaMask Snap.
|
||||
</p>
|
||||
<p className="max-w-xl text-xs leading-5 text-gray-500 dark:text-gray-500">
|
||||
Public explorer access may appear under <code>blockscout.defi-oracle.io</code> or <code>explorer.d-bis.org</code>.
|
||||
Both domains belong to the same DBIS / Defi Oracle explorer surface.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500">
|
||||
© {year} DBIS / Defi Oracle. All rights reserved.
|
||||
<footer className="mt-auto border-t border-gray-200 bg-white/90 backdrop-blur dark:border-gray-700 dark:bg-gray-900/90">
|
||||
<div className="container mx-auto px-4 py-4 sm:py-5">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="min-w-0 shrink-0 lg:max-w-xs">
|
||||
<div className="text-sm font-semibold text-gray-900 dark:text-white">DBIS Explorer</div>
|
||||
<p className="mt-1 text-xs leading-5 text-gray-600 dark:text-gray-400">
|
||||
Chain 138 explorer on Blockscout. Primary domain: <code>explorer.d-bis.org</code>.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-500">© {year} DBIS</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
|
||||
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Resources
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><Link className={footerLinkClass} href="/search">Search</Link></li>
|
||||
<li><Link className={footerLinkClass} href="/docs">Documentation</Link></li>
|
||||
<li><Link className={footerLinkClass} href="/bridge">Bridge Monitoring</Link></li>
|
||||
<li><Link className={footerLinkClass} href="/liquidity">Liquidity Access</Link></li>
|
||||
<li><Link className={footerLinkClass} href="/routes">Routes</Link></li>
|
||||
<li><Link className={footerLinkClass} href="/operations">Operations Hub</Link></li>
|
||||
<li><Link className={footerLinkClass} href="/addresses">Addresses</Link></li>
|
||||
<li><Link className={footerLinkClass} href="/watchlist">Watchlist</Link></li>
|
||||
<li><a className={footerLinkClass} href="/privacy.html">Privacy Policy</a></li>
|
||||
<li><a className={footerLinkClass} href="/terms.html">Terms of Service</a></li>
|
||||
<li><a className={footerLinkClass} href="/acknowledgments.html">Acknowledgments</a></li>
|
||||
</ul>
|
||||
<div className="hidden min-w-0 flex-1 lg:block">
|
||||
<FooterLinkGroups />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
|
||||
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Contact
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<DisclosureSection
|
||||
title="Site navigation"
|
||||
alwaysCollapsible
|
||||
className="mt-3 lg:hidden"
|
||||
>
|
||||
<FooterLinkGroups />
|
||||
</DisclosureSection>
|
||||
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
<DisclosureSection title="Public APIs" alwaysCollapsible className="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
|
||||
<FooterPublicApiLinks compact />
|
||||
</DisclosureSection>
|
||||
|
||||
<DisclosureSection title="Contact" alwaysCollapsible className="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
|
||||
<div className="space-y-1.5 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>
|
||||
Support:{' '}
|
||||
<a className={footerLinkClass} href="mailto:support@d-bis.org">
|
||||
support@d-bis.org
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Snap site:{' '}
|
||||
<Link className={footerLinkClass} href="/topology">
|
||||
Chain 138 map
|
||||
</Link>
|
||||
{' · '}
|
||||
<a className={footerLinkClass} href="/snap/" target="_blank" rel="noopener noreferrer">
|
||||
/snap/ on the current explorer domain
|
||||
Snap site
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Command center:{' '}
|
||||
<a className={footerLinkClass} href="/chain138-command-center.html" target="_blank" rel="noopener noreferrer">
|
||||
Chain 138 visual map
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-gray-500 dark:text-gray-500">
|
||||
Questions about the explorer, chain metadata, route discovery, or liquidity access
|
||||
can be sent to the support mailbox above.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DisclosureSection>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
46
frontend/src/components/common/FooterLinkGroups.tsx
Normal file
46
frontend/src/components/common/FooterLinkGroups.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { footerNavGroups, type FooterNavLink } from '@/data/footerNav'
|
||||
|
||||
const footerLinkClass =
|
||||
'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors'
|
||||
|
||||
function FooterNavLinkItem({ link }: { link: FooterNavLink }) {
|
||||
if (link.external) {
|
||||
return (
|
||||
<li>
|
||||
<a className={footerLinkClass} href={link.href}>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link className={footerLinkClass} href={link.href}>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FooterLinkGroups() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-4 sm:grid-cols-4">
|
||||
{footerNavGroups.map((group) => (
|
||||
<div key={group.id}>
|
||||
<div className="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{group.title}
|
||||
</div>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{group.links.map((link) => (
|
||||
<FooterNavLinkItem key={`${group.id}-${link.href}`} link={link} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
61
frontend/src/components/common/FooterPublicApiLinks.tsx
Normal file
61
frontend/src/components/common/FooterPublicApiLinks.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { explorerPublicApiLinks } from '@/data/explorerOperations'
|
||||
|
||||
const footerLinkClass =
|
||||
'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors'
|
||||
|
||||
function absoluteApiUrl(href: string): string {
|
||||
if (typeof window === 'undefined') return href
|
||||
if (href.startsWith('http://') || href.startsWith('https://')) return href
|
||||
return `${window.location.origin}${href.startsWith('/') ? href : `/${href}`}`
|
||||
}
|
||||
|
||||
export default function FooterPublicApiLinks({ compact = false }: { compact?: boolean }) {
|
||||
const [copiedHref, setCopiedHref] = useState<string | null>(null)
|
||||
|
||||
const copyUrl = async (href: string) => {
|
||||
if (typeof navigator === 'undefined' || !navigator.clipboard) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(absoluteApiUrl(href))
|
||||
setCopiedHref(href)
|
||||
window.setTimeout(() => setCopiedHref((current) => (current === href ? null : current)), 1500)
|
||||
} catch {
|
||||
setCopiedHref(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={compact ? 'space-y-1.5 text-sm' : 'space-y-3 text-sm'}>
|
||||
{explorerPublicApiLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<a
|
||||
className={footerLinkClass}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={compact ? link.description : undefined}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{!compact ? (
|
||||
<p className="mt-0.5 text-xs leading-5 text-gray-500 dark:text-gray-500">{link.description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copyUrl(link.href)}
|
||||
className="shrink-0 rounded-lg border border-gray-200 px-2.5 py-1 text-xs font-medium text-gray-700 transition-colors hover:border-primary-300 hover:text-primary-700 dark:border-gray-700 dark:text-gray-200 dark:hover:border-primary-500 dark:hover:text-primary-300"
|
||||
aria-label={`Copy URL for ${link.label}`}
|
||||
>
|
||||
{copiedHref === link.href ? 'Copied' : 'Copy URL'}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
68
frontend/src/components/common/FreshnessDetail.tsx
Normal file
68
frontend/src/components/common/FreshnessDetail.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { ChainActivityContext } from '@/utils/activityContext'
|
||||
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
|
||||
import { formatInteger } from '@/utils/format'
|
||||
|
||||
export default function FreshnessDetail({
|
||||
context,
|
||||
diagnosticExplanation,
|
||||
activityState,
|
||||
}: {
|
||||
context: ChainActivityContext
|
||||
diagnosticExplanation?: string | null
|
||||
activityState?: string | null
|
||||
}) {
|
||||
if (diagnosticExplanation) {
|
||||
return <>{diagnosticExplanation}</>
|
||||
}
|
||||
|
||||
if (activityState === 'quiet_chain') {
|
||||
const latestNonEmptyBlock =
|
||||
context.last_non_empty_block_number != null ? `#${formatInteger(context.last_non_empty_block_number)}` : 'unknown'
|
||||
const blockGap =
|
||||
context.block_gap_to_latest_transaction != null
|
||||
? `${formatInteger(context.block_gap_to_latest_transaction)} blocks`
|
||||
: 'unknown'
|
||||
return (
|
||||
<>
|
||||
Quiet-chain signal: head blocks may be empty while the chain remains current. Block gap to latest visible
|
||||
transaction: {blockGap}. Last non-empty block: {latestNonEmptyBlock}.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (context.transaction_visibility_unavailable) {
|
||||
return <>Use chain-head visibility and the last non-empty block as the current trust anchors.</>
|
||||
}
|
||||
|
||||
const latestNonEmptyBlock =
|
||||
context.last_non_empty_block_number != null ? `#${formatInteger(context.last_non_empty_block_number)}` : 'unknown'
|
||||
const blockGap =
|
||||
context.block_gap_to_latest_transaction != null
|
||||
? `${formatInteger(context.block_gap_to_latest_transaction)} blocks`
|
||||
: null
|
||||
|
||||
if (context.head_is_idle) {
|
||||
return (
|
||||
<>
|
||||
Latest visible transaction: <ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />. Block
|
||||
gap: {blockGap || 'unknown'}. Last non-empty block: {latestNonEmptyBlock}.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (context.state === 'active') {
|
||||
return (
|
||||
<>
|
||||
Latest visible transaction: <ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />.
|
||||
Recent indexed activity remains close to the tip.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
Latest visible transaction: <ClientRelativeTime value={context.latest_transaction_timestamp} suffix=" ago" />. Recent
|
||||
head blocks may be quiet even while the chain remains current.
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -5,14 +5,18 @@ import {
|
||||
resolveFreshnessSourceLabel,
|
||||
summarizeFreshnessConfidence,
|
||||
} from '@/utils/explorerFreshness'
|
||||
import { formatRelativeAge } from '@/utils/format'
|
||||
import FreshnessDetail from './FreshnessDetail'
|
||||
import { useUiMode } from './UiModeContext'
|
||||
|
||||
function buildSummary(context: ChainActivityContext) {
|
||||
function buildSummary(context: ChainActivityContext, activityState?: string | null) {
|
||||
if (context.transaction_visibility_unavailable) {
|
||||
return 'Chain-head visibility is current, while transaction freshness is currently unavailable.'
|
||||
}
|
||||
|
||||
if (activityState === 'quiet_chain' || (context.head_is_idle && context.state === 'low')) {
|
||||
return 'The chain head is current, but recent head blocks are quiet — this is normal low-activity visibility, not a broken index.'
|
||||
}
|
||||
|
||||
if (context.state === 'active') {
|
||||
return 'Chain head and latest indexed transactions are closely aligned.'
|
||||
}
|
||||
@@ -28,30 +32,6 @@ function buildSummary(context: ChainActivityContext) {
|
||||
return 'Freshness context is based on the latest visible public explorer evidence.'
|
||||
}
|
||||
|
||||
function buildDetail(context: ChainActivityContext, diagnosticExplanation?: string | null) {
|
||||
if (diagnosticExplanation) {
|
||||
return diagnosticExplanation
|
||||
}
|
||||
|
||||
if (context.transaction_visibility_unavailable) {
|
||||
return 'Use chain-head visibility and the last non-empty block as the current trust anchors.'
|
||||
}
|
||||
|
||||
const latestTxAge = formatRelativeAge(context.latest_transaction_timestamp)
|
||||
const latestNonEmptyBlock =
|
||||
context.last_non_empty_block_number != null ? `#${context.last_non_empty_block_number.toLocaleString()}` : 'unknown'
|
||||
|
||||
if (context.head_is_idle) {
|
||||
return `Latest visible transaction: ${latestTxAge}. Last non-empty block: ${latestNonEmptyBlock}.`
|
||||
}
|
||||
|
||||
if (context.state === 'active') {
|
||||
return `Latest visible transaction: ${latestTxAge}. Recent indexed activity remains close to the tip.`
|
||||
}
|
||||
|
||||
return `Latest visible transaction: ${latestTxAge}. Recent head blocks may be quiet even while the chain remains current.`
|
||||
}
|
||||
|
||||
function normalizeSentence(value?: string | null): string {
|
||||
if (!value) return ''
|
||||
return value.trim().replace(/[.\s]+$/, '')
|
||||
@@ -74,13 +54,14 @@ export default function FreshnessTrustNote({
|
||||
const sourceLabel = resolveFreshnessSourceLabel(stats, bridgeStatus)
|
||||
const confidenceBadges = summarizeFreshnessConfidence(stats, bridgeStatus)
|
||||
const diagnosticExplanation = stats?.diagnostics?.explanation || bridgeStatus?.data?.diagnostics?.explanation || null
|
||||
const activityState = stats?.diagnostics?.activity_state || bridgeStatus?.data?.diagnostics?.activity_state || null
|
||||
const normalizedClassName = className ? ` ${className}` : ''
|
||||
|
||||
if (mode === 'expert') {
|
||||
return (
|
||||
<div className={`rounded-2xl border border-gray-200 bg-white/80 px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-950/40${normalizedClassName}`}>
|
||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context)}</div>
|
||||
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context, activityState)}</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">{sourceLabel}</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
@@ -99,10 +80,14 @@ export default function FreshnessTrustNote({
|
||||
|
||||
return (
|
||||
<div className={`rounded-2xl border border-gray-200 bg-white/80 px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-950/40${normalizedClassName}`}>
|
||||
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context)}</div>
|
||||
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context, activityState)}</div>
|
||||
<div className="mt-1 text-gray-600 dark:text-gray-400">
|
||||
{normalizeSentence(buildDetail(context, diagnosticExplanation))}.{' '}
|
||||
{scopeLabel ? `${normalizeSentence(scopeLabel)}. ` : ''}
|
||||
<FreshnessDetail
|
||||
context={context}
|
||||
diagnosticExplanation={diagnosticExplanation}
|
||||
activityState={activityState}
|
||||
/>
|
||||
. {scopeLabel ? `${normalizeSentence(scopeLabel)}. ` : ''}
|
||||
{normalizeSentence(sourceLabel)}.
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -12,11 +12,26 @@ const STANDARD_EXPLANATIONS: Record<string, string> = {
|
||||
'ERC-2612': 'Permit support for signature-based approvals without a separate on-chain approve transaction.',
|
||||
'ERC-3009': 'Authorization-based transfer model for signed payment flows without prior allowances.',
|
||||
'ERC-5267': 'Discoverable EIP-712 domain introspection so wallets and relayers can inspect the signing domain cleanly.',
|
||||
IeMoneyToken: 'Repo-native eMoney token methodology for issuance and redemption semantics.',
|
||||
CashElectronicMoneyInterface: 'Repo-native GRU instrument methodology for issuance and redemption semantics.',
|
||||
DeterministicStorageNamespace: 'Stable namespace for upgrade-aware policy, registry, and audit resolution.',
|
||||
JurisdictionAndSupervisionMetadata: 'Governance, supervisory, disclosure, and reporting metadata required by the GRU operating model.',
|
||||
}
|
||||
|
||||
const STANDARD_DISPLAY_LABELS: Record<string, string> = {
|
||||
CashElectronicMoneyInterface: 'Cash electronic-money interface',
|
||||
DeterministicStorageNamespace: 'Deterministic storage namespace',
|
||||
JurisdictionAndSupervisionMetadata: 'Jurisdiction and supervision metadata',
|
||||
}
|
||||
|
||||
function formatStandardLabel(id: string): string {
|
||||
return STANDARD_DISPLAY_LABELS[id] || id
|
||||
}
|
||||
|
||||
function formatProfileLabel(id: string): string {
|
||||
if (id === 'gru-c-star-v2-public-network-and-payment') return 'GRU C* v2 payment profile'
|
||||
return id
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number | null): string | null {
|
||||
if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) return null
|
||||
const units = [
|
||||
@@ -56,16 +71,16 @@ export default function GruStandardsCard({
|
||||
? `Review the live contract ABI and deployment against the GRU v2 base-token matrix before treating this asset as fully canonical.`
|
||||
: `The live contract exposes the full required GRU v2 base-token surface currently checked by the explorer.`,
|
||||
profile.wrappedTransport
|
||||
? 'This looks like a wrapped transport asset, so confirm the corresponding bridge lane and reserve-verifier posture in addition to the token ABI.'
|
||||
: 'This looks like a canonical GRU asset, so the next meaningful checks are reserve, governance, and transport activation beyond the token interface itself.',
|
||||
? 'This looks like a cW public-network representation, so confirm the corresponding bridge lane and reserve-verifier posture in addition to the token ABI.'
|
||||
: 'This looks like a canonical GRU asset, so the next meaningful checks are reserve, governance, and bridge activation beyond the token interface itself.',
|
||||
profile.x402Ready
|
||||
? 'This contract appears ready for x402-style payment flows because the explorer can see the required signature and domain surfaces.'
|
||||
: 'This contract does not currently look x402-ready from the live explorer surface; verify EIP-712, ERC-5267, and permit or authorization flow exposure before using it as a payment rail.',
|
||||
profile.forwardCanonical === true
|
||||
? 'This version is marked forward-canonical, so it should be treated as the preferred successor surface even if older liquidity or transport versions still coexist.'
|
||||
? 'This version is marked forward-canonical, so it should be treated as the preferred successor surface even if older liquidity or bridge versions still coexist.'
|
||||
: profile.forwardCanonical === false
|
||||
? 'This version is not forward-canonical, which usually means it is legacy, staged, or transport-only relative to the intended primary canonical surface.'
|
||||
: 'Forward-canonical posture is not directly detectable on this contract, so rely on the transport overlay and deployment records before making promotion assumptions.',
|
||||
? 'This version is not forward-canonical, which usually means it is legacy, staged, or bridge-only relative to the intended primary canonical surface.'
|
||||
: 'Forward-canonical posture is not directly detectable on this contract, so rely on the bridge overlay and deployment records before making promotion assumptions.',
|
||||
profile.legacyAliasSupport
|
||||
? 'Legacy alias support is exposed, which is useful during version cutovers and explorer/search reconciliation.'
|
||||
: 'Legacy alias support is not visible from the current explorer contract surface, so name/version migration may need registry or deployment-record cross-checks.',
|
||||
@@ -78,9 +93,9 @@ export default function GruStandardsCard({
|
||||
<DetailRow label="Profile">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<EntityBadge label={profile.profileId} tone="info" className="normal-case tracking-normal" />
|
||||
<EntityBadge label={formatProfileLabel(profile.profileId)} tone="info" className="normal-case tracking-normal" />
|
||||
<EntityBadge
|
||||
label={profile.wrappedTransport ? 'wrapped transport' : 'canonical GRU'}
|
||||
label={profile.wrappedTransport ? 'cW public-network' : 'canonical GRU'}
|
||||
tone={profile.wrappedTransport ? 'warning' : 'success'}
|
||||
/>
|
||||
</div>
|
||||
@@ -94,14 +109,14 @@ export default function GruStandardsCard({
|
||||
{profile.standards.map((standard) => (
|
||||
<EntityBadge
|
||||
key={standard.id}
|
||||
label={standard.detected ? `${standard.id} detected` : `${standard.id} missing`}
|
||||
label={standard.detected ? `${formatStandardLabel(standard.id)} detected` : `${formatStandardLabel(standard.id)} missing`}
|
||||
tone={standard.detected ? 'success' : 'warning'}
|
||||
className="normal-case tracking-normal"
|
||||
/>
|
||||
))}
|
||||
</DetailRow>
|
||||
|
||||
<DetailRow label="Transport Posture">
|
||||
<DetailRow label="Bridge Posture">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<EntityBadge
|
||||
@@ -134,8 +149,8 @@ export default function GruStandardsCard({
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Settlement posture</div>
|
||||
<div className="mt-2 text-gray-900 dark:text-white">
|
||||
{profile.wrappedTransport
|
||||
? 'This contract presents itself like a wrapped public-transport asset instead of the canonical Chain 138 money surface.'
|
||||
: 'This contract presents itself like the canonical Chain 138 GRU money surface instead of a wrapped transport mirror.'}
|
||||
? 'This contract presents itself like a cW public-network representation instead of the canonical Chain 138 GRU surface.'
|
||||
: 'This contract presents itself like the canonical Chain 138 GRU surface instead of a cW public-network representation.'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
|
||||
@@ -150,7 +165,7 @@ export default function GruStandardsCard({
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Version posture</div>
|
||||
<div className="mt-2 text-gray-900 dark:text-white">
|
||||
{profile.activeVersion || profile.forwardVersion
|
||||
? `Active liquidity/transport version: ${profile.activeVersion || 'unknown'}; preferred forward version: ${profile.forwardVersion || 'unknown'}.`
|
||||
? `Active liquidity/bridge version: ${profile.activeVersion || 'unknown'}; preferred forward version: ${profile.forwardVersion || 'unknown'}.`
|
||||
: 'No explicit active-versus-forward version posture is available from the local GRU catalog yet.'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,7 +178,7 @@ export default function GruStandardsCard({
|
||||
{profile.standards.map((standard) => (
|
||||
<div key={`${standard.id}-explanation`} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="font-medium text-gray-900 dark:text-white">{standard.id}</div>
|
||||
<div className="font-medium text-gray-900 dark:text-white">{formatStandardLabel(standard.id)}</div>
|
||||
<EntityBadge label={standard.detected ? 'detected' : 'missing'} tone={standard.detected ? 'success' : 'warning'} />
|
||||
</div>
|
||||
<div className="mt-2 text-gray-600 dark:text-gray-400">
|
||||
@@ -190,12 +205,12 @@ export default function GruStandardsCard({
|
||||
<DetailRow label="References">
|
||||
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div><Link href="/docs/gru" className="text-primary-600 hover:underline">Explorer GRU guide</Link></div>
|
||||
<div>Canonical profile: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">{profile.profileId}</code></div>
|
||||
<div>Repo standards matrix: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">docs/04-configuration/GRU_C_STAR_V2_STANDARDS_MATRIX_AND_IMPLEMENTATION_PLAN.md</code></div>
|
||||
<div>Machine-readable profile: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">config/gru-standards-profile.json</code></div>
|
||||
<div>Transport overlay: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">config/gru-transport-active.json</code></div>
|
||||
<div>x402 support note: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">docs/04-configuration/CHAIN138_X402_TOKEN_SUPPORT.md</code></div>
|
||||
<div>Chain 138 readiness guide: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">docs/04-configuration/GRU_V2_CHAIN138_READINESS.md</code></div>
|
||||
<div>Canonical profile: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">{formatProfileLabel(profile.profileId)}</code></div>
|
||||
<div>Standards matrix: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">GRU C* v2 implementation plan</code></div>
|
||||
<div>Machine-readable profile: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">GRU standards profile</code></div>
|
||||
<div>Public-network overlay: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">GRU cW representation registry</code></div>
|
||||
<div>x402 support note: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">Chain 138 x402 token support</code></div>
|
||||
<div>Chain 138 readiness guide: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800">GRU v2 Chain 138 readiness</code></div>
|
||||
</div>
|
||||
</DetailRow>
|
||||
|
||||
|
||||
81
frontend/src/components/common/HoBackingSummaryCard.tsx
Normal file
81
frontend/src/components/common/HoBackingSummaryCard.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import type { TokenLiquidityMarketView } from '@/utils/tokenMarket'
|
||||
import {
|
||||
formatCashInBankLabel,
|
||||
formatHoLiquidityAuditFreshness,
|
||||
formatHoReserveCoverageLabel,
|
||||
formatSupplyNostroMatchLabel,
|
||||
formatTotalSupplyHoBackingLabel,
|
||||
resolveDisplayedCashInBankUsd,
|
||||
} from '@/utils/tokenMarket'
|
||||
import { formatUsdValue } from '@/utils/displayCurrency'
|
||||
|
||||
type HoBackingSummaryCardProps = {
|
||||
market: TokenLiquidityMarketView & { totalSupplyUsd?: number }
|
||||
}
|
||||
|
||||
export default function HoBackingSummaryCard({ market }: HoBackingSummaryCardProps) {
|
||||
const coverage = market.hoReserveCoverage
|
||||
const nostroMatch = market.supplyNostroMatch
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
HO backing & audit
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{formatHoLiquidityAuditFreshness(market)}</div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<div className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="font-semibold text-gray-900 dark:text-white">Nostro settlement (GL 13010)</div>
|
||||
<div>
|
||||
{formatCashInBankLabel(
|
||||
resolveDisplayedCashInBankUsd(market),
|
||||
market.cashInBankAudit,
|
||||
market.supplyNostroMatch,
|
||||
)}
|
||||
</div>
|
||||
{nostroMatch ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EntityBadge
|
||||
label={formatSupplyNostroMatchLabel(nostroMatch)}
|
||||
tone={nostroMatch.status === 'matched' ? 'success' : nostroMatch.status === 'break' ? 'warning' : 'neutral'}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{market.totalSupplyUsd != null ? (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatTotalSupplyHoBackingLabel(market.totalSupplyUsd, market.supplyNostroMatch)}
|
||||
</div>
|
||||
) : null}
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Per-token supply vs 1.2× external nostro cash. Large c* supply can break while reserve economics remain sufficient. GL 2100 M1 book money is never cash-in-bank.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="font-semibold text-gray-900 dark:text-white">Reserve coverage (GL 1000+1050)</div>
|
||||
{coverage ? (
|
||||
<>
|
||||
<div>{formatHoReserveCoverageLabel(coverage)}</div>
|
||||
<EntityBadge
|
||||
label={coverage.status === 'sufficient' ? 'Reserve sufficient' : coverage.status}
|
||||
tone={coverage.status === 'sufficient' ? 'success' : coverage.status === 'insufficient' ? 'warning' : 'neutral'}
|
||||
/>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Reserve assets {formatUsdValue(coverage.reserveAssetsUsd)} · required{' '}
|
||||
{formatUsdValue(coverage.requiredReserveUsd)} · aggregate issued c*{' '}
|
||||
{formatUsdValue(coverage.issuedCStarUsd)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-gray-500">Reserve coverage unavailable</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
M0 reserve stock vs 1.2× aggregate Chain 138 issued c*. Not correspondent nostro cash and not GL 2100 M1 book money.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
45
frontend/src/components/common/ListFilterBar.tsx
Normal file
45
frontend/src/components/common/ListFilterBar.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
|
||||
import { formatInteger } from '@/utils/format'
|
||||
|
||||
interface ListFilterBarProps {
|
||||
query: string
|
||||
onQueryChange: (value: string) => void
|
||||
placeholder?: string
|
||||
resultCount?: number
|
||||
totalCount?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function ListFilterBar({
|
||||
query,
|
||||
onQueryChange,
|
||||
placeholder = 'Filter…',
|
||||
resultCount,
|
||||
totalCount,
|
||||
className = '',
|
||||
}: ListFilterBarProps) {
|
||||
const showCounts = typeof resultCount === 'number' && typeof totalCount === 'number'
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between ${className}`}>
|
||||
<label className="block min-w-0 flex-1">
|
||||
<span className="sr-only">{placeholder}</span>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(event) => onQueryChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500"
|
||||
/>
|
||||
</label>
|
||||
{showCounts ? (
|
||||
<div className="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||
{resultCount === totalCount
|
||||
? `${formatInteger(totalCount)} items`
|
||||
: `${formatInteger(resultCount)} of ${formatInteger(totalCount)}`}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
59
frontend/src/components/common/MarketEvidenceNote.tsx
Normal file
59
frontend/src/components/common/MarketEvidenceNote.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
|
||||
import { formatTimestamp } from '@/utils/format'
|
||||
|
||||
function formatSource(source?: string | null): string {
|
||||
switch (source) {
|
||||
case 'token-aggregation':
|
||||
return 'token aggregation API'
|
||||
case 'blockscout':
|
||||
return 'Blockscout index'
|
||||
case 'derived':
|
||||
return 'derived from indexed supply and price inputs'
|
||||
case 'mission-control':
|
||||
return 'mission-control liquidity inventory'
|
||||
default:
|
||||
return source || 'source unavailable'
|
||||
}
|
||||
}
|
||||
|
||||
export default function MarketEvidenceNote({
|
||||
source = 'token-aggregation',
|
||||
lastUpdated,
|
||||
method = 'DEX route and pool aggregation; visible liquidity only where indexed.',
|
||||
compact = false,
|
||||
}: {
|
||||
source?: string | null
|
||||
lastUpdated?: string | null
|
||||
method?: string
|
||||
compact?: boolean
|
||||
}) {
|
||||
return (
|
||||
<p className={`${compact ? 'mt-1' : 'mt-3'} text-xs leading-5 text-gray-500 dark:text-gray-400`}>
|
||||
{compact ? (
|
||||
<>
|
||||
Updated{' '}
|
||||
{lastUpdated ? (
|
||||
<>
|
||||
<ClientRelativeTime value={lastUpdated} suffix={` (${formatTimestamp(lastUpdated)})`} />
|
||||
</>
|
||||
) : (
|
||||
'timestamp unavailable'
|
||||
)}{' '}
|
||||
· {formatSource(source)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Source: {formatSource(source)}. Updated:{' '}
|
||||
{lastUpdated ? (
|
||||
<>
|
||||
<ClientRelativeTime value={lastUpdated} suffix={` (${formatTimestamp(lastUpdated)})`} />
|
||||
</>
|
||||
) : (
|
||||
'timestamp unavailable'
|
||||
)}
|
||||
. Method: {method}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
78
frontend/src/components/common/MissionDeliveryModePanel.tsx
Normal file
78
frontend/src/components/common/MissionDeliveryModePanel.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import ClientRelativeTime from '@/components/common/ClientRelativeTime'
|
||||
import type { MissionControlMode } from '@/services/api/missionControl'
|
||||
|
||||
const REASON_LABELS: Record<string, string> = {
|
||||
live_homepage_stream_not_attached: 'Live homepage stream is not attached; relay posture uses snapshot polling.',
|
||||
relay_snapshot_only_source: 'Relay monitoring uses snapshot sources while other explorer feeds remain live.',
|
||||
partial_observability_inputs: 'Some freshness inputs are partial, so posture is reported conservatively.',
|
||||
}
|
||||
|
||||
const SCOPE_LABELS: Record<string, string> = {
|
||||
relay_monitoring_homepage_card_only: 'Affects relay monitoring and the homepage mission card only.',
|
||||
bridge_monitoring_and_homepage: 'Affects bridge monitoring and homepage summary surfaces.',
|
||||
homepage_summary_only: 'Affects homepage summary messaging only.',
|
||||
}
|
||||
|
||||
function humanizeKey(value?: string | null): string {
|
||||
if (!value) return 'Not specified'
|
||||
return SCOPE_LABELS[value] || REASON_LABELS[value] || value.replaceAll('_', ' ')
|
||||
}
|
||||
|
||||
function modeTone(kind?: string | null): 'success' | 'warning' | 'info' | 'neutral' {
|
||||
switch (String(kind || '').toLowerCase()) {
|
||||
case 'live':
|
||||
return 'success'
|
||||
case 'mixed':
|
||||
return 'warning'
|
||||
case 'snapshot':
|
||||
return 'info'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
export default function MissionDeliveryModePanel({
|
||||
mode,
|
||||
title = 'Delivery mode',
|
||||
className = '',
|
||||
}: {
|
||||
mode?: MissionControlMode | null
|
||||
title?: string
|
||||
className?: string
|
||||
}) {
|
||||
if (!mode?.kind) return null
|
||||
|
||||
const normalizedClassName = className ? ` ${className}` : ''
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`border border-indigo-200 bg-indigo-50/60 dark:border-indigo-900/40 dark:bg-indigo-950/20${normalizedClassName}`}
|
||||
title={title}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EntityBadge label={`mode ${mode.kind}`} tone={modeTone(mode.kind)} />
|
||||
{mode.updated_at ? (
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Updated <ClientRelativeTime value={mode.updated_at} suffix=" ago" />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{mode.reason ? (
|
||||
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Reason:</span> {humanizeKey(String(mode.reason))}
|
||||
</p>
|
||||
) : null}
|
||||
{mode.scope ? (
|
||||
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Scope:</span> {humanizeKey(String(mode.scope))}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { accessApi, institutionalTierLabels, type WalletAccessSession } from '@/
|
||||
import BrandLockup from './BrandLockup'
|
||||
import HeaderCommandPalette, { type HeaderCommandItem } from './HeaderCommandPalette'
|
||||
import { useUiMode } from './UiModeContext'
|
||||
import { formatTimestamp } from '@/utils/format'
|
||||
|
||||
type MenuItem = {
|
||||
href?: string
|
||||
@@ -364,6 +365,7 @@ function UiModeToggle({ mobile = false }: { mobile?: boolean }) {
|
||||
function AccountButton({
|
||||
walletSession,
|
||||
connectingWallet,
|
||||
connectError,
|
||||
onConnect,
|
||||
onCopyAddress,
|
||||
onSwitchWallet,
|
||||
@@ -371,6 +373,7 @@ function AccountButton({
|
||||
}: {
|
||||
walletSession: WalletAccessSession | null
|
||||
connectingWallet: boolean
|
||||
connectError?: string | null
|
||||
onConnect: () => void
|
||||
onCopyAddress: () => void
|
||||
onSwitchWallet: () => void
|
||||
@@ -385,7 +388,7 @@ function AccountButton({
|
||||
},
|
||||
{
|
||||
href: '/wallet',
|
||||
label: 'Settings',
|
||||
label: 'Wallet tools',
|
||||
description: 'Review network, token-list, and wallet configuration guidance.',
|
||||
},
|
||||
{
|
||||
@@ -407,21 +410,28 @@ function AccountButton({
|
||||
|
||||
if (!walletSession) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConnect}
|
||||
className="inline-flex items-center gap-2 rounded-2xl bg-gray-950 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:bg-white dark:text-gray-950 dark:hover:bg-gray-100 dark:focus-visible:ring-offset-gray-900"
|
||||
>
|
||||
<span className="inline-flex h-2.5 w-2.5 rounded-full bg-emerald-400" aria-hidden />
|
||||
<span>{connectingWallet ? 'Connecting…' : 'Connect Wallet'}</span>
|
||||
</button>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConnect}
|
||||
className="inline-flex items-center gap-2 rounded-2xl bg-gray-950 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:bg-white dark:text-gray-950 dark:hover:bg-gray-100 dark:focus-visible:ring-offset-gray-900"
|
||||
>
|
||||
<span className="inline-flex h-2.5 w-2.5 rounded-full bg-emerald-400" aria-hidden />
|
||||
<span>{connectingWallet ? 'Connecting…' : 'Connect Wallet'}</span>
|
||||
</button>
|
||||
{connectError ? (
|
||||
<p role="alert" className="max-w-xs text-right text-xs text-red-600 dark:text-red-400">
|
||||
{connectError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sessionSummary = getSessionSummary(walletSession)
|
||||
const tierLabel = getAccessTier(walletSession)
|
||||
const expiresLabel = walletSession.expiresAt
|
||||
? new Date(walletSession.expiresAt).toLocaleString()
|
||||
? formatTimestamp(walletSession.expiresAt)
|
||||
: 'Session expiry unavailable'
|
||||
|
||||
return (
|
||||
@@ -469,6 +479,7 @@ export default function Navbar() {
|
||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
|
||||
const [walletSession, setWalletSession] = useState<WalletAccessSession | null>(null)
|
||||
const [connectingWallet, setConnectingWallet] = useState(false)
|
||||
const [walletConnectError, setWalletConnectError] = useState<string | null>(null)
|
||||
const mobilePanelId = useId()
|
||||
|
||||
const isExploreActive =
|
||||
@@ -478,13 +489,16 @@ export default function Navbar() {
|
||||
pathname.startsWith('/addresses')
|
||||
const isDataActive =
|
||||
pathname.startsWith('/tokens') ||
|
||||
pathname.startsWith('/wallet') ||
|
||||
pathname.startsWith('/analytics') ||
|
||||
pathname.startsWith('/pools') ||
|
||||
pathname.startsWith('/protocols') ||
|
||||
pathname.startsWith('/watchlist')
|
||||
const isOperationsActive =
|
||||
pathname.startsWith('/bridge') ||
|
||||
pathname.startsWith('/routes') ||
|
||||
pathname.startsWith('/liquidity') ||
|
||||
pathname.startsWith('/ho-liquidity') ||
|
||||
pathname.startsWith('/operations') ||
|
||||
pathname.startsWith('/system') ||
|
||||
pathname.startsWith('/operator') ||
|
||||
@@ -528,13 +542,14 @@ export default function Navbar() {
|
||||
const handleConnectWallet = async () => {
|
||||
try {
|
||||
setConnectingWallet(true)
|
||||
setWalletConnectError(null)
|
||||
await accessApi.connectWalletSession()
|
||||
router.push('/access')
|
||||
setMobileMenuOpen(false)
|
||||
router.push('/wallet')
|
||||
} catch (error) {
|
||||
console.error('Wallet connect failed', error)
|
||||
router.push('/access')
|
||||
setMobileMenuOpen(false)
|
||||
const message = error instanceof Error ? error.message : 'Wallet connection failed.'
|
||||
setWalletConnectError(message)
|
||||
} finally {
|
||||
setConnectingWallet(false)
|
||||
}
|
||||
@@ -580,22 +595,26 @@ export default function Navbar() {
|
||||
const dataItems: MenuItem[] = useMemo(
|
||||
() => [
|
||||
{ href: '/tokens', label: 'Tokens', description: 'Review curated assets, standards, and token detail pages.' },
|
||||
{ href: '/wallet', label: 'Wallet tools', description: 'Add Chain 138 to MetaMask and import tokens via EIP-747.' },
|
||||
{ href: '/analytics', label: 'Analytics', description: 'Open explorer-visible transaction and block activity summaries.' },
|
||||
{ href: '/pools', label: 'Pools', description: 'Browse mission-control pool inventory and route-backed liquidity context.' },
|
||||
{ href: '/protocols', label: 'Protocols', description: 'Review official upstream protocol contracts and production guardrails on Chain 138.' },
|
||||
{ href: '/watchlist', label: 'Watchlist', description: 'Jump into tracked addresses and saved explorer entities.' },
|
||||
],
|
||||
[],
|
||||
)
|
||||
const operationsItems: MenuItem[] = useMemo(
|
||||
() => [
|
||||
{ href: '/operations', label: 'Operations Hub', description: 'Open the consolidated operator surface for live support workflows.' },
|
||||
{ href: '/bridge', label: 'Bridge Monitoring', description: 'Inspect relay lanes, queue posture, and bridge trace tooling.' },
|
||||
{ href: '/operations', label: 'Operations hub', description: 'Open the consolidated operator surface for live support workflows.' },
|
||||
{ href: '/ho-liquidity', label: 'HO nostro & reserve', description: 'Fineract GL 13010 nostro cash, HO reserve coverage, and PMM cash liquidity.' },
|
||||
{ href: '/bridge', label: 'Bridge', description: 'Inspect relay lanes, queue posture, and bridge trace tooling.' },
|
||||
{ href: '/routes', label: 'Routes', description: 'Review live route coverage, same-chain lanes, and bridge paths.' },
|
||||
{ href: '/liquidity', label: 'Liquidity', description: 'Check planner-backed route access and live liquidity posture.' },
|
||||
{ href: '/protocols', label: 'Protocols', description: 'Official DODO, UniV2/V3, CCIP, and integration contracts on Chain 138.' },
|
||||
{ href: '/system', label: 'System', description: 'Inspect topology, RPC capability, and public integration inventory.' },
|
||||
{ href: '/operator', label: 'Operator Surface', description: 'Open planner, route, and relay shortcuts in one public page.' },
|
||||
{ href: '/weth', label: 'WETH References', description: 'Review wrapped-asset references and bridge-oriented WETH context.' },
|
||||
{ href: '/chain138-command-center.html', label: 'Command Center', description: 'Open the visual command-center reference.', external: true },
|
||||
{ href: '/operator', label: 'Operator', description: 'Open planner, route, and relay shortcuts in one public page.' },
|
||||
{ href: '/weth', label: 'WETH', description: 'Review wrapped-asset references and bridge-oriented WETH context.' },
|
||||
{ href: '/topology', label: 'Command Center', description: 'Open the visual topology and architecture map.' },
|
||||
],
|
||||
[],
|
||||
)
|
||||
@@ -703,12 +722,12 @@ export default function Navbar() {
|
||||
<>
|
||||
<header className="sticky top-0 z-40 border-b border-gray-200/90 bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/88 dark:border-gray-800 dark:bg-gray-950/92">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex min-h-[76px] items-center gap-4 lg:min-h-[84px]">
|
||||
<div className="flex min-h-[60px] items-center gap-3 lg:min-h-[64px]">
|
||||
<Link
|
||||
href="/"
|
||||
className="group inline-flex min-w-0 items-center gap-3 rounded-2xl py-2 pr-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-950"
|
||||
className="group inline-flex shrink-0 items-center gap-2 rounded-lg py-1.5 pr-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-950 max-sm:max-w-[9.5rem] sm:max-w-none"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label="Go to SolaceScan home"
|
||||
aria-label="Go to DBIS Explorer home"
|
||||
>
|
||||
<BrandLockup />
|
||||
</Link>
|
||||
@@ -727,12 +746,13 @@ export default function Navbar() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="ml-auto hidden items-center gap-3 lg:flex">
|
||||
<div className="ml-auto hidden items-center gap-3 xl:flex">
|
||||
<SearchControl active={isSearchActive} onSelect={() => setCommandPaletteOpen(true)} />
|
||||
<UiModeToggle />
|
||||
<AccountButton
|
||||
walletSession={walletSession}
|
||||
connectingWallet={connectingWallet}
|
||||
connectError={walletConnectError}
|
||||
onConnect={() => void handleConnectWallet()}
|
||||
onCopyAddress={() => void handleCopyAddress()}
|
||||
onSwitchWallet={() => void handleSwitchWallet()}
|
||||
@@ -740,7 +760,7 @@ export default function Navbar() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 lg:hidden">
|
||||
<div className="ml-auto flex items-center gap-2 xl:hidden">
|
||||
{walletSession ? (
|
||||
<Link
|
||||
href="/access"
|
||||
@@ -793,7 +813,7 @@ export default function Navbar() {
|
||||
{mobileMenuOpen ? (
|
||||
<div
|
||||
id={mobilePanelId}
|
||||
className="border-t border-gray-200 py-4 dark:border-gray-800 lg:hidden"
|
||||
className="border-t border-gray-200 py-4 dark:border-gray-800 xl:hidden"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<SearchControl
|
||||
@@ -806,6 +826,12 @@ export default function Navbar() {
|
||||
/>
|
||||
<UiModeToggle mobile />
|
||||
|
||||
{walletConnectError ? (
|
||||
<p role="alert" className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-300">
|
||||
{walletConnectError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-500 dark:text-gray-400">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user