Compare commits

...

14 Commits

Author SHA1 Message Date
defiQUG
e397245ec9 Add market evidence notes to explorer surfaces
All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 3m6s
2026-04-30 03:53:13 -07:00
defiQUG
8cd8bfa195 Unify explorer DBIS taxonomy and branding
All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 2m18s
2026-04-30 03:06:49 -07:00
defiQUG
3b7e24080f Refresh Next type environment
Some checks failed
phoenix-deploy Deploy failed: Command failed: bash scripts/deployment/phoenix-deploy-explorer-live-from-workspace.sh nginx: the configuration file /et
Deploy Explorer Live / deploy (push) Failing after 1m48s
2026-04-30 01:58:15 -07:00
defiQUG
ba08199051 Align GRU explorer terminology
Some checks failed
Deploy Explorer Live / deploy (push) Has been cancelled
2026-04-30 01:57:51 -07:00
defiQUG
0ba2a70c34 Refresh token metadata categories
Some checks failed
Deploy Explorer Live / deploy (push) Has been cancelled
phoenix-deploy Deployed to explorer-live
2026-04-30 01:57:12 -07:00
defiQUG
ac40184d6b Fix SolaceScan frontend service release path
All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 2m10s
2026-04-29 06:42:20 -07:00
defiQUG
7a16ddccf7 Add verified contract source workspace
Some checks failed
phoenix-deploy Deploy failed: Command failed: bash scripts/deployment/phoenix-deploy-explorer-live-from-workspace.sh nginx: the configuration file /et
Deploy Explorer Live / deploy (push) Failing after 3m47s
2026-04-29 06:21:56 -07:00
defiQUG
1f5167aded Expose full verified contract source payloads 2026-04-29 06:21:36 -07:00
defiQUG
f5eb874210 Harden VMID 5000 frontend deploy server discovery 2026-04-29 06:19:32 -07:00
defiQUG
1aa81f454a feat(explorer): add live token/native pricing and legacy tx route compatibility
Some checks failed
phoenix-deploy Deploy failed: Command failed: bash scripts/deployment/phoenix-deploy-explorer-live-from-workspace.sh nginx: the configuration file /et
Deploy Explorer Live / deploy (push) Failing after 4m8s
2026-04-25 23:45:07 -07:00
Codex
1b5cebf505 Add Gitea live redeploy workflow
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 8s
phoenix-deploy Deployed to explorer-live
2026-04-23 09:51:01 -07:00
fe9edd842b Merge pull request 'security: tighten gitleaks regex + document history-purge audit trail' (#14) from devin/1776542851-harden-gitleaks-and-document-purge into master
Some checks failed
CI / Backend (go 1.23.x) (push) Successful in 51s
CI / Backend security scanners (push) Failing after 45s
CI / Frontend (node 20) (push) Successful in 2m5s
CI / gitleaks (secret scan) (push) Failing after 7s
e2e-full / e2e-full (push) Failing after 21s
2026-04-18 20:08:58 +00:00
fdb14dc420 security: tighten gitleaks regex for escaped form, document history-purge audit trail
Some checks failed
CI / Backend (go 1.23.x) (pull_request) Successful in 56s
CI / Backend security scanners (pull_request) Failing after 40s
CI / Frontend (node 20) (pull_request) Successful in 2m19s
CI / gitleaks (secret scan) (pull_request) Failing after 7s
e2e-full / e2e-full (pull_request) Has been skipped
Two small follow-ups to the out-of-band git-history rewrite that
purged L@ker$2010 / L@kers2010 / L@ker\$2010 from every branch and
tag:

.gitleaks.toml:
  - Regex was L@kers?\$?2010 which catches the expanded form but
    NOT the shell-escaped form (L@ker\$2010) that slipped past PR #3
    in scripts/setup-database.sh. PR #13 fixed the live leak but did
    not tighten the detector. New regex L@kers?\\?\$?2010 catches
    both forms so future pastes of either form fail CI.
  - Description rewritten without the literal password (the previous
    description was redacted by the history rewrite itself and read
    'Legacy hardcoded ... (***REDACTED-LEGACY-PW*** / ***REDACTED-LEGACY-PW***)'
    which was cryptic).

docs/SECURITY.md:
  - New 'History-purge audit trail' section recording what was done,
    how it was verified (0 literal password matches in any blob or
    commit message; 0 legacy-password findings from a post-rewrite
    gitleaks scan), and what operator cleanup is still required on
    the Gitea host to drop the 13 refs/pull/*/head refs that still
    pin the pre-rewrite commits (the update hook declined those refs
    over HTTPS, so only an admin on the Gitea VM can purge them via
    'git update-ref -d' + 'git gc --prune=now' in the bare repo).
  - New 'Re-introduction guard' subsection pointing at the tightened
    regex and commit 78e1ff5.

Verification:
  gitleaks detect --no-git --source . --config .gitleaks.toml   # 0 legacy hits
  git log --all -p | grep -cE 'L@ker\$2010|L@kers2010'         # 0
2026-04-18 20:08:13 +00:00
7c018965eb Merge pull request 'fix(scripts): require DB_PASSWORD env var in setup-database.sh' (#13) from devin/1776542488-fix-setup-database-hardcoded-password into master
Some checks failed
CI / Backend (go 1.23.x) (push) Has been cancelled
CI / Backend security scanners (push) Has been cancelled
CI / Frontend (node 20) (push) Has been cancelled
CI / gitleaks (secret scan) (push) Has been cancelled
2026-04-18 20:02:37 +00:00
72 changed files with 13587 additions and 6704 deletions

View File

@@ -0,0 +1,43 @@
name: Deploy Explorer Live
on:
workflow_dispatch:
push:
branches: [main, master]
paths:
- '.gitea/workflows/deploy-live.yml'
- 'backend/**'
- 'config/**'
- 'deployment/**'
- 'docs/**'
- 'frontend/**'
- 'scripts/**'
- 'package.json'
- 'package-lock.json'
- 'Makefile'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Validate live deploy assets
run: |
test -f scripts/deploy-explorer-config-to-vmid5000.sh
test -f scripts/deploy-explorer-ai-to-vmid5000.sh
test -f scripts/deploy-next-frontend-to-vmid5000.sh
test -f deployment/LIVE_DEPLOYMENT_MAP.md
- name: Trigger explorer-live deployment
run: |
SHA="$(git rev-parse HEAD)"
BRANCH="${GITHUB_REF_NAME:-}"
if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
fi
curl -sSf -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\"}"

View File

@@ -12,8 +12,8 @@ useDefault = true
[[rules]] [[rules]]
id = "explorer-legacy-db-password-L@ker" id = "explorer-legacy-db-password-L@ker"
description = "Legacy hardcoded Postgres / SSH password (***REDACTED-LEGACY-PW*** / ***REDACTED-LEGACY-PW***)" description = "Legacy hardcoded Postgres / SSH password (redacted). Matches both the expanded form and the shell-escaped form (backslash-dollar) that appeared in scripts/setup-database.sh."
regex = '''L@kers?\$?2010''' regex = '''L@kers?\\?\$?2010'''
tags = ["password", "explorer-legacy"] tags = ["password", "explorer-legacy"]
[allowlist] [allowlist]

View File

@@ -5,7 +5,7 @@
"minor": 1, "minor": 1,
"patch": 0 "patch": 0
}, },
"generatedBy": "SolaceScan", "generatedBy": "DBIS Explorer",
"timestamp": "2026-03-28T00:00:00Z", "timestamp": "2026-03-28T00:00:00Z",
"chainId": 138, "chainId": 138,
"chainName": "DeFi Oracle Meta Mainnet", "chainName": "DeFi Oracle Meta Mainnet",

View File

@@ -4,7 +4,7 @@
"defaultChainId": 138, "defaultChainId": 138,
"explorerUrl": "https://explorer.d-bis.org", "explorerUrl": "https://explorer.d-bis.org",
"tokenListUrl": "https://explorer.d-bis.org/api/config/token-list", "tokenListUrl": "https://explorer.d-bis.org/api/config/token-list",
"generatedBy": "SolaceScan", "generatedBy": "DBIS Explorer",
"chains": [ "chains": [
{"chainId":"0x8a","chainIdDecimal":138,"chainName":"DeFi Oracle Meta Mainnet","shortName":"dbis","rpcUrls":["https://rpc-http-pub.d-bis.org","https://rpc.d-bis.org","https://rpc2.d-bis.org","https://rpc.defi-oracle.io"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://explorer.d-bis.org","https://blockscout.defi-oracle.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"],"infoURL":"https://explorer.d-bis.org","explorerApiUrl":"https://explorer.d-bis.org/api/v2","testnet":false}, {"chainId":"0x8a","chainIdDecimal":138,"chainName":"DeFi Oracle Meta Mainnet","shortName":"dbis","rpcUrls":["https://rpc-http-pub.d-bis.org","https://rpc.d-bis.org","https://rpc2.d-bis.org","https://rpc.defi-oracle.io"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://explorer.d-bis.org","https://blockscout.defi-oracle.io"],"iconUrls":["https://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":"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},

View File

@@ -485,7 +485,7 @@
], ],
"blockers": [ "blockers": [
"Desired public EVM targets still missing cW suites: Wemix.", "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." "Arbitrum bootstrap remains blocked on the current Mainnet hub leg: tx 0x97df657f0e31341ca852666766e553650531bbcc86621246d041985d7261bb07 reverted before any bridge event was emitted."
], ],
"resolutionMatrix": [ "resolutionMatrix": [
@@ -540,7 +540,7 @@
{ {
"key": "wave1_transport_pending", "key": "wave1_transport_pending",
"state": "open", "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": [ "targets": [
{ {
"code": "EUR", "code": "EUR",
@@ -614,7 +614,7 @@
], ],
"resolution": [ "resolution": [
"Enable bridge controls and supervision policy for each Wave 1 canonical asset on Chain 138.", "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." "Verify the overlay promotion with check-gru-global-priority-rollout.sh and check-gru-v2-chain138-readiness.sh before attaching public liquidity."
], ],
"runbooks": [ "runbooks": [
@@ -623,7 +623,7 @@
"scripts/verify/check-gru-global-priority-rollout.sh", "scripts/verify/check-gru-global-priority-rollout.sh",
"scripts/verify/check-gru-v2-chain138-readiness.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", "key": "first_tier_public_pools_not_live",
@@ -801,9 +801,9 @@
} }
], ],
"resolution": [ "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.", "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": [ "runbooks": [
"config/gru-global-priority-currency-rollout.json", "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.", "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.", "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.", "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": [ "runbooks": [
"config/solana-gru-bridge-lineup.json", "config/solana-gru-bridge-lineup.json",
@@ -842,7 +842,7 @@
} }
], ],
"notes": [ "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." "Chain 138 canonical venues remain a separate live surface from the public cW mesh."
] ]
} }

View File

@@ -265,7 +265,7 @@
"nextStep": "activate_transport_and_attach_public_liquidity" "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": { "protocols": {
"publicCwMesh": [ "publicCwMesh": [

View File

@@ -520,7 +520,7 @@ func (s *Server) HandleMissionControlBridgeTrace(w http.ResponseWriter, r *http.
"from_registry": fromLabel, "from_registry": fromLabel,
"to": toAddr, "to": toAddr,
"to_registry": toLabel, "to_registry": toLabel,
"blockscout_url": publicBase + "/tx/" + strings.ToLower(tx), "blockscout_url": publicBase + "/transactions/" + strings.ToLower(tx),
"source": source, "source": source,
} }
if registryLoadErr != nil && len(reg) == 0 { if registryLoadErr != nil && len(reg) == 0 {

View File

@@ -177,7 +177,7 @@ func TestHandleMissionControlBridgeTraceLabelsFromRegistry(t *testing.T) {
require.Equal(t, strings.ToLower(toAddr), out.Data["to"]) require.Equal(t, strings.ToLower(toAddr), out.Data["to"])
require.Equal(t, "CHAIN138_SOURCE_BRIDGE", out.Data["from_registry"]) require.Equal(t, "CHAIN138_SOURCE_BRIDGE", out.Data["from_registry"])
require.Equal(t, "CHAIN138_DEST_BRIDGE", out.Data["to_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) { func TestHandleMissionControlBridgeTraceFallsBackToAddressInventoryLabels(t *testing.T) {

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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);

View File

@@ -479,7 +479,7 @@ EOF
```bash ```bash
cat > /etc/systemd/system/solacescanscout-frontend.service << 'EOF' cat > /etc/systemd/system/solacescanscout-frontend.service << 'EOF'
[Unit] [Unit]
Description=SolaceScan Next Frontend Service Description=DBIS Explorer Next Frontend Service
After=network.target explorer-api.service After=network.target explorer-api.service
Requires=explorer-api.service Requires=explorer-api.service

View File

@@ -1,6 +1,6 @@
# Live Deployment Map # 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?" 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?"

View File

@@ -20,6 +20,7 @@ That file reflects the live split deployment now in production:
- Frontend deploy: [`scripts/deploy-next-frontend-to-vmid5000.sh`](../scripts/deploy-next-frontend-to-vmid5000.sh) - Frontend deploy: [`scripts/deploy-next-frontend-to-vmid5000.sh`](../scripts/deploy-next-frontend-to-vmid5000.sh)
- Config deploy: [`scripts/deploy-explorer-config-to-vmid5000.sh`](../scripts/deploy-explorer-config-to-vmid5000.sh) - Config deploy: [`scripts/deploy-explorer-config-to-vmid5000.sh`](../scripts/deploy-explorer-config-to-vmid5000.sh)
- Explorer config/API deploy: [`scripts/deploy-explorer-ai-to-vmid5000.sh`](../scripts/deploy-explorer-ai-to-vmid5000.sh) - Explorer config/API deploy: [`scripts/deploy-explorer-ai-to-vmid5000.sh`](../scripts/deploy-explorer-ai-to-vmid5000.sh)
- Gitea live redeploy action: [`.gitea/workflows/deploy-live.yml`](../.gitea/workflows/deploy-live.yml), target `explorer-live`
- RPC/API-key edge enforcement: [`ACCESS_EDGE_ENFORCEMENT_RUNBOOK.md`](./ACCESS_EDGE_ENFORCEMENT_RUNBOOK.md) - RPC/API-key edge enforcement: [`ACCESS_EDGE_ENFORCEMENT_RUNBOOK.md`](./ACCESS_EDGE_ENFORCEMENT_RUNBOOK.md)
- Public health audit: [`scripts/check-explorer-health.sh`](../scripts/check-explorer-health.sh) - Public health audit: [`scripts/check-explorer-health.sh`](../scripts/check-explorer-health.sh)
- Full public smoke: [`check-explorer-e2e.sh`](../../scripts/verify/check-explorer-e2e.sh) - Full public smoke: [`check-explorer-e2e.sh`](../../scripts/verify/check-explorer-e2e.sh)

View File

@@ -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: # Keep the existing higher-priority locations for:
# - /api/ # - /api/
# - /api/config/token-list # - /api/config/token-list

View File

@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=SolaceScan Next Frontend Service Description=DBIS Explorer Next Frontend Service
After=network.target After=network.target
Wants=network.target Wants=network.target

View File

@@ -224,7 +224,7 @@ User → chainlist.org → Search "DBIS" → Click "Add to MetaMask"
``` ```
User → MetaMask → Click "View on Explorer" 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 displays transaction details
→ Blockscout API provides the data → Blockscout API provides the data
``` ```
@@ -285,4 +285,3 @@ User → MetaMask → View Token Balance
**Last Updated**: 2025-12-24 **Last Updated**: 2025-12-24
**Status**: Analysis Complete **Status**: Analysis Complete

View File

@@ -63,6 +63,58 @@ initial public review.
- Purging from history (`git filter-repo`) does **not** retroactively - Purging from history (`git filter-repo`) does **not** retroactively
secure a leaked secret — rotate first, clean history later. secure a leaked secret — rotate first, clean history later.
## History-purge audit trail
Following the rotation checklist above, the legacy `L@ker$2010` /
`L@kers2010` / `L@ker\$2010` password strings were purged from every
branch and tag in this repository using `git filter-repo
--replace-text` followed by a `--replace-message` pass for commit
message text. The rewritten history was force-pushed with
`git push --mirror --force`.
Verification post-rewrite:
```
git log --all -p | grep -cE 'L@ker\$2010|L@kers2010|L@ker\\\$2010'
0
gitleaks detect --no-git --source . --config .gitleaks.toml
0 legacy-password findings
```
### Residual server-side state (not purgable from the client)
Gitea's `refs/pull/*/head` refs (the read-only mirror of each PR's
original head commit) **cannot be force-updated over HTTPS** — the
server's `update` hook declines them. After a history rewrite the
following cleanup must be performed **on the Gitea host** by an
administrator:
1. Run `gitea admin repo-sync-release-archive` and
`gitea doctor --run all --fix` if available.
2. Or manually, as the gitea user on the server:
```bash
cd /var/lib/gitea/data/gitea-repositories/d-bis/explorer-monorepo.git
git for-each-ref --format='%(refname)' 'refs/pull/*/head' | \
xargs -n1 git update-ref -d
git gc --prune=now --aggressive
```
3. Restart Gitea.
Until this server-side cleanup is performed, the 13 `refs/pull/*/head`
refs still pin the pre-rewrite commits containing the legacy
password. This does not affect branches, the default clone, or
`master` — but the old commits remain reachable by SHA through the
Gitea web UI (e.g. on the merged PR's **Files Changed** tab).
### Re-introduction guard
The `.gitleaks.toml` rule `explorer-legacy-db-password-L@ker` was
tightened from `L@kers?\$?2010` to `L@kers?\\?\$?2010` so it also
catches the shell-escaped form that slipped past the original PR #3
scrub (see commit `78e1ff5`). Future attempts to paste any variant of
the legacy password — in source, shell scripts, or env files — will
fail the `gitleaks` CI job wired in PR #5.
## Build-time / CI checks (wired in PR #5) ## Build-time / CI checks (wired in PR #5)
- `gitleaks` pre-commit + CI gate on every PR. - `gitleaks` pre-commit + CI gate on every PR.

View File

@@ -1,5 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited // 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.

View File

@@ -1,9 +1,17 @@
const path = require('path')
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: 'standalone', output: 'standalone',
outputFileTracingRoot: path.resolve(__dirname, '..', '..'),
async redirects() { async redirects() {
return [ return [
{
source: '/tx/:hash',
destination: '/transactions/:hash',
permanent: true,
},
{ {
source: '/more', source: '/more',
destination: '/operations', destination: '/operations',

16155
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"smoke:routes": "node ./scripts/smoke-routes.mjs", "smoke:routes": "node ./scripts/smoke-routes.mjs",
"start": "PORT=${PORT:-3000} node ./scripts/start-standalone.mjs", "start": "PORT=${PORT:-3000} node ./scripts/start-standalone.mjs",
"start:next": "next start", "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", "type-check": "tsc --noEmit -p tsconfig.check.json",
"test": "npm run lint && npm run type-check", "test": "npm run lint && npm run type-check",
"test:unit": "vitest run" "test:unit": "vitest run"
@@ -22,12 +22,12 @@
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.14.2", "@tanstack/react-query": "^5.14.2",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.6.2", "axios": "^1.15.2",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^3.0.6", "date-fns": "^3.0.6",
"js-sha3": "^0.9.3", "js-sha3": "^0.9.3",
"next": "^14.0.4", "next": "^15.5.15",
"postcss": "^8.4.32", "postcss": "^8.5.10",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.3.6",
@@ -35,11 +35,16 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.5", "@types/node": "^20.10.5",
"@types/prop-types": "^15.7.15",
"@types/react": "^18.2.45", "@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-next": "^14.0.4", "eslint-config-next": "^15.5.15",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vitest": "^1.6.1" "vitest": "^4.1.5"
},
"overrides": {
"esbuild": "^0.28.0",
"postcss": "^8.5.10"
} }
} }

View File

@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Acknowledgments | SolaceScan</title> <title>Acknowledgments | DBIS Explorer</title>
<meta name="description" content="Acknowledgments for the SolaceScan Chain 138 explorer."> <meta name="description" content="Acknowledgments for the DBIS Explorer Chain 138 explorer.">
<style> <style>
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; } 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; } .shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
@@ -19,7 +19,7 @@
<body> <body>
<div class="shell"> <div class="shell">
<div class="topbar"> <div class="topbar">
<div class="brand">SolaceScan Acknowledgments</div> <div class="brand">DBIS Explorer Acknowledgments</div>
<a href="/">Back to explorer</a> <a href="/">Back to explorer</a>
</div> </div>
<div class="card"> <div class="card">
@@ -28,10 +28,10 @@
<ul> <ul>
<li><strong>Blockscout</strong> for explorer indexing and API compatibility.</li> <li><strong>Blockscout</strong> for explorer indexing and API compatibility.</li>
<li><strong>MetaMask</strong> for wallet connectivity and Snap support.</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>ethers.js</strong> for wallet and Ethereum interaction support.</li>
<li><strong>Font Awesome</strong> for iconography.</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> </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> <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> </div>

View File

@@ -206,7 +206,7 @@ flowchart TB
subgraph CCIP_L2["Other live CCIP EVM destinations"] 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"] 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["Leaf — per-chain native DEX · cW public-network representation · partial edge pools"]
end end
subgraph ALLTRA["ALL Mainnet 651940"] subgraph ALLTRA["ALL Mainnet 651940"]
@@ -404,9 +404,9 @@ flowchart LR
<!-- 4 Cross-chain --> <!-- 4 Cross-chain -->
<div class="content" id="panel-4" role="tabpanel" aria-labelledby="tab-4" hidden> <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, Alltra round-trip, the dedicated c-to-cW mint corridors, and the orchestrated swap-bridge-swap target.</p>
<div class="mermaid-wrap"> <div class="mermaid-wrap">
<h3>CCIP — WETH primary transport</h3> <h3>CCIP — WETH primary routing lane</h3>
<div class="mermaid"> <div class="mermaid">
sequenceDiagram sequenceDiagram
participant U as User or bot participant U as User or bot

View File

@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documentation Redirect | SolaceScan</title> <title>Documentation Redirect | DBIS Explorer</title>
<meta name="description" content="Redirecting to the canonical SolaceScan documentation hub."> <meta name="description" content="Redirecting to the canonical DBIS Explorer documentation hub.">
<meta http-equiv="refresh" content="0; url=/docs"> <meta http-equiv="refresh" content="0; url=/docs">
<link rel="canonical" href="https://blockscout.defi-oracle.io/docs"> <link rel="canonical" href="https://blockscout.defi-oracle.io/docs">
<style> <style>
@@ -23,7 +23,7 @@
<body> <body>
<div class="shell"> <div class="shell">
<div class="topbar"> <div class="topbar">
<div class="brand">SolaceScan Documentation</div> <div class="brand">DBIS Explorer Documentation</div>
<a href="/">Back to explorer</a> <a href="/">Back to explorer</a>
</div> </div>
<div class="card"> <div class="card">

View File

@@ -1124,7 +1124,7 @@
} }
// Sign message // Sign message
const message = `Sign this message to authenticate with SolaceScan.\n\nNonce: ${nonceData.nonce}`; const message = `Sign this message to authenticate with DBIS Explorer.\n\nNonce: ${nonceData.nonce}`;
const signer = provider.getSigner(); const signer = provider.getSigner();
const signature = await signer.signMessage(message); const signature = await signer.signMessage(message);
@@ -4518,7 +4518,7 @@
title: 'Tools', title: 'Tools',
items: [ items: [
{ title: 'Input Data Decoder', icon: 'fa-file-code', status: 'Live', badgeClass: 'badge-info', desc: 'Open transaction detail pages to decode calldata, logs, and contract interactions already exposed by the explorer.', action: 'showTransactionsList();', href: '/transactions' }, { title: 'Input Data Decoder', icon: 'fa-file-code', status: 'Live', badgeClass: 'badge-info', desc: 'Open transaction detail pages to decode calldata, logs, and contract interactions already exposed by the explorer.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Unit Converter', icon: 'fa-scale-balanced', status: 'Live', badgeClass: 'badge-success', desc: 'Convert wei, gwei, ether, and common Chain 138 stablecoin units with a quick in-page helper.', action: 'showUnitConverterModal();', href: '/operations' }, { title: 'Unit Converter', icon: 'fa-scale-balanced', status: 'Live', badgeClass: 'badge-success', desc: 'Convert wei, gwei, ether, and common Chain 138 6-decimal GRU units with a quick in-page helper.', action: 'showUnitConverterModal();', href: '/operations' },
{ title: 'CSV Export', icon: 'fa-file-csv', status: 'Live', badgeClass: 'badge-success', desc: 'Export pool state and route inventory snapshots for operator review and downstream ingestion.', action: 'showPools(); updatePath(\'/pools\'); setTimeout(function(){ if (typeof exportPoolsCSV === \"function\") exportPoolsCSV(); }, 200);', href: '/pools' }, { title: 'CSV Export', icon: 'fa-file-csv', status: 'Live', badgeClass: 'badge-success', desc: 'Export pool state and route inventory snapshots for operator review and downstream ingestion.', action: 'showPools(); updatePath(\'/pools\'); setTimeout(function(){ if (typeof exportPoolsCSV === \"function\") exportPoolsCSV(); }, 200);', href: '/pools' },
{ title: 'Account Balance Checker', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Jump into indexed addresses to inspect balances, token inventory, internal transfers, and recent activity.', action: 'showAddresses();', href: '/addresses' } { title: 'Account Balance Checker', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Jump into indexed addresses to inspect balances, token inventory, internal transfers, and recent activity.', action: 'showAddresses();', href: '/addresses' }
] ]
@@ -4551,7 +4551,7 @@
var html = '<div style="display:grid; grid-template-columns:minmax(240px, 0.9fr) repeat(3, minmax(220px, 1fr)); gap:1rem; align-items:start;">'; var html = '<div style="display:grid; grid-template-columns:minmax(240px, 0.9fr) repeat(3, minmax(220px, 1fr)); gap:1rem; align-items:start;">';
html += '<div style="border:1px solid var(--border); border-radius:18px; padding:1.25rem; background:linear-gradient(180deg, rgba(59,130,246,0.08), rgba(15,23,42,0.02)); min-height:100%;">'; html += '<div style="border:1px solid var(--border); border-radius:18px; padding:1.25rem; background:linear-gradient(180deg, rgba(59,130,246,0.08), rgba(15,23,42,0.02)); min-height:100%;">';
html += '<div style="font-size:1.25rem; font-weight:800; margin-bottom:0.75rem;">Operations Hub</div>'; html += '<div style="font-size:1.25rem; font-weight:800; margin-bottom:0.75rem;">Operations Hub</div>';
html += '<div style="color:var(--text-light); line-height:1.7; margin-bottom:1rem;">Discover SolaceScan operational explorer tools in one place, grouped the way users expect from a polished specialist explorer.</div>'; html += '<div style="color:var(--text-light); line-height:1.7; margin-bottom:1rem;">Discover DBIS Explorer operational tools in one place, grouped the way users expect from a polished specialist explorer.</div>';
html += '<div style="display:grid; gap:0.75rem;">'; html += '<div style="display:grid; gap:0.75rem;">';
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Now live</div><div style="font-weight:700;">Route matrix, ingestion APIs, smart search, pool exports, and live Mainnet stable bridge discovery.</div></div>'; html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Now live</div><div style="font-weight:700;">Route matrix, ingestion APIs, smart search, pool exports, and live Mainnet stable bridge discovery.</div></div>';
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Good entry points</div><div style="display:flex; flex-wrap:wrap; gap:0.5rem;">'; html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Good entry points</div><div style="display:flex; flex-wrap:wrap; gap:0.5rem;">';
@@ -4602,12 +4602,12 @@
modal.innerHTML = '' + modal.innerHTML = '' +
'<div style="width:min(560px, 100%); border-radius:18px; border:1px solid var(--border); background:var(--background); box-shadow:0 24px 90px rgba(0,0,0,0.35); overflow:hidden;">' + '<div style="width:min(560px, 100%); border-radius:18px; border:1px solid var(--border); background:var(--background); box-shadow:0 24px 90px rgba(0,0,0,0.35); overflow:hidden;">' +
'<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.1rem; border-bottom:1px solid var(--border);">' + '<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.1rem; border-bottom:1px solid var(--border);">' +
'<div><div style="font-size:1.1rem; font-weight:800;">Unit Converter</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.2rem;">Wei, gwei, ether, and 6-decimal stablecoin units for Chain 138.</div></div>' + '<div><div style="font-size:1.1rem; font-weight:800;">Unit Converter</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.2rem;">Wei, gwei, ether, and 6-decimal GRU units for Chain 138.</div></div>' +
'<button type="button" class="btn btn-secondary" id="unitConverterCloseBtn"><i class="fas fa-times"></i></button>' + '<button type="button" class="btn btn-secondary" id="unitConverterCloseBtn"><i class="fas fa-times"></i></button>' +
'</div>' + '</div>' +
'<div style="padding:1rem 1.1rem; display:grid; gap:0.9rem;">' + '<div style="padding:1rem 1.1rem; display:grid; gap:0.9rem;">' +
'<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Amount</span><input id="unitConverterAmount" type="number" min="0" step="any" placeholder="1.0" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"></label>' + '<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Amount</span><input id="unitConverterAmount" type="number" min="0" step="any" placeholder="1.0" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"></label>' +
'<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Unit</span><select id="unitConverterUnit" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"><option value="ether">Ether / WETH</option><option value="gwei">Gwei</option><option value="wei">Wei</option><option value="stable">Stablecoin (6 decimals)</option></select></label>' + '<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Unit</span><select id="unitConverterUnit" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"><option value="ether">Ether / WETH</option><option value="gwei">Gwei</option><option value="wei">Wei</option><option value="stable">GRU token (6 decimals)</option></select></label>' +
'<div id="unitConverterResults" style="display:grid; gap:0.55rem;"></div>' + '<div id="unitConverterResults" style="display:grid; gap:0.55rem;"></div>' +
'</div>' + '</div>' +
'</div>'; '</div>';

View File

@@ -8,11 +8,11 @@
<meta http-equiv="Expires" content="0"> <meta http-equiv="Expires" content="0">
<!-- CSP: unsafe-eval required by ethers.js v5 UMD from CDN (uses new Function for ABI). Our code avoids eval/string setTimeout. Can be removed when moving to ethers v6 build (no UMD eval). --> <!-- CSP: unsafe-eval required by ethers.js v5 UMD from CDN (uses new Function for ABI). Our code avoids eval/string setTimeout. Can be removed when moving to ethers v6 build (no UMD eval). -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self' https://blockscout.defi-oracle.io https://explorer.d-bis.org https://rpc-http-pub.d-bis.org wss://rpc-ws-pub.d-bis.org http://192.168.11.221:8545 ws://192.168.11.221:8546;"> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self' https://blockscout.defi-oracle.io https://explorer.d-bis.org https://rpc-http-pub.d-bis.org wss://rpc-ws-pub.d-bis.org http://192.168.11.221:8545 ws://192.168.11.221:8546;">
<title>SolaceScan | Chain 138 Explorer by DBIS</title> <title>DBIS Explorer | Chain 138 Explorer by DBIS</title>
<meta name="description" content="SolaceScan - Chain 138 Explorer by DBIS. Public explorer surfaces for blocks, transactions, addresses, routes, wallet tools, and bridge monitoring."> <meta name="description" content="DBIS Explorer - Chain 138 Explorer by DBIS. Public explorer surfaces for blocks, transactions, addresses, routes, wallet tools, and bridge monitoring.">
<meta name="keywords" content="blockchain explorer, ChainID 138, DBIS, SolaceScan, bridge monitoring, wallet tools, blockchain, ethereum, blockscout"> <meta name="keywords" content="blockchain explorer, ChainID 138, DBIS, DBIS Explorer, bridge monitoring, wallet tools, blockchain, ethereum, blockscout">
<meta name="author" content="SolaceScan"> <meta name="author" content="DBIS Explorer">
<meta name="application-name" content="SolaceScan"> <meta name="application-name" content="DBIS Explorer">
<meta name="theme-color" content="#667eea"> <meta name="theme-color" content="#667eea">
<script> <script>
(function(){function t(){var e=document.getElementById('navLinks'),n=document.getElementById('navToggle'),r=document.getElementById('navToggleIcon');if(e&&n){var o=e.classList.toggle('nav-open');n.setAttribute('aria-expanded',o?'true':'false');if(r)r.className=o?'fas fa-times':'fas fa-bars';}}function c(){var e=document.getElementById('navLinks'),n=document.getElementById('navToggle'),r=document.getElementById('navToggleIcon');if(e)e.classList.remove('nav-open');if(n)n.setAttribute('aria-expanded','false');if(r)r.className='fas fa-bars';}window.toggleNavMenu=t;window.closeNavMenu=c;})(); (function(){function t(){var e=document.getElementById('navLinks'),n=document.getElementById('navToggle'),r=document.getElementById('navToggleIcon');if(e&&n){var o=e.classList.toggle('nav-open');n.setAttribute('aria-expanded',o?'true':'false');if(r)r.className=o?'fas fa-times':'fas fa-bars';}}function c(){var e=document.getElementById('navLinks'),n=document.getElementById('navToggle'),r=document.getElementById('navToggleIcon');if(e)e.classList.remove('nav-open');if(n)n.setAttribute('aria-expanded','false');if(r)r.className='fas fa-bars';}window.toggleNavMenu=t;window.closeNavMenu=c;})();
@@ -20,15 +20,15 @@
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="https://blockscout.defi-oracle.io/"> <meta property="og:url" content="https://blockscout.defi-oracle.io/">
<meta property="og:title" content="SolaceScan - Chain 138 Explorer by DBIS"> <meta property="og:title" content="DBIS Explorer - Chain 138 Explorer by DBIS">
<meta property="og:description" content="Comprehensive blockchain explorer for ChainID 138 with cross-chain bridge monitoring and WETH utilities."> <meta property="og:description" content="Comprehensive blockchain explorer for ChainID 138 with cross-chain bridge monitoring and WETH utilities.">
<meta property="og:image" content="https://blockscout.defi-oracle.io/og-image.png"> <meta property="og:image" content="https://blockscout.defi-oracle.io/og-image.png">
<meta property="og:site_name" content="SolaceScan"> <meta property="og:site_name" content="DBIS Explorer">
<!-- Twitter --> <!-- Twitter -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="https://blockscout.defi-oracle.io/"> <meta name="twitter:url" content="https://blockscout.defi-oracle.io/">
<meta name="twitter:title" content="SolaceScan - Chain 138 Explorer by DBIS"> <meta name="twitter:title" content="DBIS Explorer - Chain 138 Explorer by DBIS">
<meta name="twitter:description" content="Comprehensive blockchain explorer for ChainID 138 with cross-chain bridge monitoring and WETH utilities."> <meta name="twitter:description" content="Comprehensive blockchain explorer for ChainID 138 with cross-chain bridge monitoring and WETH utilities.">
<meta name="twitter:image" content="https://blockscout.defi-oracle.io/og-image.png"> <meta name="twitter:image" content="https://blockscout.defi-oracle.io/og-image.png">
@@ -1214,7 +1214,7 @@
<a class="logo" href="/" aria-label="Go to explorer home" style="text-decoration:none; color:inherit;"> <a class="logo" href="/" aria-label="Go to explorer home" style="text-decoration:none; color:inherit;">
<i class="fas fa-cube"></i> <i class="fas fa-cube"></i>
<div style="display: flex; flex-direction: column; gap: 0.25rem;"> <div style="display: flex; flex-direction: column; gap: 0.25rem;">
<span>SolaceScan</span> <span>DBIS Explorer</span>
<span style="font-size: 0.75rem; font-weight: normal; opacity: 0.9;">Chain 138 Explorer by DBIS</span> <span style="font-size: 0.75rem; font-weight: normal; opacity: 0.9;">Chain 138 Explorer by DBIS</span>
</div> </div>
</a> </a>
@@ -1784,14 +1784,14 @@
<div class="container"> <div class="container">
<div class="site-footer-grid"> <div class="site-footer-grid">
<div> <div>
<div style="font-size: 1.05rem; font-weight: 700; margin-bottom: 0.5rem;">SolaceScan</div> <div style="font-size: 1.05rem; font-weight: 700; margin-bottom: 0.5rem;">DBIS Explorer</div>
<div class="site-footer-note"> <div class="site-footer-note">
Built on Blockscout foundations for the DBIS / Defi Oracle Chain 138 explorer surface. Built on Blockscout foundations for the DBIS Chain 138 explorer surface.
Explorer data, block indexing, and public chain visibility are powered by Blockscout, Explorer data, block indexing, and public chain visibility are powered by Blockscout,
Chain 138 RPC, and the MetaMask Snap companion. Chain 138 RPC, and the MetaMask Snap companion.
</div> </div>
<div class="site-footer-note" style="margin-top: 0.8rem;"> <div class="site-footer-note" style="margin-top: 0.8rem;">
© 2026 DBIS / Defi Oracle. All rights reserved. © 2026 DBIS. All rights reserved.
</div> </div>
</div> </div>
<div> <div>

View File

@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy | SolaceScan</title> <title>Privacy Policy | DBIS Explorer</title>
<meta name="description" content="Privacy policy for the SolaceScan Chain 138 explorer operated by DBIS / Defi Oracle."> <meta name="description" content="Privacy policy for the DBIS Explorer Chain 138 explorer operated by DBIS.">
<style> <style>
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; } 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; } .shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
@@ -19,13 +19,13 @@
<body> <body>
<div class="shell"> <div class="shell">
<div class="topbar"> <div class="topbar">
<div class="brand">SolaceScan Privacy Policy</div> <div class="brand">DBIS Explorer Privacy Policy</div>
<a href="/">Back to explorer</a> <a href="/">Back to explorer</a>
</div> </div>
<div class="card"> <div class="card">
<h1 style="margin-top:0;">Privacy Policy</h1> <h1 style="margin-top:0;">Privacy Policy</h1>
<p class="muted">Last updated: 2026-03-25</p> <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> <h2>What we store locally</h2>
<ul> <ul>
<li>We may store theme preference, locale, recent searches, and similar local UI settings in your browser.</li> <li>We may store theme preference, locale, recent searches, and similar local UI settings in your browser.</li>

View File

@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terms of Service | SolaceScan</title> <title>Terms of Service | DBIS Explorer</title>
<meta name="description" content="Terms of service for the SolaceScan Chain 138 explorer operated by DBIS / Defi Oracle."> <meta name="description" content="Terms of service for the DBIS Explorer Chain 138 explorer operated by DBIS.">
<style> <style>
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; } 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; } .shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
@@ -19,13 +19,13 @@
<body> <body>
<div class="shell"> <div class="shell">
<div class="topbar"> <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> <a href="/">Back to explorer</a>
</div> </div>
<div class="card"> <div class="card">
<h1 style="margin-top:0;">Terms of Service</h1> <h1 style="margin-top:0;">Terms of Service</h1>
<p class="muted">Last updated: 2026-03-25</p> <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> <h2>Service scope</h2>
<ul> <ul>
<li>Blockchain data may be delayed, incomplete, or temporarily unavailable.</li> <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> <li>Bridge, route, liquidity, and operational surfaces are investigative and informational unless a page explicitly presents an authenticated management workflow.</li>
</ul> </ul>
<h2>Operator identity</h2> <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> <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> <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> <h2>Disputes and interpretation</h2>

View File

@@ -4,7 +4,7 @@ const baseUrl = (process.env.BASE_URL || 'https://explorer.d-bis.org').replace(/
const addressUnderTest = process.env.SMOKE_ADDRESS || '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506' const addressUnderTest = process.env.SMOKE_ADDRESS || '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506'
const checks = [ const checks = [
{ path: '/', expectTexts: ['SolaceScan', 'Recent Blocks', 'Open wallet tools'] }, { path: '/', expectTexts: ['DBIS Explorer', 'Recent Blocks', 'Open wallet tools'] },
{ path: '/blocks', expectTexts: ['Blocks'] }, { path: '/blocks', expectTexts: ['Blocks'] },
{ path: '/transactions', expectTexts: ['Transactions'] }, { path: '/transactions', expectTexts: ['Transactions'] },
{ path: '/addresses', expectTexts: ['Addresses', 'Open An Address'] }, { path: '/addresses', expectTexts: ['Addresses', 'Open An Address'] },

View File

@@ -7,7 +7,20 @@ import process from 'node:process'
const projectRoot = process.cwd() const projectRoot = process.cwd()
const standaloneRoot = path.join(projectRoot, '.next', 'standalone') const standaloneRoot = path.join(projectRoot, '.next', 'standalone')
const standaloneNextRoot = path.join(standaloneRoot, '.next') 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) { async function copyIfPresent(sourcePath, destinationPath) {
if (!existsSync(sourcePath)) { if (!existsSync(sourcePath)) {
@@ -19,15 +32,16 @@ async function copyIfPresent(sourcePath, destinationPath) {
} }
async function main() { 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.') console.error('Standalone server build is missing. Run `npm run build` first.')
process.exit(1) process.exit(1)
} }
await copyIfPresent(path.join(projectRoot, '.next', 'static'), path.join(standaloneNextRoot, 'static')) 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', stdio: 'inherit',
env: process.env, env: process.env,
}) })

View File

@@ -11,7 +11,7 @@ export default function BrandLockup({ compact = false }: { compact?: boolean })
compact ? 'text-[1.45rem]' : 'text-[1.65rem]', compact ? 'text-[1.45rem]' : 'text-[1.65rem]',
].join(' ')} ].join(' ')}
> >
SolaceScan DBIS Explorer
</span> </span>
<span <span
className={[ className={[

View File

@@ -15,18 +15,29 @@ function toneClasses(tone: 'neutral' | 'success' | 'warning' | 'info') {
export function getEntityBadgeTone(tag: string): 'neutral' | 'success' | 'warning' | 'info' { export function getEntityBadgeTone(tag: string): 'neutral' | 'success' | 'warning' | 'info' {
const normalized = tag.toLowerCase() const normalized = tag.toLowerCase()
if (normalized === 'compliant' || normalized === 'listed' || normalized === 'verified') { if (normalized === 'compliant' || normalized === 'listed' || normalized === 'verified' || normalized === 'gru') {
return 'success' return 'success'
} }
if (normalized === 'wrapped') { if (normalized === 'wrapped' || normalized === 'treasury-bond') {
return 'warning' return 'warning'
} }
if (normalized === 'bridge' || normalized === 'canonical' || normalized === 'official') { if (normalized === 'bridge' || normalized === 'canonical' || normalized === 'official' || normalized === 'electronic-money' || normalized === 'commodity') {
return 'info' return 'info'
} }
return 'neutral' return 'neutral'
} }
export function formatEntityBadgeLabel(label: string): string {
const normalized = label.toLowerCase()
const labels: Record<string, string> = {
'reference-asset': 'reference asset',
'electronic-money': 'cash e-money',
'treasury-bond': 'treasury / gov bond',
gru: 'GRU',
}
return labels[normalized] || label
}
export default function EntityBadge({ export default function EntityBadge({
label, label,
tone, tone,
@@ -46,7 +57,7 @@ export default function EntityBadge({
className, className,
)} )}
> >
{label} {formatEntityBadgeLabel(label)}
</span> </span>
) )
} }

View File

@@ -25,7 +25,7 @@ export default function ExplorerAgentTool() {
{ {
role: 'assistant', role: 'assistant',
content: 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"> <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 className="flex items-start justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700">
<div> <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"> <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. Page-aware guidance for the explorer. Helpful, read-only, and designed for quick investigation.
</p> </p>
@@ -163,15 +163,16 @@ export default function ExplorerAgentTool() {
<button <button
type="button" type="button"
onClick={() => setOpen((value) => !value)} 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-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"> <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> <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" /> <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> </svg>
</span> </span>
Agent Tool <span className="hidden lg:inline">AI Assist</span>
</button> </button>
</div> </div>
) )

View File

@@ -12,18 +12,18 @@ export default function Footer() {
<div className="grid gap-4 sm:gap-6 md:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,1fr)]"> <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="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"> <div className="text-base font-semibold text-gray-900 dark:text-white sm:text-lg">
SolaceScan DBIS Explorer
</div> </div>
<p className="max-w-xl text-sm leading-6 text-gray-600 dark:text-gray-400"> <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. Built on Blockscout for the DBIS Chain 138 explorer surface.
Explorer data is powered by Blockscout, Chain 138 RPC, and the companion MetaMask Snap. Explorer data is powered by Blockscout, Chain 138 RPC, and the companion MetaMask Snap.
</p> </p>
<p className="max-w-xl text-xs leading-5 text-gray-500 dark:text-gray-500"> <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>. Primary public explorer access is served at <code>explorer.d-bis.org</code>.
Both domains belong to the same DBIS / Defi Oracle explorer surface. <code> blockscout.defi-oracle.io</code> is the Blockscout companion domain for the same Chain 138 explorer surface.
</p> </p>
<p className="text-xs text-gray-500 dark:text-gray-500"> <p className="text-xs text-gray-500 dark:text-gray-500">
© {year} DBIS / Defi Oracle. All rights reserved. © {year} DBIS. All rights reserved.
</p> </p>
</div> </div>

View File

@@ -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-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-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.', '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.', DeterministicStorageNamespace: 'Stable namespace for upgrade-aware policy, registry, and audit resolution.',
JurisdictionAndSupervisionMetadata: 'Governance, supervisory, disclosure, and reporting metadata required by the GRU operating model.', 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 { function formatDuration(seconds: number | null): string | null {
if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) return null if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) return null
const units = [ 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.` ? `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.`, : `The live contract exposes the full required GRU v2 base-token surface currently checked by the explorer.`,
profile.wrappedTransport 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 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 transport activation beyond the token interface itself.', : '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 profile.x402Ready
? 'This contract appears ready for x402-style payment flows because the explorer can see the required signature and domain surfaces.' ? '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.', : '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 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 : 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.' ? '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 transport overlay and deployment records before making promotion assumptions.', : '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 profile.legacyAliasSupport
? 'Legacy alias support is exposed, which is useful during version cutovers and explorer/search reconciliation.' ? '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.', : '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"> <DetailRow label="Profile">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-wrap gap-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 <EntityBadge
label={profile.wrappedTransport ? 'wrapped transport' : 'canonical GRU'} label={profile.wrappedTransport ? 'cW public-network' : 'canonical GRU'}
tone={profile.wrappedTransport ? 'warning' : 'success'} tone={profile.wrappedTransport ? 'warning' : 'success'}
/> />
</div> </div>
@@ -94,14 +109,14 @@ export default function GruStandardsCard({
{profile.standards.map((standard) => ( {profile.standards.map((standard) => (
<EntityBadge <EntityBadge
key={standard.id} 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'} tone={standard.detected ? 'success' : 'warning'}
className="normal-case tracking-normal" className="normal-case tracking-normal"
/> />
))} ))}
</DetailRow> </DetailRow>
<DetailRow label="Transport Posture"> <DetailRow label="Bridge Posture">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<EntityBadge <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="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"> <div className="mt-2 text-gray-900 dark:text-white">
{profile.wrappedTransport {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 a cW public-network representation instead of the canonical Chain 138 GRU surface.'
: 'This contract presents itself like the canonical Chain 138 GRU money surface instead of a wrapped transport mirror.'} : 'This contract presents itself like the canonical Chain 138 GRU surface instead of a cW public-network representation.'}
</div> </div>
</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"> <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="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"> <div className="mt-2 text-gray-900 dark:text-white">
{profile.activeVersion || profile.forwardVersion {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.'} : 'No explicit active-versus-forward version posture is available from the local GRU catalog yet.'}
</div> </div>
</div> </div>
@@ -163,7 +178,7 @@ export default function GruStandardsCard({
{profile.standards.map((standard) => ( {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 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="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'} /> <EntityBadge label={standard.detected ? 'detected' : 'missing'} tone={standard.detected ? 'success' : 'warning'} />
</div> </div>
<div className="mt-2 text-gray-600 dark:text-gray-400"> <div className="mt-2 text-gray-600 dark:text-gray-400">
@@ -190,12 +205,12 @@ export default function GruStandardsCard({
<DetailRow label="References"> <DetailRow label="References">
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400"> <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><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>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>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>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">config/gru-standards-profile.json</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>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>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">docs/04-configuration/CHAIN138_X402_TOKEN_SUPPORT.md</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">docs/04-configuration/GRU_V2_CHAIN138_READINESS.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">GRU v2 Chain 138 readiness</code></div>
</div> </div>
</DetailRow> </DetailRow>

View File

@@ -0,0 +1,37 @@
import { formatRelativeAge, 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
}) {
const freshness = lastUpdated ? `${formatRelativeAge(lastUpdated)} (${formatTimestamp(lastUpdated)})` : 'timestamp unavailable'
const text = `Source: ${formatSource(source)}. Updated: ${freshness}. Method: ${method}`
return (
<p className={`${compact ? 'mt-1' : 'mt-3'} text-xs leading-5 text-gray-500 dark:text-gray-400`}>
{text}
</p>
)
}

View File

@@ -704,7 +704,7 @@ export default function Navbar() {
href="/" 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 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"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
aria-label="Go to SolaceScan home" aria-label="Go to DBIS Explorer home"
> >
<BrandLockup /> <BrandLockup />
</Link> </Link>

View File

@@ -0,0 +1,357 @@
'use client'
import { FormEvent, useMemo, useState } from 'react'
import clsx from 'clsx'
import { Card } from '@/libs/frontend-ui-primitives'
import EntityBadge from '@/components/common/EntityBadge'
import { getExplorerApiBase } from '@/services/api/blockscout'
import type { ContractProfile, ContractSourceFile } from '@/services/api/contracts'
interface ContractCodeWorkspaceProps {
address: string
profile: ContractProfile
}
interface OutlineEntry {
type: 'contract' | 'interface' | 'library' | 'function' | 'event' | 'error'
name: string
line: number
}
const QUICK_PROMPTS = [
'What does this contract do?',
'What are the functions available in this contract?',
'Which functions can change state or move funds?',
'Who has special permissions or control in this contract?',
'What are potential risks or red flags in this contract?',
] as const
function makeFallbackSourceFile(profile: ContractProfile): ContractSourceFile | null {
if (!profile.source_code_preview && !profile.abi_full && !profile.abi) return null
return {
path: profile.contract_name ? `${profile.contract_name}.sol` : 'Contract.sol',
content: profile.source_code_full || profile.source_code_preview || profile.abi_full || profile.abi || '',
}
}
function parseOutline(content: string): OutlineEntry[] {
const entries: OutlineEntry[] = []
content.split('\n').forEach((line, index) => {
const lineNumber = index + 1
const typeMatch = line.match(/^\s*(?:abstract\s+)?(contract|interface|library)\s+([A-Za-z_][A-Za-z0-9_]*)/)
if (typeMatch) {
entries.push({
type: typeMatch[1] as OutlineEntry['type'],
name: typeMatch[2],
line: lineNumber,
})
return
}
const memberMatch = line.match(/^\s*(function|event|error)\s+([A-Za-z_][A-Za-z0-9_]*)/)
if (memberMatch) {
entries.push({
type: memberMatch[1] as OutlineEntry['type'],
name: memberMatch[2],
line: lineNumber,
})
}
})
return entries
}
function sourceExcerptForPrompt(files: ContractSourceFile[]): string {
return files
.slice(0, 4)
.map((file) => `File: ${file.path}\n${file.content.slice(0, 2600)}`)
.join('\n\n')
.slice(0, 5200)
}
export default function ContractCodeWorkspace({ address, profile }: ContractCodeWorkspaceProps) {
const files = useMemo(() => {
const normalized = profile.source_files?.length ? profile.source_files : []
const fallback = makeFallbackSourceFile(profile)
return normalized.length > 0 ? normalized : fallback ? [fallback] : []
}, [profile])
const [activeTab, setActiveTab] = useState<'source' | 'reader'>('source')
const [activePath, setActivePath] = useState(files[0]?.path || '')
const [prompt, setPrompt] = useState('What does this contract do?')
const [model, setModel] = useState('Explorer AI')
const [saveHistory, setSaveHistory] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [readerAnswer, setReaderAnswer] = useState('')
const [readerError, setReaderError] = useState('')
const [expanded, setExpanded] = useState(false)
const activeFile = files.find((file) => file.path === activePath) || files[0]
const outline = useMemo(() => parseOutline(activeFile?.content || ''), [activeFile?.content])
const sourceLines = useMemo(() => (activeFile?.content || '').split('\n'), [activeFile?.content])
const selectedFiles = files
const sourceAvailable = files.length > 0 && Boolean(activeFile?.content)
const handleCopySource = async () => {
if (!activeFile?.content || typeof navigator === 'undefined') return
await navigator.clipboard?.writeText(activeFile.content)
}
const handleCopyLink = async () => {
if (typeof navigator === 'undefined' || typeof window === 'undefined') return
await navigator.clipboard?.writeText(`${window.location.href.split('#')[0]}#contract-source`)
}
const askReader = async (question: string) => {
const trimmed = question.trim()
if (!trimmed || submitting) return
setPrompt(trimmed)
setReaderError('')
setReaderAnswer('')
setSubmitting(true)
try {
const context = [
`Contract address: ${address}`,
profile.contract_name ? `Contract name: ${profile.contract_name}` : '',
profile.compiler_version ? `Compiler: ${profile.compiler_version}` : '',
profile.license_type ? `License: ${profile.license_type}` : '',
profile.proxy_type ? `Proxy type: ${profile.proxy_type}` : '',
`Read methods: ${profile.read_methods.map((method) => method.signature).slice(0, 24).join(', ') || 'none reported'}`,
`Write methods: ${profile.write_methods.map((method) => method.signature).slice(0, 24).join(', ') || 'none reported'}`,
sourceAvailable ? `Verified source excerpts:\n${sourceExcerptForPrompt(selectedFiles)}` : 'Verified source text is not available.',
].filter(Boolean).join('\n')
const response = await fetch(`${getExplorerApiBase()}/api/v1/ai/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [
{
role: 'user',
content: `${trimmed}\n\nUse this contract context and answer concisely. Do not invent behavior that is not supported by the ABI or source.\n\n${context}`,
},
],
pageContext: {
path: `/addresses/${address}`,
view: 'contract-code-reader',
address,
},
}),
})
const payload = await response.json().catch(() => null)
if (!response.ok) {
throw new Error(payload?.error?.message || `AI reader returned HTTP ${response.status}`)
}
setReaderAnswer(String(payload?.reply || payload?.message?.content || 'No answer returned.'))
} catch (error) {
setReaderError(error instanceof Error ? error.message : 'Code Reader is temporarily unavailable.')
} finally {
setSubmitting(false)
}
}
const handleSubmit = async (event: FormEvent) => {
event.preventDefault()
await askReader(prompt)
}
if (!sourceAvailable && !profile.abi_available) {
return null
}
return (
<Card className="mb-6" title="Contract Source Code">
<section id="contract-source" className="space-y-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setActiveTab('source')}
className={clsx(
'rounded-lg px-3 py-2 text-sm font-semibold transition',
activeTab === 'source'
? 'bg-primary-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700',
)}
>
Source
</button>
<button
type="button"
onClick={() => setActiveTab('reader')}
className={clsx(
'rounded-lg px-3 py-2 text-sm font-semibold transition',
activeTab === 'reader'
? 'bg-primary-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700',
)}
>
Code Reader
</button>
</div>
<div className="flex flex-wrap gap-2">
{profile.source_verified ? <EntityBadge label="verified source" tone="success" /> : null}
{profile.abi_available ? <EntityBadge label="abi available" tone="info" /> : null}
{profile.compiler_version ? <EntityBadge label={profile.compiler_version} tone="neutral" className="normal-case tracking-normal" /> : null}
</div>
</div>
{activeTab === 'source' ? (
<div className={clsx('overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700', expanded ? 'min-h-[46rem]' : '')}>
<div className="grid lg:grid-cols-[18rem_minmax(0,1fr)]">
<aside className="border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900 lg:border-b-0 lg:border-r">
<div className="flex items-center justify-between border-b border-gray-200 px-4 py-3 text-xs font-semibold uppercase text-gray-500 dark:border-gray-700 dark:text-gray-400">
<span>Explorer</span>
<span>{files.length} file{files.length === 1 ? '' : 's'}</span>
</div>
<div className="max-h-72 overflow-auto p-2 lg:max-h-[34rem]">
{files.map((file) => (
<button
type="button"
key={file.path}
onClick={() => setActivePath(file.path)}
className={clsx(
'block w-full rounded-md px-3 py-2 text-left text-sm transition',
file.path === activeFile?.path
? 'bg-white font-semibold text-gray-950 shadow-sm dark:bg-gray-800 dark:text-white'
: 'text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-800',
)}
>
<span className="block truncate">{file.path}</span>
</button>
))}
</div>
{outline.length > 0 ? (
<div className="border-t border-gray-200 p-2 dark:border-gray-700">
<div className="px-3 py-2 text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Outline</div>
<div className="max-h-64 overflow-auto">
{outline.slice(0, 80).map((entry) => (
<button
key={`${entry.type}-${entry.name}-${entry.line}`}
type="button"
onClick={() => document.getElementById(`source-line-${entry.line}`)?.scrollIntoView({ block: 'center' })}
className="flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-xs text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-800"
>
<span className="w-16 uppercase text-gray-400">{entry.type}</span>
<span className="min-w-0 flex-1 truncate font-mono">{entry.name}</span>
<span className="text-gray-400">{entry.line}</span>
</button>
))}
</div>
</div>
) : null}
</aside>
<div className="min-w-0 bg-gray-950 text-gray-100">
<div className="flex flex-col gap-3 border-b border-gray-800 bg-gray-900 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<div className="truncate font-mono text-sm text-white">{activeFile?.path || 'Source'}</div>
<div className="mt-1 text-xs text-gray-400">{sourceLines.length} lines</div>
</div>
<div className="flex flex-wrap gap-2">
<button type="button" onClick={handleCopySource} className="rounded-md border border-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-200 hover:bg-gray-800">
Copy
</button>
<button type="button" onClick={handleCopyLink} className="rounded-md border border-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-200 hover:bg-gray-800">
Link
</button>
<button type="button" onClick={() => setExpanded((value) => !value)} className="rounded-md border border-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-200 hover:bg-gray-800">
{expanded ? 'Collapse' : 'Expand'}
</button>
</div>
</div>
<pre className={clsx('overflow-auto p-0 text-xs leading-5', expanded ? 'max-h-[52rem]' : 'max-h-[34rem]')}>
<code className="block min-w-max py-4">
{sourceLines.map((line, index) => (
<span id={`source-line-${index + 1}`} key={`${activeFile?.path}-${index}`} className="grid grid-cols-[4.5rem_minmax(0,1fr)] px-4 hover:bg-white/5">
<span className="select-none pr-4 text-right text-gray-500">{index + 1}</span>
<span className="whitespace-pre text-gray-100">{line || ' '}</span>
</span>
))}
</code>
</pre>
</div>
</div>
</div>
) : (
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-700">
<form onSubmit={handleSubmit} className="grid gap-5 lg:grid-cols-[28rem_minmax(0,1fr)]">
<div className="space-y-4 lg:border-r lg:border-gray-200 lg:pr-5 lg:dark:border-gray-700">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-gray-900 dark:text-white">Choose Model</span>
<select
value={model}
onChange={(event) => setModel(event.target.value)}
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-950 dark:text-white"
>
<option>Explorer AI</option>
<option>Grok</option>
</select>
</label>
<div>
<div className="mb-2 text-sm font-semibold text-gray-900 dark:text-white">File Browser</div>
<div className="space-y-2 rounded-lg bg-gray-50 p-3 dark:bg-gray-900">
{files.map((file) => (
<label key={file.path} className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200">
<input type="checkbox" checked readOnly className="h-4 w-4 rounded border-gray-300 text-primary-600" />
<span className="truncate">{file.path}</span>
</label>
))}
</div>
</div>
</div>
<div className="min-w-0 space-y-4">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-semibold text-gray-900 dark:text-white">Prompt</div>
<label className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300">
<input type="checkbox" checked={saveHistory} onChange={(event) => setSaveHistory(event.target.checked)} className="h-4 w-4 rounded border-gray-300 text-primary-600" />
Save History
</label>
</div>
<div className="flex gap-3">
<textarea
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
rows={3}
className="min-h-24 flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-950 dark:text-white"
/>
<button
type="submit"
disabled={submitting || !prompt.trim()}
className="h-12 rounded-lg bg-primary-600 px-4 text-sm font-semibold text-white transition hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{submitting ? '...' : 'Send'}
</button>
</div>
<div className="flex flex-wrap gap-2">
{QUICK_PROMPTS.map((quickPrompt) => (
<button
key={quickPrompt}
type="button"
onClick={() => void askReader(quickPrompt)}
className="rounded-full border border-gray-300 px-3 py-1.5 text-xs font-semibold text-gray-700 hover:border-primary-400 hover:text-primary-700 dark:border-gray-700 dark:text-gray-300 dark:hover:text-primary-300"
>
{quickPrompt}
</button>
))}
</div>
{readerAnswer ? (
<div className="whitespace-pre-wrap rounded-lg bg-gray-50 p-4 text-sm text-gray-800 dark:bg-gray-900 dark:text-gray-100">
{readerAnswer}
</div>
) : null}
{readerError ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200">
{readerError}
</div>
) : null}
</div>
</form>
</div>
)}
</section>
</Card>
)
}

View File

@@ -20,6 +20,7 @@ import { statsApi, type ExplorerStats } from '@/services/api/stats'
import { summarizeChainActivity } from '@/utils/activityContext' import { summarizeChainActivity } from '@/utils/activityContext'
import ActivityContextPanel from '@/components/common/ActivityContextPanel' import ActivityContextPanel from '@/components/common/ActivityContextPanel'
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote' import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel' import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness' import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
import { import {
@@ -318,6 +319,12 @@ export default function LiquidityOperationsPage({
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400"> <div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{formatNumber(dexCount)} DEX families in the current discovered pools. {formatNumber(dexCount)} DEX families in the current discovered pools.
</div> </div>
<MarketEvidenceNote
source="mission-control"
lastUpdated={routeMatrix?.updated}
method="Route matrix, provider capabilities, and mission-control pool inventory are reconciled for visible public liquidity only."
compact
/>
</Card> </Card>
<Card> <Card>
<div className="text-sm text-gray-500 dark:text-gray-400">Fallback posture</div> <div className="text-sm text-gray-500 dark:text-gray-400">Fallback posture</div>
@@ -354,6 +361,12 @@ export default function LiquidityOperationsPage({
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400"> <div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Seen from {pool.sourceSymbols.join(', ')} Seen from {pool.sourceSymbols.join(', ')}
</div> </div>
<MarketEvidenceNote
source="mission-control"
lastUpdated={routeMatrix?.updated}
method="Pool TVL is the visible mission-control value for discovered route-backed liquidity."
compact
/>
</div> </div>
))} ))}
{aggregatedPools.length === 0 ? ( {aggregatedPools.length === 0 ? (

View File

@@ -105,7 +105,7 @@ export default function WethOperationsPage({
<OperationsPageShell page={page}> <OperationsPageShell page={page}>
<Card className="mb-6 border border-amber-200 bg-amber-50/70 dark:border-amber-900/50 dark:bg-amber-950/20"> <Card className="mb-6 border border-amber-200 bg-amber-50/70 dark:border-amber-900/50 dark:bg-amber-950/20">
<p className="text-sm leading-6 text-amber-950 dark:text-amber-100"> <p className="text-sm leading-6 text-amber-950 dark:text-amber-100">
These WETH references are bridge and transport surfaces, not a claim that Ethereum mainnet WETH contracts are native Chain 138 assets. These WETH references are bridge-lane and public-network representation surfaces, not a claim that Ethereum mainnet WETH contracts are native Chain 138 assets.
Use this page to review wrapped-asset lane posture, counterpart contracts, and operational dependencies. Use this page to review wrapped-asset lane posture, counterpart contracts, and operational dependencies.
</p> </p>
</Card> </Card>

View File

@@ -21,8 +21,10 @@ import { transactionsApi, type Transaction } from '@/services/api/transactions'
import { summarizeChainActivity } from '@/utils/activityContext' import { summarizeChainActivity } from '@/utils/activityContext'
import ActivityContextPanel from '@/components/common/ActivityContextPanel' import ActivityContextPanel from '@/components/common/ActivityContextPanel'
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote' import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
import { Explain, useUiMode } from '@/components/common/UiModeContext' import { Explain, useUiMode } from '@/components/common/UiModeContext'
import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness' import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness'
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
type HomeStats = ExplorerStats type HomeStats = ExplorerStats
@@ -92,6 +94,15 @@ function compactStatNote(guided: string, expert: string, mode: 'guided' | 'exper
return mode === 'guided' ? guided : expert return mode === 'guided' ? guided : expert
} }
function formatUsd(value: number | undefined) {
if (value == null || !Number.isFinite(value)) return 'Unavailable'
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: value >= 100 ? 0 : 2,
}).format(value)
}
export default function Home({ export default function Home({
initialStats = null, initialStats = null,
initialRecentBlocks = [], initialRecentBlocks = [],
@@ -109,6 +120,7 @@ export default function Home({
const [activitySnapshot, setActivitySnapshot] = useState<ExplorerRecentActivitySnapshot | null>(initialActivitySnapshot) const [activitySnapshot, setActivitySnapshot] = useState<ExplorerRecentActivitySnapshot | null>(initialActivitySnapshot)
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus) const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
const [relaySummary, setRelaySummary] = useState<MissionControlRelaySummary | null>(initialRelaySummary) const [relaySummary, setRelaySummary] = useState<MissionControlRelaySummary | null>(initialRelaySummary)
const [featuredPrices, setFeaturedPrices] = useState<TokenAggregationTokenSnapshot[]>([])
const [missionExpanded, setMissionExpanded] = useState(false) const [missionExpanded, setMissionExpanded] = useState(false)
const [relayExpanded, setRelayExpanded] = useState(false) const [relayExpanded, setRelayExpanded] = useState(false)
const [relayPage, setRelayPage] = useState(1) const [relayPage, setRelayPage] = useState(1)
@@ -166,6 +178,29 @@ export default function Home({
} }
}, []) }, [])
useEffect(() => {
let cancelled = false
tokenAggregationApi.getTokensByAddressSafe(138, [
'0x93E66202A11B1772E55407B32B44e5Cd8eda7f22',
'0xf22258f57794CC8E06237084b353Ab30fFfa640b',
'0x290e52a8819A4fBd0714e517225429AA2B70EC6B',
'0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
]).then(({ data }) => {
if (!cancelled) {
setFeaturedPrices(data)
}
}).catch((error) => {
if (!cancelled && process.env.NODE_ENV !== 'production') {
console.warn('Failed to load featured token prices:', error)
}
})
return () => {
cancelled = true
}
}, [])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@@ -575,7 +610,7 @@ export default function Home({
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10"> <div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Chain 138 Status</div> <div className="text-xs font-semibold uppercase tracking-wide opacity-70">Chain 138 Status</div>
<div className="mt-2 text-lg font-semibold">{chainStatus.status || 'unknown'}</div> <div className="mt-2 text-lg font-semibold">{chainStatus.status || 'unknown'}</div>
<div className="mt-1 text-sm opacity-80">{chainStatus.name || 'Defi Oracle Meta Mainnet'}</div> <div className="mt-1 text-sm opacity-80">{chainStatus.name || 'DeFi Oracle Meta Mainnet'}</div>
</div> </div>
<div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10"> <div className="rounded-2xl border border-white/40 bg-white/55 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-black/10">
<div className="text-xs font-semibold uppercase tracking-wide opacity-70">Head Age</div> <div className="text-xs font-semibold uppercase tracking-wide opacity-70">Head Age</div>
@@ -738,6 +773,36 @@ export default function Home({
</div> </div>
)} )}
{featuredPrices.length > 0 ? (
<div className="mb-8">
<Card title="Live Price Feed">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{featuredPrices.map((token) => (
<Link
key={token.address}
href={`/tokens/${token.address}`}
className="rounded-2xl border border-gray-200 bg-gray-50/70 px-4 py-3 transition hover:border-primary-400 hover:shadow-sm dark:border-gray-800 dark:bg-gray-900/40"
>
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{token.symbol || token.name || 'Token'}
</div>
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
{formatUsd(token.market?.priceUsd)}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Visible liquidity: {formatUsd(token.market?.liquidityUsd)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{token.market?.lastUpdated ? `Updated ${formatRelativeAge(token.market.lastUpdated)}` : 'Update time unavailable'}
</div>
<MarketEvidenceNote lastUpdated={token.market?.lastUpdated} compact />
</Link>
))}
</div>
</Card>
</div>
) : null}
<div className="mb-8"> <div className="mb-8">
<ActivityContextPanel <ActivityContextPanel
context={activityContext} context={activityContext}

View File

@@ -148,7 +148,7 @@ const FALLBACK_CAPABILITIES_138: CapabilitiesCatalog = {
name: 'Chain 138 RPC Capabilities', name: 'Chain 138 RPC Capabilities',
version: { major: 1, minor: 1, patch: 0 }, version: { major: 1, minor: 1, patch: 0 },
timestamp: '2026-03-28T00:00:00Z', timestamp: '2026-03-28T00:00:00Z',
generatedBy: 'SolaceScan', generatedBy: 'DBIS Explorer',
chainId: 138, chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet', chainName: 'DeFi Oracle Meta Mainnet',
rpcUrl: 'https://rpc-http-pub.d-bis.org', rpcUrl: 'https://rpc-http-pub.d-bis.org',

View File

@@ -29,13 +29,27 @@ import {
} from '@/utils/watchlist' } from '@/utils/watchlist'
import PageIntro from '@/components/common/PageIntro' import PageIntro from '@/components/common/PageIntro'
import GruStandardsCard from '@/components/common/GruStandardsCard' import GruStandardsCard from '@/components/common/GruStandardsCard'
import ContractCodeWorkspace from '@/components/explorer/ContractCodeWorkspace'
import { getGruStandardsProfileSafe, type GruStandardsProfile } from '@/services/api/gru' import { getGruStandardsProfileSafe, type GruStandardsProfile } from '@/services/api/gru'
import { getGruExplorerMetadata } from '@/services/api/gruExplorerData' import { getGruExplorerMetadata } from '@/services/api/gruExplorerData'
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
import { estimateNativeUsdValue, getNativeAssetDescriptor, getNativeAssetMarketSafe } from '@/services/api/nativeAssetPricing'
function isValidAddress(value: string) { function isValidAddress(value: string) {
return /^0x[a-fA-F0-9]{40}$/.test(value) return /^0x[a-fA-F0-9]{40}$/.test(value)
} }
function formatUsd(value: string | number | undefined): string {
if (value == null) return 'Unavailable'
const numeric = typeof value === 'number' ? value : Number(value)
if (!Number.isFinite(numeric)) return 'Unavailable'
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: numeric >= 100 ? 0 : 2,
}).format(numeric)
}
export default function AddressDetailPage() { export default function AddressDetailPage() {
const router = useRouter() const router = useRouter()
const address = typeof router.query.address === 'string' ? router.query.address : '' const address = typeof router.query.address === 'string' ? router.query.address : ''
@@ -51,6 +65,8 @@ export default function AddressDetailPage() {
const [watchlistEntries, setWatchlistEntries] = useState<string[]>([]) const [watchlistEntries, setWatchlistEntries] = useState<string[]>([])
const [methodResults, setMethodResults] = useState<Record<string, { loading: boolean; value?: string; error?: string }>>({}) const [methodResults, setMethodResults] = useState<Record<string, { loading: boolean; value?: string; error?: string }>>({})
const [methodInputs, setMethodInputs] = useState<Record<string, string[]>>({}) const [methodInputs, setMethodInputs] = useState<Record<string, string[]>>({})
const [tokenMarkets, setTokenMarkets] = useState<Record<string, TokenAggregationTokenSnapshot>>({})
const [nativeAssetPriceUsd, setNativeAssetPriceUsd] = useState<number | undefined>()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const loadAddressInfo = useCallback(async () => { const loadAddressInfo = useCallback(async () => {
@@ -137,6 +153,46 @@ export default function AddressDetailPage() {
} }
}, []) }, [])
useEffect(() => {
let active = true
const tokenAddresses = [
...(addressInfo?.token_contract?.address ? [addressInfo.token_contract.address] : []),
...tokenBalances.map((balance) => balance.token_address),
...tokenTransfers.map((transfer) => transfer.token_address),
].filter((candidate, index, values): candidate is string => typeof candidate === 'string' && candidate.trim().length > 0 && values.indexOf(candidate) === index)
tokenAggregationApi.getTokensByAddressSafe(chainId, tokenAddresses).then(({ data }) => {
if (!active) return
setTokenMarkets(Object.fromEntries(data.map((snapshot) => [snapshot.address.toLowerCase(), snapshot])))
}).catch(() => {
if (active) {
setTokenMarkets({})
}
})
return () => {
active = false
}
}, [addressInfo?.token_contract?.address, chainId, tokenBalances, tokenTransfers])
useEffect(() => {
let active = true
getNativeAssetMarketSafe(chainId).then(({ data }) => {
if (!active) return
setNativeAssetPriceUsd(data?.market?.priceUsd)
}).catch(() => {
if (active) {
setNativeAssetPriceUsd(undefined)
}
})
return () => {
active = false
}
}, [chainId])
const watchlistAddress = normalizeWatchlistAddress(addressInfo?.address || address) const watchlistAddress = normalizeWatchlistAddress(addressInfo?.address || address)
const isSavedToWatchlist = watchlistAddress const isSavedToWatchlist = watchlistAddress
? isWatchlistEntry(watchlistEntries, watchlistAddress) ? isWatchlistEntry(watchlistEntries, watchlistAddress)
@@ -272,7 +328,17 @@ export default function AddressDetailPage() {
}, },
{ {
header: 'Value', header: 'Value',
accessor: (tx: TransactionSummary) => formatWeiAsEth(tx.value), accessor: (tx: TransactionSummary) => {
const nativeValueUsd = estimateNativeUsdValue(tx.value, nativeAssetPriceUsd)
return (
<div className="space-y-1 text-sm">
<div>{formatWeiAsEth(tx.value)}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{nativeValueUsd != null ? `Current USD: ${formatUsd(nativeValueUsd)}` : 'Current USD unavailable'}
</div>
</div>
)
},
}, },
{ {
header: 'Status', header: 'Status',
@@ -327,6 +393,20 @@ export default function AddressDetailPage() {
: 'N/A' : 'N/A'
), ),
}, },
{
header: 'Current Price',
accessor: (balance: AddressTokenBalance) => {
const market = tokenMarkets[balance.token_address.toLowerCase()]?.market
return (
<div className="space-y-1 text-sm">
<div>{formatUsd(market?.priceUsd)}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Liq. {formatUsd(market?.liquidityUsd)}
</div>
</div>
)
},
},
] ]
const tokenTransferColumns = [ const tokenTransferColumns = [
@@ -344,7 +424,7 @@ export default function AddressDetailPage() {
{gruMetadata ? <EntityBadge label="GRU" tone="success" /> : null} {gruMetadata ? <EntityBadge label="GRU" tone="success" /> : null}
{gruMetadata?.x402Ready ? <EntityBadge label="x402 ready" tone="info" /> : null} {gruMetadata?.x402Ready ? <EntityBadge label="x402 ready" tone="info" /> : null}
{gruMetadata?.iso20022Ready ? <EntityBadge label="ISO-20022" tone="info" /> : null} {gruMetadata?.iso20022Ready ? <EntityBadge label="ISO-20022" tone="info" /> : null}
{gruMetadata?.transportActiveVersion ? <EntityBadge label={`transport ${gruMetadata.transportActiveVersion}`} tone="warning" /> : null} {gruMetadata?.transportActiveVersion ? <EntityBadge label={`cW public-network ${gruMetadata.transportActiveVersion}`} tone="warning" /> : null}
</div> </div>
{transfer.token_address && ( {transfer.token_address && (
<Link href={`/tokens/${transfer.token_address}`} className="text-primary-600 hover:underline"> <Link href={`/tokens/${transfer.token_address}`} className="text-primary-600 hover:underline">
@@ -383,6 +463,20 @@ export default function AddressDetailPage() {
header: 'When', header: 'When',
accessor: (transfer: AddressTokenTransfer) => formatTimestamp(transfer.timestamp), accessor: (transfer: AddressTokenTransfer) => formatTimestamp(transfer.timestamp),
}, },
{
header: 'Current Price',
accessor: (transfer: AddressTokenTransfer) => {
const market = tokenMarkets[transfer.token_address.toLowerCase()]?.market
return (
<div className="space-y-1 text-sm">
<div>{formatUsd(market?.priceUsd)}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Liq. {formatUsd(market?.liquidityUsd)}
</div>
</div>
)
},
},
] ]
const incomingTransactions = transactions.filter( const incomingTransactions = transactions.filter(
@@ -403,6 +497,8 @@ export default function AddressDetailPage() {
const gruTransferCount = tokenTransfers.filter((transfer) => const gruTransferCount = tokenTransfers.filter((transfer) =>
Boolean(getGruExplorerMetadata({ address: transfer.token_address, symbol: transfer.token_symbol })), Boolean(getGruExplorerMetadata({ address: transfer.token_address, symbol: transfer.token_symbol })),
).length ).length
const nativeAssetSymbol = getNativeAssetDescriptor(chainId).symbol
const nativeBalanceUsd = estimateNativeUsdValue(addressInfo?.balance, nativeAssetPriceUsd)
return ( return (
<div className="container mx-auto px-4 py-6 sm:py-8"> <div className="container mx-auto px-4 py-6 sm:py-8">
@@ -473,8 +569,14 @@ export default function AddressDetailPage() {
<Address address={addressInfo.address} /> <Address address={addressInfo.address} />
</DetailRow> </DetailRow>
{addressInfo.balance && ( {addressInfo.balance && (
<DetailRow label="Coin Balance">{formatWeiAsEth(addressInfo.balance)}</DetailRow> <DetailRow label="Coin Balance">
{formatWeiAsEth(addressInfo.balance)}
{nativeBalanceUsd != null ? ` (${formatUsd(nativeBalanceUsd)})` : ''}
</DetailRow>
)} )}
<DetailRow label="Current Native Asset Price">
{nativeAssetPriceUsd != null ? `${formatUsd(nativeAssetPriceUsd)} per ${nativeAssetSymbol}` : 'Unavailable'}
</DetailRow>
<DetailRow label="Watchlist"> <DetailRow label="Watchlist">
{isSavedToWatchlist ? 'Saved for quick access' : 'Not saved yet'} {isSavedToWatchlist ? 'Saved for quick access' : 'Not saved yet'}
</DetailRow> </DetailRow>
@@ -601,20 +703,6 @@ export default function AddressDetailPage() {
</code> </code>
</DetailRow> </DetailRow>
)} )}
{contractProfile?.source_code_preview && (
<DetailRow label="Source Preview">
<code className="block max-h-56 overflow-auto whitespace-pre-wrap break-words rounded bg-gray-50 p-2 text-xs dark:bg-gray-950">
{contractProfile.source_code_preview}
</code>
</DetailRow>
)}
{contractProfile?.abi && (
<DetailRow label="ABI Preview">
<code className="block max-h-56 overflow-auto whitespace-pre-wrap break-words rounded bg-gray-50 p-2 text-xs dark:bg-gray-950">
{contractProfile.abi}
</code>
</DetailRow>
)}
{contractProfile?.read_methods && contractProfile.read_methods.length > 0 && ( {contractProfile?.read_methods && contractProfile.read_methods.length > 0 && (
<DetailRow label="Read Methods"> <DetailRow label="Read Methods">
<div className="space-y-3"> <div className="space-y-3">
@@ -760,6 +848,10 @@ export default function AddressDetailPage() {
</Card> </Card>
)} )}
{addressInfo.is_contract && contractProfile ? (
<ContractCodeWorkspace address={addressInfo.address} profile={contractProfile} />
) : null}
{gruProfile ? <div className="mb-6"><GruStandardsCard profile={gruProfile} /></div> : null} {gruProfile ? <div className="mb-6"><GruStandardsCard profile={gruProfile} /></div> : null}
<Card title="Token Balances" className="mb-6"> <Card title="Token Balances" className="mb-6">

View File

@@ -11,7 +11,7 @@ export default function GruDocsPage() {
<PageIntro <PageIntro
eyebrow="Explorer Documentation" eyebrow="Explorer Documentation"
title="GRU Guide" title="GRU Guide"
description="A user-facing summary of the GRU standards, transport posture, and x402 readiness model, with concrete places to inspect those signals on live token, address, and search pages." description="A user-facing summary of the GRU standards, bridge posture, public-network representations, and x402 readiness model, with concrete places to inspect those signals on live token, address, and search pages."
actions={[ actions={[
{ href: '/tokens', label: 'Browse tokens' }, { href: '/tokens', label: 'Browse tokens' },
{ href: '/search?q=cUSDC', label: 'Search cUSDC' }, { href: '/search?q=cUSDC', label: 'Search cUSDC' },
@@ -23,7 +23,7 @@ export default function GruDocsPage() {
<Card title="What The Explorer Is Showing You"> <Card title="What The Explorer Is Showing You">
<div className="space-y-4 text-sm text-gray-600 dark:text-gray-400"> <div className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
<p> <p>
The explorer now distinguishes between canonical GRU money surfaces on Chain 138 and wrapped transport assets used on public-chain bridge lanes. The explorer now distinguishes between canonical GRU surfaces on Chain 138 and cW public-network representations used on bridge lanes.
It also highlights when a token looks ready for x402-style payment flows. It also highlights when a token looks ready for x402-style payment flows.
</p> </p>
<p> <p>
@@ -49,6 +49,14 @@ export default function GruDocsPage() {
<Card title="Standards Summary"> <Card title="Standards Summary">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 text-sm dark:border-gray-700 dark:bg-gray-900/40 md:col-span-2">
<div className="font-medium text-gray-900 dark:text-white">Public token language</div>
<div className="mt-2 text-gray-600 dark:text-gray-400">
The explorer follows the GRU monetary policy taxonomy: <strong>c</strong> means compliant instrument created by a regulated financial entity or institution,
<strong> W</strong> means wrapped representation on a public network, <strong>XXX</strong> is the ISO-4217 currency code or ISO-style commodity code,
<strong> C</strong> marks cash-tokenized electronic money, and <strong> T</strong> marks treasury or government bond exposure.
</div>
</div>
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 text-sm dark:border-gray-700 dark:bg-gray-900/40"> <div className="rounded-xl border border-gray-200 bg-gray-50 p-4 text-sm dark:border-gray-700 dark:bg-gray-900/40">
<div className="font-medium text-gray-900 dark:text-white">Base token profile</div> <div className="font-medium text-gray-900 dark:text-white">Base token profile</div>
<div className="mt-2 text-gray-600 dark:text-gray-400"> <div className="mt-2 text-gray-600 dark:text-gray-400">
@@ -95,7 +103,7 @@ export default function GruDocsPage() {
<Card title="Chain 138 Practical Reading"> <Card title="Chain 138 Practical Reading">
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-400"> <div className="space-y-3 text-sm text-gray-600 dark:text-gray-400">
<p> <p>
A token can be forward-canonical and x402-ready even while older liquidity or transport lanes still run on a prior version. A token can be forward-canonical and x402-ready even while older liquidity or bridge lanes still run on a prior version.
That is why the explorer separates active liquidity posture from forward-canonical posture. That is why the explorer separates active liquidity posture from forward-canonical posture.
</p> </p>
<p> <p>

View File

@@ -8,7 +8,7 @@ const docsCards = [
{ {
title: 'GRU Guide', title: 'GRU Guide',
href: '/docs/gru', href: '/docs/gru',
description: 'Understand GRU standards, x402 readiness, wrapped transport posture, and forward-canonical versioning as surfaced by the explorer.', description: 'Understand GRU standards, x402 readiness, cW public-network posture, and forward-canonical versioning as surfaced by the explorer.',
}, },
{ {
title: 'Transaction Evidence Matrix', title: 'Transaction Evidence Matrix',
@@ -88,8 +88,8 @@ export default function DocsIndexPage() {
<Card title="Operator & Domains"> <Card title="Operator & Domains">
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-400"> <div className="space-y-3 text-sm text-gray-600 dark:text-gray-400">
<p> <p>
SolaceScan is the public Chain 138 explorer operated by DBIS / Defi Oracle. The explorer may be reached through DBIS Explorer is the public Chain 138 explorer operated by DBIS. Primary public access is served at
<code> blockscout.defi-oracle.io</code> or <code> explorer.d-bis.org</code>. <code> explorer.d-bis.org</code>; <code> blockscout.defi-oracle.io</code> is the Blockscout companion domain.
</p> </p>
<p> <p>
These domains are part of the same explorer and companion-tooling surface, including the Snap install path at These domains are part of the same explorer and companion-tooling surface, including the Snap install path at

View File

@@ -12,7 +12,7 @@ export default function HomeAliasPage() {
return ( return (
<main className="container mx-auto px-4 py-12"> <main className="container mx-auto px-4 py-12">
<div className="mx-auto max-w-xl rounded-xl border border-gray-200 bg-white p-6 text-center shadow-sm dark:border-gray-700 dark:bg-gray-800"> <div className="mx-auto max-w-xl rounded-xl border border-gray-200 bg-white p-6 text-center shadow-sm dark:border-gray-700 dark:bg-gray-800">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Redirecting to SolaceScan</h1> <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Redirecting to DBIS Explorer</h1>
<p className="mt-3 text-sm leading-7 text-gray-600 dark:text-gray-400"> <p className="mt-3 text-sm leading-7 text-gray-600 dark:text-gray-400">
The legacy <code className="rounded bg-gray-100 px-1 py-0.5 text-xs dark:bg-gray-900">/home</code> route now redirects to the main explorer landing page. The legacy <code className="rounded bg-gray-100 px-1 py-0.5 text-xs dark:bg-gray-900">/home</code> route now redirects to the main explorer landing page.
</p> </p>

View File

@@ -4,6 +4,7 @@ import { useRouter } from 'next/router'
import { Card, Address } from '@/libs/frontend-ui-primitives' import { Card, Address } from '@/libs/frontend-ui-primitives'
import Link from 'next/link' import Link from 'next/link'
import { configApi, type TokenListToken } from '@/services/api/config' import { configApi, type TokenListToken } from '@/services/api/config'
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
import EntityBadge from '@/components/common/EntityBadge' import EntityBadge from '@/components/common/EntityBadge'
import { import {
inferDirectSearchTarget, inferDirectSearchTarget,
@@ -15,6 +16,7 @@ import {
import PageIntro from '@/components/common/PageIntro' import PageIntro from '@/components/common/PageIntro'
import { fetchPublicJson } from '@/utils/publicExplorer' import { fetchPublicJson } from '@/utils/publicExplorer'
import { useUiMode } from '@/components/common/UiModeContext' import { useUiMode } from '@/components/common/UiModeContext'
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
type SearchFilterMode = 'all' | 'gru' | 'x402' | 'wrapped' type SearchFilterMode = 'all' | 'gru' | 'x402' | 'wrapped'
@@ -24,6 +26,15 @@ interface SearchPageProps {
initialCuratedTokens: TokenListToken[] initialCuratedTokens: TokenListToken[]
} }
function formatUsd(value: number | undefined): string {
if (value == null || !Number.isFinite(value)) return 'Unavailable'
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: value >= 100 ? 0 : 2,
}).format(value)
}
export default function SearchPage({ export default function SearchPage({
initialQuery, initialQuery,
initialRawResults, initialRawResults,
@@ -40,6 +51,7 @@ export default function SearchPage({
const [curatedTokens, setCuratedTokens] = useState<TokenListToken[]>(initialCuratedTokens) const [curatedTokens, setCuratedTokens] = useState<TokenListToken[]>(initialCuratedTokens)
const [savedQueries, setSavedQueries] = useState<string[]>([]) const [savedQueries, setSavedQueries] = useState<string[]>([])
const [filterMode, setFilterMode] = useState<SearchFilterMode>('all') const [filterMode, setFilterMode] = useState<SearchFilterMode>('all')
const [tokenMarkets, setTokenMarkets] = useState<Record<string, TokenAggregationTokenSnapshot>>({})
const runSearch = async (rawQuery: string) => { const runSearch = async (rawQuery: string) => {
const trimmedQuery = rawQuery.trim() const trimmedQuery = rawQuery.trim()
@@ -190,6 +202,27 @@ export default function SearchPage({
{ label: 'Other', items: groupedResults.other }, { label: 'Other', items: groupedResults.other },
] ]
useEffect(() => {
let active = true
const tokenAddresses = filteredResults
.filter((result) => result.type === 'token' && typeof result.data.address === 'string' && result.data.address.trim().length > 0)
.map((result) => result.data.address as string)
tokenAggregationApi.getTokensByAddressSafe(138, tokenAddresses).then(({ data }) => {
if (!active) return
setTokenMarkets(Object.fromEntries(data.map((snapshot) => [snapshot.address.toLowerCase(), snapshot])))
}).catch(() => {
if (active) {
setTokenMarkets({})
}
})
return () => {
active = false
}
}, [filteredResults])
return ( return (
<div className="container mx-auto px-4 py-6 sm:py-8"> <div className="container mx-auto px-4 py-6 sm:py-8">
<PageIntro <PageIntro
@@ -341,30 +374,46 @@ export default function SearchPage({
</Link> </Link>
)} )}
{(result.type === 'address' || result.type === 'token') && result.data.address && ( {(result.type === 'address' || result.type === 'token') && result.data.address && (
<Link (() => {
href={result.href || (result.type === 'token' ? `/tokens/${result.data.address}` : `/addresses/${result.data.address}`)} const market = result.type === 'token'
className="inline-flex flex-col gap-2 text-primary-600 hover:underline" ? tokenMarkets[result.data.address.toLowerCase()]?.market
> : null
<div className="flex flex-wrap items-center gap-2"> return (
<EntityBadge <Link
label={result.type === 'token' ? 'token' : 'address'} href={result.href || (result.type === 'token' ? `/tokens/${result.data.address}` : `/addresses/${result.data.address}`)}
tone={result.type === 'token' ? 'success' : 'neutral'} className="inline-flex flex-col gap-2 text-primary-600 hover:underline"
/> >
{result.symbol && <EntityBadge label={result.symbol} tone="info" />} <div className="flex flex-wrap items-center gap-2">
{result.token_type && <EntityBadge label={result.token_type} tone="warning" />} <EntityBadge
{result.is_curated_token && <EntityBadge label="listed" tone="success" />} label={result.type === 'token' ? 'token' : 'address'}
{result.is_gru_token && <EntityBadge label="GRU" tone="success" />} tone={result.type === 'token' ? 'success' : 'neutral'}
{result.is_x402_ready && <EntityBadge label="x402 ready" tone="info" />} />
{result.is_wrapped_transport && <EntityBadge label="wrapped" tone="warning" />} {result.symbol && <EntityBadge label={result.symbol} tone="info" />}
{result.currency_code ? <EntityBadge label={result.currency_code} tone="neutral" /> : null} {result.token_type && <EntityBadge label={result.token_type} tone="warning" />}
{result.match_reason ? <EntityBadge label={result.match_reason} tone="info" className="normal-case tracking-normal" /> : null} {result.is_curated_token && <EntityBadge label="listed" tone="success" />}
{result.matched_tags?.map((tag) => <EntityBadge key={tag} label={tag} />)} {result.is_gru_token && <EntityBadge label="GRU" tone="success" />}
</div> {result.is_x402_ready && <EntityBadge label="x402 ready" tone="info" />}
<span className="font-medium text-gray-900 dark:text-white"> {result.is_wrapped_transport && <EntityBadge label="cW public-network" tone="warning" />}
{result.name || result.symbol || result.label} {result.currency_code ? <EntityBadge label={result.currency_code} tone="neutral" /> : null}
</span> {result.match_reason ? <EntityBadge label={result.match_reason} tone="info" className="normal-case tracking-normal" /> : null}
<Address address={result.data.address} truncate showCopy={false} /> {result.matched_tags?.map((tag) => <EntityBadge key={tag} label={tag} />)}
</Link> </div>
<span className="font-medium text-gray-900 dark:text-white">
{result.name || result.symbol || result.label}
</span>
<Address address={result.data.address} truncate showCopy={false} />
{market ? (
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="flex flex-wrap gap-x-3 gap-y-1">
<span>Live price: {formatUsd(market.priceUsd)}</span>
<span>Visible liquidity: {formatUsd(market.liquidityUsd)}</span>
</div>
<MarketEvidenceNote lastUpdated={market.lastUpdated} compact />
</div>
) : null}
</Link>
)
})()
)} )}
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-sm text-gray-500"> <div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-sm text-gray-500">
<span>Type: {result.type}</span> <span>Type: {result.type}</span>

View File

@@ -11,6 +11,7 @@ import PageIntro from '@/components/common/PageIntro'
import { DetailRow } from '@/components/common/DetailRow' import { DetailRow } from '@/components/common/DetailRow'
import EntityBadge from '@/components/common/EntityBadge' import EntityBadge from '@/components/common/EntityBadge'
import GruStandardsCard from '@/components/common/GruStandardsCard' import GruStandardsCard from '@/components/common/GruStandardsCard'
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
import { formatTokenAmount, formatTimestamp } from '@/utils/format' import { formatTokenAmount, formatTimestamp } from '@/utils/format'
import { getGruStandardsProfileSafe, type GruStandardsProfile } from '@/services/api/gru' import { getGruStandardsProfileSafe, type GruStandardsProfile } from '@/services/api/gru'
import { getGruExplorerMetadata } from '@/services/api/gruExplorerData' import { getGruExplorerMetadata } from '@/services/api/gruExplorerData'
@@ -346,9 +347,17 @@ export default function TokenDetailPage() {
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"> <div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Market Context</div> <div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Market Context</div>
<div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300"> <div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<div>Indicative price: {formatUsd(token.exchange_rate)}</div> <div>Current price: {formatUsd(token.exchange_rate)}</div>
<div>24h volume: {formatUsd(token.volume_24h)}</div> <div>24h volume: {formatUsd(token.volume_24h)}</div>
<div>Market cap: {formatUsd(token.circulating_market_cap)}</div> <div>Market cap: {formatUsd(token.circulating_market_cap)}</div>
<div>Visible liquidity: {formatUsd(token.liquidity_usd)}</div>
<div>Valuation source: {token.price_source === 'token-aggregation' ? 'live token aggregation' : token.price_source || 'unavailable'}</div>
<div>Market snapshot: {token.market_updated_at ? formatTimestamp(token.market_updated_at) : 'Unavailable'}</div>
<MarketEvidenceNote
source={token.price_source}
lastUpdated={token.market_updated_at}
method="Merged Blockscout token profile with token aggregation price, volume, and visible-liquidity fields where available."
/>
</div> </div>
</div> </div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"> <div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
@@ -429,7 +438,7 @@ export default function TokenDetailPage() {
<Card title="Other Networks"> <Card title="Other Networks">
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
These are sibling representations or settlement counterparts for the same GRU asset family on other networks, drawn from the local transport and mapping posture used by this workspace. These are sibling representations or settlement counterparts for the same GRU asset family on other networks, drawn from the local public-network overlay and mapping posture used by this workspace.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
{gruExplorerMetadata.otherNetworks.map((network) => ( {gruExplorerMetadata.otherNetworks.map((network) => (

View File

@@ -5,13 +5,15 @@ import { useEffect, useMemo, useState } from 'react'
import { Card } from '@/libs/frontend-ui-primitives' import { Card } from '@/libs/frontend-ui-primitives'
import PageIntro from '@/components/common/PageIntro' import PageIntro from '@/components/common/PageIntro'
import EntityBadge from '@/components/common/EntityBadge' import EntityBadge from '@/components/common/EntityBadge'
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
import { tokensApi } from '@/services/api/tokens' import { tokensApi } from '@/services/api/tokens'
import type { TokenListToken } from '@/services/api/config' import type { TokenListToken } from '@/services/api/config'
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
import { fetchPublicJson } from '@/utils/publicExplorer' import { fetchPublicJson } from '@/utils/publicExplorer'
const quickSearches = [ const quickSearches = [
{ label: 'cUSDT', description: 'Canonical bridged USDT liquidity and address results.' }, { label: 'cUSDT', description: 'Canonical compliant USD treasury / government bond liquidity and address results.' },
{ label: 'cUSDC', description: 'Canonical bridged USDC routes and address coverage.' }, { label: 'cUSDC', description: 'Canonical compliant USD cash electronic-money routes and address coverage.' },
{ label: 'cXAUC', description: 'Gold-backed cXAUC pools and token references.' }, { label: 'cXAUC', description: 'Gold-backed cXAUC pools and token references.' },
{ label: 'cXAUT', description: 'Gold-backed cXAUT references and search coverage.' }, { label: 'cXAUT', description: 'Gold-backed cXAUT references and search coverage.' },
{ label: 'cEURT', description: 'EUR liquidity and cXAUC-connected route coverage.' }, { label: 'cEURT', description: 'EUR liquidity and cXAUC-connected route coverage.' },
@@ -27,10 +29,33 @@ interface TokensPageProps {
initialCuratedTokens: TokenListToken[] initialCuratedTokens: TokenListToken[]
} }
function formatUsd(value: number | undefined): string {
if (value == null || !Number.isFinite(value)) return 'Unavailable'
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: value >= 100 ? 0 : 2,
}).format(value)
}
function tagPriority(tag: string) {
const order: Record<string, number> = {
gru: 0,
compliant: 1,
'treasury-bond': 2,
'electronic-money': 2,
commodity: 2,
'reference-asset': 0,
defi: 4,
}
return order[tag] ?? 3
}
export default function TokensPage({ initialCuratedTokens }: TokensPageProps) { export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
const router = useRouter() const router = useRouter()
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [curatedTokens, setCuratedTokens] = useState<TokenListToken[]>(initialCuratedTokens) const [curatedTokens, setCuratedTokens] = useState<TokenListToken[]>(initialCuratedTokens)
const [featuredMarkets, setFeaturedMarkets] = useState<Record<string, TokenAggregationTokenSnapshot>>({})
const handleSubmit = (event: React.FormEvent) => { const handleSubmit = (event: React.FormEvent) => {
event.preventDefault() event.preventDefault()
@@ -68,12 +93,34 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
return selected.length > 0 ? selected : curatedTokens.slice(0, 6) return selected.length > 0 ? selected : curatedTokens.slice(0, 6)
}, [curatedTokens]) }, [curatedTokens])
useEffect(() => {
let active = true
const featuredAddresses = featuredCuratedTokens
.map((token) => token.address)
.filter((address): address is string => typeof address === 'string' && address.trim().length > 0)
tokenAggregationApi.getTokensByAddressSafe(138, featuredAddresses).then(({ data }) => {
if (!active) return
const next = Object.fromEntries(data.map((snapshot) => [snapshot.address.toLowerCase(), snapshot]))
setFeaturedMarkets(next)
}).catch(() => {
if (active) {
setFeaturedMarkets({})
}
})
return () => {
active = false
}
}, [featuredCuratedTokens])
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<PageIntro <PageIntro
eyebrow="Token Discovery" eyebrow="Token Discovery"
title="Tokens" title="Tokens"
description="Browse curated Chain 138 assets, open token contracts directly, and move into holders, transfers, liquidity, and provenance without pretending a search box is a complete token strategy." description="Browse curated Chain 138 assets, open token contracts directly, and review holders, transfers, liquidity, and provenance from the same institutional explorer surface."
actions={[ actions={[
{ href: '/wallet', label: 'Wallet tools' }, { href: '/wallet', label: 'Wallet tools' },
{ href: '/liquidity', label: 'Liquidity access' }, { href: '/liquidity', label: 'Liquidity access' },
@@ -81,7 +128,7 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
]} ]}
/> />
<Card className="mb-6" title="Find A Token"> <Card className="mb-6" title="Find a token">
<form onSubmit={handleSubmit} className="flex flex-col gap-3 md:flex-row"> <form onSubmit={handleSubmit} className="flex flex-col gap-3 md:flex-row">
<input <input
type="text" type="text"
@@ -106,7 +153,7 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
<div className="grid gap-6 lg:grid-cols-3"> <div className="grid gap-6 lg:grid-cols-3">
<Card title="Curated Registry"> <Card title="Curated Registry">
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Review listed Chain 138 assets with provenance tags such as compliant, wrapped, and bridge-aware before acting on a symbol match. Review listed Chain 138 assets with provenance tags such as GRU, compliant, cW public-network, and reference asset before acting on a symbol match.
</p> </p>
<div className="mt-4"> <div className="mt-4">
<Link href="/tokens" className="text-primary-600 hover:underline"> <Link href="/tokens" className="text-primary-600 hover:underline">
@@ -139,25 +186,37 @@ export default function TokensPage({ initialCuratedTokens }: TokensPageProps) {
<div className="mt-8"> <div className="mt-8">
<Card title="Curated Chain 138 tokens"> <Card title="Curated Chain 138 tokens">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{featuredCuratedTokens.map((token) => ( {featuredCuratedTokens
<Link .filter((token): token is TokenListToken & { address: string } => typeof token.address === 'string' && token.address.trim().length > 0)
key={token.address} .map((token) => {
href={`/tokens/${token.address}`} const market = featuredMarkets[token.address.toLowerCase()]?.market
className="rounded-lg border border-gray-200 p-4 transition hover:border-primary-400 hover:shadow-sm dark:border-gray-700" return (
> <Link
<div className="text-sm font-semibold text-gray-900 dark:text-white">{token.symbol || token.name || 'Token'}</div> key={token.address}
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400"> href={`/tokens/${token.address}`}
{token.name || 'Listed in the Chain 138 token registry.'} className="rounded-lg border border-gray-200 p-4 transition hover:border-primary-400 hover:shadow-sm dark:border-gray-700"
</p> >
{token.tags && token.tags.length > 0 && ( <div className="text-sm font-semibold text-gray-900 dark:text-white">{token.symbol || token.name || 'Token'}</div>
<div className="mt-3 flex flex-wrap gap-2"> <p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{token.tags.slice(0, 3).map((tag) => ( {token.name || 'Listed in the Chain 138 token registry.'}
<EntityBadge key={`${token.address}-${tag}`} label={tag} className="px-2 py-1 text-[11px]" /> </p>
))} {market ? (
</div> <div className="mt-3 space-y-1 text-sm text-gray-700 dark:text-gray-300">
)} <div>Live price: {formatUsd(market.priceUsd)}</div>
</Link> <div>Visible liquidity: {formatUsd(market.liquidityUsd)}</div>
))} <MarketEvidenceNote lastUpdated={market.lastUpdated} compact />
</div>
) : null}
{token.tags && token.tags.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{[...token.tags].sort((a, b) => tagPriority(a) - tagPriority(b)).slice(0, 3).map((tag) => (
<EntityBadge key={`${token.address}-${tag}`} label={tag} className="px-2 py-1 text-[11px]" />
))}
</div>
)}
</Link>
)
})}
</div> </div>
</Card> </Card>
</div> </div>

View File

@@ -17,11 +17,47 @@ import EntityBadge from '@/components/common/EntityBadge'
import PageIntro from '@/components/common/PageIntro' import PageIntro from '@/components/common/PageIntro'
import { getGruCatalogPosture } from '@/services/api/gruCatalog' import { getGruCatalogPosture } from '@/services/api/gruCatalog'
import { assessTransactionCompliance } from '@/utils/transactionCompliance' import { assessTransactionCompliance } from '@/utils/transactionCompliance'
import { tokenAggregationApi, type TokenAggregationHistoricalPriceSnapshot } from '@/services/api/tokenAggregation'
import { estimateNativeUsdValue, estimateTokenUsdValue, getNativeAssetDescriptor, getNativeAssetPriceAtSafe } from '@/services/api/nativeAssetPricing'
function isValidTransactionHash(value: string) { function isValidTransactionHash(value: string) {
return /^0x[a-fA-F0-9]{64}$/.test(value) return /^0x[a-fA-F0-9]{64}$/.test(value)
} }
function formatUsd(value: string | number | undefined): string {
if (value == null) return 'Unavailable'
const numeric = typeof value === 'number' ? value : Number(value)
if (!Number.isFinite(numeric)) return 'Unavailable'
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: numeric >= 100 ? 0 : 2,
}).format(numeric)
}
function formatHistoricalPriceSource(source: string | undefined): string {
switch (source) {
case 'ohlcv_5m':
return 'historical OHLCV 5m'
case 'ohlcv_15m':
return 'historical OHLCV 15m'
case 'ohlcv_1h':
return 'historical OHLCV 1h'
case 'ohlcv_4h':
return 'historical OHLCV 4h'
case 'ohlcv_24h':
return 'historical OHLCV 24h'
case 'coingecko_history':
return 'historical CoinGecko market'
case 'current_market_fallback':
return 'current market fallback'
case 'canonical_fallback':
return 'canonical fallback'
default:
return 'unavailable'
}
}
export default function TransactionDetailPage() { export default function TransactionDetailPage() {
const router = useRouter() const router = useRouter()
const hash = typeof router.query.hash === 'string' ? router.query.hash : '' const hash = typeof router.query.hash === 'string' ? router.query.hash : ''
@@ -31,6 +67,8 @@ export default function TransactionDetailPage() {
const [transaction, setTransaction] = useState<Transaction | null>(null) const [transaction, setTransaction] = useState<Transaction | null>(null)
const [internalCalls, setInternalCalls] = useState<TransactionInternalCall[]>([]) const [internalCalls, setInternalCalls] = useState<TransactionInternalCall[]>([])
const [diagnostic, setDiagnostic] = useState<TransactionLookupDiagnostic | null>(null) const [diagnostic, setDiagnostic] = useState<TransactionLookupDiagnostic | null>(null)
const [historicalTokenPrices, setHistoricalTokenPrices] = useState<Record<string, TokenAggregationHistoricalPriceSnapshot>>({})
const [historicalNativePrice, setHistoricalNativePrice] = useState<TokenAggregationHistoricalPriceSnapshot | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const loadTransaction = useCallback(async () => { const loadTransaction = useCallback(async () => {
@@ -91,6 +129,62 @@ export default function TransactionDetailPage() {
loadTransaction() loadTransaction()
}, [hash, isValidHash, loadTransaction, router.isReady]) }, [hash, isValidHash, loadTransaction, router.isReady])
useEffect(() => {
let active = true
const tokenAddresses = (transaction?.token_transfers || [])
.map((transfer) => transfer.token_address)
.filter((candidate, index, values): candidate is string => typeof candidate === 'string' && candidate.trim().length > 0 && values.indexOf(candidate) === index)
if (!transaction?.created_at || tokenAddresses.length === 0) {
setHistoricalTokenPrices({})
return () => {
active = false
}
}
Promise.all(tokenAddresses.map((address) => tokenAggregationApi.getPriceAtSafe(chainId, address, transaction.created_at))).then((results) => {
if (!active) return
const snapshots = results
.filter((result): result is { ok: true; data: TokenAggregationHistoricalPriceSnapshot | null } => result.ok)
.map((result) => result.data)
.filter((snapshot): snapshot is TokenAggregationHistoricalPriceSnapshot => Boolean(snapshot?.tokenAddress))
setHistoricalTokenPrices(Object.fromEntries(snapshots.map((snapshot) => [snapshot.tokenAddress.toLowerCase(), snapshot])))
}).catch(() => {
if (active) {
setHistoricalTokenPrices({})
}
})
return () => {
active = false
}
}, [chainId, transaction?.created_at, transaction?.token_transfers])
useEffect(() => {
let active = true
if (!transaction?.created_at) {
setHistoricalNativePrice(null)
return () => {
active = false
}
}
getNativeAssetPriceAtSafe(chainId, transaction.created_at).then(({ data }) => {
if (!active) return
setHistoricalNativePrice(data)
}).catch(() => {
if (active) {
setHistoricalNativePrice(null)
}
})
return () => {
active = false
}
}, [chainId, transaction?.created_at])
const tokenTransferColumns = [ const tokenTransferColumns = [
{ {
header: 'Token', header: 'Token',
@@ -137,6 +231,24 @@ export default function TransactionDetailPage() {
formatTokenAmount(transfer.amount, transfer.token_decimals, transfer.token_symbol) formatTokenAmount(transfer.amount, transfer.token_decimals, transfer.token_symbol)
), ),
}, },
{
header: 'Transfer-Time Value',
accessor: (transfer: TransactionTokenTransfer) => {
const historicalPrice = historicalTokenPrices[transfer.token_address.toLowerCase()]
const totalUsd = estimateTokenUsdValue(transfer.amount, transfer.token_decimals, historicalPrice?.priceUsd)
return (
<div className="space-y-1 text-sm">
<div>{totalUsd != null ? formatUsd(totalUsd) : 'Unavailable'}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Unit price: {formatUsd(historicalPrice?.priceUsd)}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Source: {formatHistoricalPriceSource(historicalPrice?.source)}
</div>
</div>
)
},
},
] ]
const internalCallColumns = [ const internalCallColumns = [
@@ -186,6 +298,9 @@ export default function TransactionDetailPage() {
: null : null
const tokenTransferCount = transaction?.token_transfers?.length || 0 const tokenTransferCount = transaction?.token_transfers?.length || 0
const internalCallCount = internalCalls.length const internalCallCount = internalCalls.length
const nativeAssetSymbol = getNativeAssetDescriptor(chainId).symbol
const nativeValueUsd = estimateNativeUsdValue(transaction?.value, historicalNativePrice?.priceUsd)
const nativeFeeUsd = estimateNativeUsdValue(transaction?.fee, historicalNativePrice?.priceUsd)
const complianceAssessment = transaction const complianceAssessment = transaction
? assessTransactionCompliance({ ? assessTransactionCompliance({
transaction, transaction,
@@ -275,7 +390,10 @@ export default function TransactionDetailPage() {
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"> <div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Gas & Fees</div> <div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Gas & Fees</div>
<div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300"> <div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<div>Fee: {transaction.fee ? formatWeiAsEth(transaction.fee) : 'Unavailable'}</div> <div>
Fee: {transaction.fee ? formatWeiAsEth(transaction.fee) : 'Unavailable'}
{nativeFeeUsd != null ? ` (${formatUsd(nativeFeeUsd)})` : ''}
</div>
<div>Gas used: {transaction.gas_used != null ? transaction.gas_used.toLocaleString() : 'Unavailable'}</div> <div>Gas used: {transaction.gas_used != null ? transaction.gas_used.toLocaleString() : 'Unavailable'}</div>
<div>Utilization: {gasUtilization != null ? `${gasUtilization}%` : 'Unavailable'}</div> <div>Utilization: {gasUtilization != null ? `${gasUtilization}%` : 'Unavailable'}</div>
</div> </div>
@@ -283,7 +401,12 @@ export default function TransactionDetailPage() {
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"> <div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Value Movement</div> <div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Value Movement</div>
<div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300"> <div className="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<div>Native value: {formatWeiAsEth(transaction.value)}</div> <div>
Native value: {formatWeiAsEth(transaction.value)}
{nativeValueUsd != null ? ` (${formatUsd(nativeValueUsd)})` : ''}
</div>
<div>Transfer-time native price: {historicalNativePrice?.priceUsd != null ? `${formatUsd(historicalNativePrice.priceUsd)} per ${nativeAssetSymbol}` : 'Unavailable'}</div>
<div>Pricing source: {formatHistoricalPriceSource(historicalNativePrice?.source)}</div>
<div>Token transfers: {tokenTransferCount.toLocaleString()}</div> <div>Token transfers: {tokenTransferCount.toLocaleString()}</div>
<div>Internal calls: {internalCallCount.toLocaleString()}</div> <div>Internal calls: {internalCallCount.toLocaleString()}</div>
</div> </div>
@@ -368,8 +491,16 @@ export default function TransactionDetailPage() {
</Link> </Link>
</DetailRow> </DetailRow>
)} )}
<DetailRow label="Value">{formatWeiAsEth(transaction.value)}</DetailRow> <DetailRow label="Value">
{transaction.fee && <DetailRow label="Fee">{formatWeiAsEth(transaction.fee)}</DetailRow>} {formatWeiAsEth(transaction.value)}
{nativeValueUsd != null ? ` (${formatUsd(nativeValueUsd)})` : ''}
</DetailRow>
{transaction.fee && (
<DetailRow label="Fee">
{formatWeiAsEth(transaction.fee)}
{nativeFeeUsd != null ? ` (${formatUsd(nativeFeeUsd)})` : ''}
</DetailRow>
)}
<DetailRow label="Gas Price"> <DetailRow label="Gas Price">
{transaction.gas_price != null ? `${transaction.gas_price / 1e9} Gwei` : 'N/A'} {transaction.gas_price != null ? `${transaction.gas_price / 1e9} Gwei` : 'N/A'}
</DetailRow> </DetailRow>

View File

@@ -146,7 +146,7 @@ function setStoredWalletSession(session: WalletAccessSession | null) {
} }
function buildWalletMessage(nonce: string) { function buildWalletMessage(nonce: string) {
return `Sign this message to authenticate with SolaceScan.\n\nNonce: ${nonce}` return `Sign this message to authenticate with DBIS Explorer.\n\nNonce: ${nonce}`
} }
async function fetchWalletJson<T>(path: string, init?: RequestInit): Promise<T> { async function fetchWalletJson<T>(path: string, init?: RequestInit): Promise<T> {

View File

@@ -67,9 +67,57 @@ describe('contractsApi', () => {
expect(result.data?.optimization_runs).toBe(200) expect(result.data?.optimization_runs).toBe(200)
expect(result.data?.constructor_arguments?.endsWith('...')).toBe(true) expect(result.data?.constructor_arguments?.endsWith('...')).toBe(true)
expect(result.data?.abi).toContain('"symbol"') expect(result.data?.abi).toContain('"symbol"')
expect(result.data?.abi_full).toContain('"symbol"')
expect(result.data?.source_files).toEqual([{ path: 'MockToken.sol', content: 'contract MockToken {}' }])
expect(result.data?.read_methods.map((method) => method.signature)).toContain('symbol()') expect(result.data?.read_methods.map((method) => method.signature)).toContain('symbol()')
}) })
it('extracts Etherscan-style multi-file verified sources', async () => {
vi.stubGlobal(
'fetch',
vi
.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({
has_custom_methods_read: true,
has_custom_methods_write: true,
implementations: [],
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
status: '1',
message: 'OK',
result: '[]',
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
status: '1',
message: 'OK',
result: [
{
ContractName: 'RWAToken',
SourceCode:
'{{"language":"Solidity","sources":{"contracts/RWAToken.sol":{"content":"contract RWAToken {}"},"contracts/interfaces/IRWA.sol":{"content":"interface IRWA {}"}},"settings":{}}}',
},
],
}),
}),
)
const result = await contractsApi.getProfileSafe('0xcontract')
expect(result.ok).toBe(true)
expect(result.data?.source_files).toEqual([
{ path: 'contracts/RWAToken.sol', content: 'contract RWAToken {}' },
{ path: 'contracts/interfaces/IRWA.sol', content: 'interface IRWA {}' },
])
})
it('calls a simple zero-arg read method through public RPC', async () => { it('calls a simple zero-arg read method through public RPC', async () => {
vi.stubGlobal( vi.stubGlobal(
'fetch', 'fetch',

View File

@@ -18,6 +18,11 @@ export interface ContractMethodExecutionResult {
value: string value: string
} }
export interface ContractSourceFile {
path: string
content: string
}
export interface ContractProfile { export interface ContractProfile {
has_custom_methods_read: boolean has_custom_methods_read: boolean
has_custom_methods_write: boolean has_custom_methods_write: boolean
@@ -36,7 +41,10 @@ export interface ContractProfile {
license_type?: string license_type?: string
constructor_arguments?: string constructor_arguments?: string
abi?: string abi?: string
abi_full?: string
source_code_full?: string
source_code_preview?: string source_code_preview?: string
source_files: ContractSourceFile[]
source_status_text?: string source_status_text?: string
read_methods: ContractMethod[] read_methods: ContractMethod[]
write_methods: ContractMethod[] write_methods: ContractMethod[]
@@ -63,6 +71,7 @@ interface ContractCompatibilityAbiResponse {
interface ContractCompatibilitySourceRecord { interface ContractCompatibilitySourceRecord {
Address?: string Address?: string
ContractName?: string ContractName?: string
FileName?: string
CompilerVersion?: string CompilerVersion?: string
OptimizationUsed?: string | number OptimizationUsed?: string | number
Runs?: string | number Runs?: string | number
@@ -111,6 +120,47 @@ function normalizeNumber(value: string | number | null | undefined): number | un
return undefined return undefined
} }
function displaySourcePath(record: ContractCompatibilitySourceRecord | undefined): string {
const fileName = record?.FileName?.trim()
if (fileName) return fileName
const contractName = record?.ContractName?.trim()
if (contractName) return contractName.endsWith('.sol') ? contractName : `${contractName}.sol`
return 'Contract.sol'
}
function parseSourceFiles(sourceCode: string | undefined, record?: ContractCompatibilitySourceRecord): ContractSourceFile[] {
const trimmed = sourceCode?.trim()
if (!trimmed) return []
const candidates = [trimmed]
if (trimmed.startsWith('{{') && trimmed.endsWith('}}')) {
candidates.push(trimmed.slice(1, -1))
}
for (const candidate of candidates) {
try {
const parsed = JSON.parse(candidate) as {
sources?: Record<string, { content?: string } | string>
}
if (parsed && typeof parsed === 'object' && parsed.sources && typeof parsed.sources === 'object') {
return Object.entries(parsed.sources)
.map(([path, value]) => ({
path,
content: typeof value === 'string' ? value : value?.content || '',
}))
.filter((file) => file.content.trim().length > 0)
}
} catch {}
}
return [
{
path: displaySourcePath(record),
content: trimmed,
},
]
}
function parseABI(abiString?: string): ContractMethod[] { function parseABI(abiString?: string): ContractMethod[] {
if (!abiString) return [] if (!abiString) return []
try { try {
@@ -359,6 +409,7 @@ export const contractsApi = {
? sourceRecord.ABI ? sourceRecord.ABI
: undefined : undefined
const sourceCode = sourceRecord?.SourceCode const sourceCode = sourceRecord?.SourceCode
const sourceFiles = parseSourceFiles(sourceCode, sourceRecord)
const parsedMethods = parseABI(abiString) const parsedMethods = parseABI(abiString)
const sourceVerified = Boolean( const sourceVerified = Boolean(
abiString || abiString ||
@@ -391,7 +442,10 @@ export const contractsApi = {
license_type: sourceRecord?.LicenseType || undefined, license_type: sourceRecord?.LicenseType || undefined,
constructor_arguments: truncateHex(sourceRecord?.ConstructorArguments, 90), constructor_arguments: truncateHex(sourceRecord?.ConstructorArguments, 90),
abi: truncateText(abiString, 1200), abi: truncateText(abiString, 1200),
abi_full: abiString,
source_code_full: sourceCode,
source_code_preview: truncateText(sourceCode, 1200), source_code_preview: truncateText(sourceCode, 1200),
source_files: sourceFiles,
source_status_text: sourceStatusText || undefined, source_status_text: sourceStatusText || undefined,
read_methods: parsedMethods.filter(isReadMethod), read_methods: parsedMethods.filter(isReadMethod),
write_methods: parsedMethods.filter((method) => !isReadMethod(method)), write_methods: parsedMethods.filter((method) => !isReadMethod(method)),

View File

@@ -30,7 +30,7 @@ export interface GruStandardsProfile {
metadata: GruMetadataField[] metadata: GruMetadataField[]
} }
const GRU_PROFILE_ID = 'gru-c-star-v2-transport-and-payment' const GRU_PROFILE_ID = 'gru-c-star-v2-public-network-and-payment'
const STANDARD_DEFINITIONS = [ const STANDARD_DEFINITIONS = [
{ id: 'ERC-20', required: true }, { id: 'ERC-20', required: true },
@@ -40,7 +40,7 @@ const STANDARD_DEFINITIONS = [
{ id: 'ERC-2612', required: true }, { id: 'ERC-2612', required: true },
{ id: 'ERC-3009', required: true }, { id: 'ERC-3009', required: true },
{ id: 'ERC-5267', required: true }, { id: 'ERC-5267', required: true },
{ id: 'IeMoneyToken', required: true }, { id: 'CashElectronicMoneyInterface', required: true },
{ id: 'DeterministicStorageNamespace', required: true }, { id: 'DeterministicStorageNamespace', required: true },
{ id: 'JurisdictionAndSupervisionMetadata', required: true }, { id: 'JurisdictionAndSupervisionMetadata', required: true },
] as const ] as const
@@ -146,7 +146,7 @@ export async function getGruStandardsProfileSafe(input: {
'ERC-2612': nonces != null || hasMethod(contractProfile, 'permit') || hasMethod(contractProfile, 'nonces'), 'ERC-2612': nonces != null || hasMethod(contractProfile, 'permit') || hasMethod(contractProfile, 'nonces'),
'ERC-3009': authorizationState != null || hasMethod(contractProfile, 'authorizationState'), 'ERC-3009': authorizationState != null || hasMethod(contractProfile, 'authorizationState'),
'ERC-5267': hasMethod(contractProfile, 'eip712Domain'), 'ERC-5267': hasMethod(contractProfile, 'eip712Domain'),
IeMoneyToken: currencyCode != null || versionTag != null, CashElectronicMoneyInterface: currencyCode != null || versionTag != null,
DeterministicStorageNamespace: storageNamespace != null, DeterministicStorageNamespace: storageNamespace != null,
JurisdictionAndSupervisionMetadata: JurisdictionAndSupervisionMetadata:
governanceProfileId != null || governanceProfileId != null ||
@@ -187,7 +187,7 @@ export async function getGruStandardsProfileSafe(input: {
regulatoryDisclosureURI ? { label: 'Disclosure URI', value: regulatoryDisclosureURI } : null, regulatoryDisclosureURI ? { label: 'Disclosure URI', value: regulatoryDisclosureURI } : null,
reportingURI ? { label: 'Reporting URI', value: reportingURI } : null, reportingURI ? { label: 'Reporting URI', value: reportingURI } : null,
minimumUpgradeNoticePeriod ? { label: 'Upgrade Notice Period', value: `${minimumUpgradeNoticePeriod} seconds` } : null, minimumUpgradeNoticePeriod ? { label: 'Upgrade Notice Period', value: `${minimumUpgradeNoticePeriod} seconds` } : null,
wrappedTransport != null ? { label: 'Wrapped Transport', value: wrappedTransport } : null, wrappedTransport != null ? { label: 'cW Public-Network Representation', value: wrappedTransport } : null,
forwardCanonical != null ? { label: 'Forward Canonical', value: forwardCanonical } : null, forwardCanonical != null ? { label: 'Forward Canonical', value: forwardCanonical } : null,
legacyAliasSupport ? { label: 'Legacy Alias Support', value: 'true' } : null, legacyAliasSupport ? { label: 'Legacy Alias Support', value: 'true' } : null,
{ label: 'x402 Readiness', value: x402Ready ? 'true' : 'false' }, { label: 'x402 Readiness', value: x402Ready ? 'true' : 'false' },

View File

@@ -70,13 +70,13 @@ const GRU_EXPLORER_ENTRIES: GruExplorerEntry[] = [
otherNetworks: [ otherNetworks: [
networkLink(651940, 'ALL Mainnet (Alltra)', 'AUSDC', '0xa95EeD79f84E6A0151eaEb9d441F9Ffd50e8e881', 'Primary Alltra-native origin counterpart.'), networkLink(651940, 'ALL Mainnet (Alltra)', 'AUSDC', '0xa95EeD79f84E6A0151eaEb9d441F9Ffd50e8e881', 'Primary Alltra-native origin counterpart.'),
networkLink(1, 'Ethereum Mainnet', 'USDC', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'Native Ethereum settlement counterpart.'), networkLink(1, 'Ethereum Mainnet', 'USDC', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'Native Ethereum settlement counterpart.'),
networkLink(1, 'Ethereum Mainnet', 'cWUSDC', '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a', 'GRU wrapped transport representation on Ethereum.'), networkLink(1, 'Ethereum Mainnet', 'cWUSDC', '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a', 'GRU wrapped public-network representation on Ethereum.'),
networkLink(56, 'BNB Chain', 'USDC', '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', 'Native BNB Chain settlement counterpart.'), networkLink(56, 'BNB Chain', 'USDC', '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', 'Native BNB Chain settlement counterpart.'),
networkLink(56, 'BNB Chain', 'cWUSDC', '0x5355148C4740fcc3D7a96F05EdD89AB14851206b', 'GRU wrapped transport representation on BNB Chain.'), networkLink(56, 'BNB Chain', 'cWUSDC', '0x5355148C4740fcc3D7a96F05EdD89AB14851206b', 'GRU wrapped public-network representation on BNB Chain.'),
networkLink(137, 'Polygon', 'USDC', '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', 'Native Polygon settlement counterpart.'), networkLink(137, 'Polygon', 'USDC', '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', 'Native Polygon settlement counterpart.'),
networkLink(137, 'Polygon', 'cWUSDC', '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', 'GRU wrapped transport representation on Polygon.'), networkLink(137, 'Polygon', 'cWUSDC', '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', 'GRU wrapped public-network representation on Polygon.'),
networkLink(100, 'Gnosis', 'USDC', '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', 'Native Gnosis settlement counterpart.'), networkLink(100, 'Gnosis', 'USDC', '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', 'Native Gnosis settlement counterpart.'),
networkLink(100, 'Gnosis', 'cWUSDC', '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', 'GRU wrapped transport representation on Gnosis.'), networkLink(100, 'Gnosis', 'cWUSDC', '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4', 'GRU wrapped public-network representation on Gnosis.'),
], ],
}, },
{ {
@@ -96,13 +96,13 @@ const GRU_EXPLORER_ENTRIES: GruExplorerEntry[] = [
otherNetworks: [ otherNetworks: [
networkLink(651940, 'ALL Mainnet (Alltra)', 'AUSDT', '0x015B1897Ed5279930bC2Be46F661894d219292A6', 'Primary Alltra-native origin counterpart.'), networkLink(651940, 'ALL Mainnet (Alltra)', 'AUSDT', '0x015B1897Ed5279930bC2Be46F661894d219292A6', 'Primary Alltra-native origin counterpart.'),
networkLink(1, 'Ethereum Mainnet', 'USDT', '0xdAC17F958D2ee523a2206206994597C13D831ec7', 'Native Ethereum settlement counterpart.'), networkLink(1, 'Ethereum Mainnet', 'USDT', '0xdAC17F958D2ee523a2206206994597C13D831ec7', 'Native Ethereum settlement counterpart.'),
networkLink(1, 'Ethereum Mainnet', 'cWUSDT', '0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE', 'GRU wrapped transport representation on Ethereum.'), networkLink(1, 'Ethereum Mainnet', 'cWUSDT', '0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE', 'GRU wrapped public-network representation on Ethereum.'),
networkLink(56, 'BNB Chain', 'USDT', '0x55d398326f99059fF775485246999027B3197955', 'Native BNB Chain settlement counterpart.'), networkLink(56, 'BNB Chain', 'USDT', '0x55d398326f99059fF775485246999027B3197955', 'Native BNB Chain settlement counterpart.'),
networkLink(56, 'BNB Chain', 'cWUSDT', '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB', 'GRU wrapped transport representation on BNB Chain.'), networkLink(56, 'BNB Chain', 'cWUSDT', '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB', 'GRU wrapped public-network representation on BNB Chain.'),
networkLink(137, 'Polygon', 'USDT', '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', 'Native Polygon settlement counterpart.'), networkLink(137, 'Polygon', 'USDT', '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', 'Native Polygon settlement counterpart.'),
networkLink(137, 'Polygon', 'cWUSDT', '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', 'GRU wrapped transport representation on Polygon.'), networkLink(137, 'Polygon', 'cWUSDT', '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', 'GRU wrapped public-network representation on Polygon.'),
networkLink(100, 'Gnosis', 'USDT', '0x4ECaBa5870353805a9F068101A40E0f32ed605C6', 'Native Gnosis settlement counterpart.'), networkLink(100, 'Gnosis', 'USDT', '0x4ECaBa5870353805a9F068101A40E0f32ed605C6', 'Native Gnosis settlement counterpart.'),
networkLink(100, 'Gnosis', 'cWUSDT', '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', 'GRU wrapped transport representation on Gnosis.'), networkLink(100, 'Gnosis', 'cWUSDT', '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF', 'GRU wrapped public-network representation on Gnosis.'),
], ],
}, },
{ {
@@ -116,9 +116,9 @@ const GRU_EXPLORER_ENTRIES: GruExplorerEntry[] = [
canonicalForwardVersion: 'v2', canonicalForwardVersion: 'v2',
canonicalForwardAddress: '0x243e6581Dc8a98d98B92265858b322b193555C81', canonicalForwardAddress: '0x243e6581Dc8a98d98B92265858b322b193555C81',
otherNetworks: [ otherNetworks: [
networkLink(56, 'BNB Chain', 'cWEURC', '0x50b073d0D1D2f002745cb9FC28a057d5be84911c', 'GRU wrapped transport representation on BNB Chain.'), networkLink(56, 'BNB Chain', 'cWEURC', '0x50b073d0D1D2f002745cb9FC28a057d5be84911c', 'GRU wrapped public-network representation on BNB Chain.'),
networkLink(137, 'Polygon', 'cWEURC', '0x3CD9ee18db7ad13616FCC1c83bC6098e03968E66', 'GRU wrapped transport representation on Polygon.'), networkLink(137, 'Polygon', 'cWEURC', '0x3CD9ee18db7ad13616FCC1c83bC6098e03968E66', 'GRU wrapped public-network representation on Polygon.'),
networkLink(100, 'Gnosis', 'cWEURC', '0x25603ae4bff0b71d637b3573d1b6657f5f6d17ef', 'GRU wrapped transport representation on Gnosis.'), networkLink(100, 'Gnosis', 'cWEURC', '0x25603ae4bff0b71d637b3573d1b6657f5f6d17ef', 'GRU wrapped public-network representation on Gnosis.'),
], ],
}, },
{ {
@@ -132,9 +132,9 @@ const GRU_EXPLORER_ENTRIES: GruExplorerEntry[] = [
canonicalForwardVersion: 'v2', canonicalForwardVersion: 'v2',
canonicalForwardAddress: '0x2bAFA83d8fF8BaE9505511998987D0659791605B', canonicalForwardAddress: '0x2bAFA83d8fF8BaE9505511998987D0659791605B',
otherNetworks: [ otherNetworks: [
networkLink(56, 'BNB Chain', 'cWEURT', '0x1ED9E491A5eCd53BeF21962A5FCE24880264F63f', 'GRU wrapped transport representation on BNB Chain.'), networkLink(56, 'BNB Chain', 'cWEURT', '0x1ED9E491A5eCd53BeF21962A5FCE24880264F63f', 'GRU wrapped public-network representation on BNB Chain.'),
networkLink(137, 'Polygon', 'cWEURT', '0xBeF5A0Bcc0E77740c910f197138cdD90F98d2427', 'GRU wrapped transport representation on Polygon.'), networkLink(137, 'Polygon', 'cWEURT', '0xBeF5A0Bcc0E77740c910f197138cdD90F98d2427', 'GRU wrapped public-network representation on Polygon.'),
networkLink(100, 'Gnosis', 'cWEURT', '0x8e54c52d34a684e22865ac9f2d7c27c30561a7b9', 'GRU wrapped transport representation on Gnosis.'), networkLink(100, 'Gnosis', 'cWEURT', '0x8e54c52d34a684e22865ac9f2d7c27c30561a7b9', 'GRU wrapped public-network representation on Gnosis.'),
], ],
}, },
...[ ...[

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import { estimateNativeUsdValue, estimateTokenUsdValue, getNativeAssetDescriptor } from './nativeAssetPricing'
describe('nativeAssetPricing', () => {
it('resolves the chain 138 native asset descriptor', () => {
expect(getNativeAssetDescriptor(138)).toEqual({
symbol: 'ETH',
pricingAddress: '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
})
})
it('estimates USD values from wei using the live asset price', () => {
expect(estimateNativeUsdValue('970000000000000', 2490)).toBe('2.4153')
expect(estimateNativeUsdValue('1000000000000000000', 2490)).toBe('2490')
})
it('returns undefined when pricing inputs are unavailable', () => {
expect(estimateNativeUsdValue(undefined, 2490)).toBeUndefined()
expect(estimateNativeUsdValue('970000000000000', undefined)).toBeUndefined()
expect(estimateNativeUsdValue('not-a-number', 2490)).toBeUndefined()
})
it('estimates token USD values using token decimals', () => {
expect(estimateTokenUsdValue('1000000', 6, 1)).toBe('1')
expect(estimateTokenUsdValue('250000000', 8, 2)).toBe('5')
})
it('preserves precision for large raw balances', () => {
expect(estimateNativeUsdValue('123456789012345678901234567890', 2316.7203872128002)).toBeTruthy()
})
})

View File

@@ -0,0 +1,99 @@
import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from './tokenAggregation'
interface NativeAssetDescriptor {
symbol: string
pricingAddress: string
}
const NATIVE_ASSET_BY_CHAIN_ID: Record<number, NativeAssetDescriptor> = {
138: {
symbol: 'ETH',
pricingAddress: '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
},
}
export function getNativeAssetDescriptor(chainId: number): NativeAssetDescriptor {
return NATIVE_ASSET_BY_CHAIN_ID[chainId] || { symbol: 'ETH', pricingAddress: NATIVE_ASSET_BY_CHAIN_ID[138].pricingAddress }
}
export async function getNativeAssetMarketSafe(
chainId: number,
): Promise<{ ok: boolean; data: TokenAggregationTokenSnapshot | null }> {
const descriptor = getNativeAssetDescriptor(chainId)
return tokenAggregationApi.getTokenSafe(chainId, descriptor.pricingAddress)
}
export async function getNativeAssetPriceAtSafe(
chainId: number,
timestamp: string,
): Promise<{ ok: boolean; data: Awaited<ReturnType<typeof tokenAggregationApi.getPriceAtSafe>>['data'] }> {
const descriptor = getNativeAssetDescriptor(chainId)
return tokenAggregationApi.getPriceAtSafe(chainId, descriptor.pricingAddress, timestamp)
}
function decimalToScaledInteger(value: number, scale: number): { scaled: bigint; scale: bigint } | null {
if (!Number.isFinite(value)) {
return null
}
const normalized = value.toFixed(scale)
const negative = normalized.startsWith('-')
const unsigned = negative ? normalized.slice(1) : normalized
const [whole, fraction = ''] = unsigned.split('.')
try {
const scaled = BigInt(whole + fraction.padEnd(scale, '0'))
return {
scaled: negative ? -scaled : scaled,
scale: 10n ** BigInt(scale),
}
} catch {
return null
}
}
function formatScaledUsd(
rawAmount: string,
tokenDecimals: number,
priceUsd: number,
priceScale = 8,
outputScale = 6,
): string | undefined {
if (!rawAmount || !Number.isFinite(priceUsd) || tokenDecimals < 0) {
return undefined
}
try {
const amount = BigInt(rawAmount)
const parsedPrice = decimalToScaledInteger(priceUsd, priceScale)
if (!parsedPrice) {
return undefined
}
const numerator = amount * parsedPrice.scaled * (10n ** BigInt(outputScale))
const denominator = (10n ** BigInt(tokenDecimals)) * parsedPrice.scale
const rounded = (numerator + (denominator / 2n)) / denominator
const divisor = 10n ** BigInt(outputScale)
const whole = rounded / divisor
const fraction = (rounded % divisor).toString().padStart(outputScale, '0').replace(/0+$/, '')
return fraction ? `${whole.toString()}.${fraction}` : whole.toString()
} catch {
return undefined
}
}
export function estimateNativeUsdValue(
valueWei: string | null | undefined,
priceUsd: number | undefined,
): string | undefined {
return valueWei && priceUsd != null ? formatScaledUsd(valueWei, 18, priceUsd) : undefined
}
export function estimateTokenUsdValue(
rawAmount: string | null | undefined,
decimals: number,
priceUsd: number | undefined,
): string | undefined {
return rawAmount && priceUsd != null ? formatScaledUsd(rawAmount, decimals, priceUsd) : undefined
}

View File

@@ -0,0 +1,159 @@
import { resolveExplorerApiBase } from '../../../libs/frontend-api-client/api-base'
export interface TokenAggregationMarketSnapshot {
priceUsd?: number
volume24h?: number
liquidityUsd?: number
lastUpdated?: string | null
}
export interface TokenAggregationTokenSnapshot {
chainId: number
address: string
name?: string
symbol?: string
decimals?: number
totalSupply?: string
market?: TokenAggregationMarketSnapshot | null
}
export interface TokenAggregationHistoricalPriceSnapshot {
chainId: number
tokenAddress: string
requestedTimestamp: string
effectiveTimestamp?: string
priceUsd?: number
source?: string
}
interface RawTokenAggregationTokenResponse {
token?: {
chainId?: number | string | null
address?: string | null
name?: string | null
symbol?: string | null
decimals?: number | string | null
totalSupply?: string | null
market?: {
priceUsd?: number | string | null
volume24h?: number | string | null
liquidityUsd?: number | string | null
lastUpdated?: string | null
} | null
} | null
}
interface RawTokenAggregationHistoricalPriceResponse {
chainId?: number | string | null
tokenAddress?: string | null
requestedTimestamp?: string | null
effectiveTimestamp?: string | null
priceUsd?: number | string | null
source?: string | null
}
function toNumber(value: number | string | null | undefined): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value)
if (Number.isFinite(parsed)) return parsed
}
return undefined
}
function normalizeTokenSnapshot(raw: RawTokenAggregationTokenResponse): TokenAggregationTokenSnapshot | null {
const token = raw.token
if (!token?.address) {
return null
}
return {
chainId: toNumber(token.chainId) ?? 138,
address: token.address,
name: token.name || undefined,
symbol: token.symbol || undefined,
decimals: toNumber(token.decimals),
totalSupply: token.totalSupply || undefined,
market: token.market
? {
priceUsd: toNumber(token.market.priceUsd),
volume24h: toNumber(token.market.volume24h),
liquidityUsd: toNumber(token.market.liquidityUsd),
lastUpdated: token.market.lastUpdated || null,
}
: null,
}
}
function normalizeHistoricalPriceSnapshot(
raw: RawTokenAggregationHistoricalPriceResponse,
): TokenAggregationHistoricalPriceSnapshot | null {
if (!raw.tokenAddress || !raw.requestedTimestamp) {
return null
}
return {
chainId: toNumber(raw.chainId) ?? 138,
tokenAddress: raw.tokenAddress,
requestedTimestamp: raw.requestedTimestamp,
effectiveTimestamp: raw.effectiveTimestamp || undefined,
priceUsd: toNumber(raw.priceUsd),
source: raw.source || undefined,
}
}
function getTokenAggregationBase(): string {
return `${resolveExplorerApiBase()}/token-aggregation/api/v1`
}
export const tokenAggregationApi = {
getTokenSafe: async (chainId: number, address: string): Promise<{ ok: boolean; data: TokenAggregationTokenSnapshot | null }> => {
try {
const response = await fetch(`${getTokenAggregationBase()}/tokens/${address}?chainId=${chainId}`)
if (!response.ok) {
return { ok: false, data: null }
}
const raw = (await response.json()) as RawTokenAggregationTokenResponse
return { ok: true, data: normalizeTokenSnapshot(raw) }
} catch {
return { ok: false, data: null }
}
},
getTokensByAddressSafe: async (
chainId: number,
addresses: string[],
): Promise<{ ok: boolean; data: TokenAggregationTokenSnapshot[] }> => {
const uniqueAddresses = [...new Set(addresses.map((address) => address.trim()).filter(Boolean))]
if (uniqueAddresses.length === 0) {
return { ok: true, data: [] }
}
const results = await Promise.all(uniqueAddresses.map((address) => tokenAggregationApi.getTokenSafe(chainId, address)))
const data = results
.filter((result): result is { ok: true; data: TokenAggregationTokenSnapshot | null } => result.ok)
.map((result) => result.data)
.filter((snapshot): snapshot is TokenAggregationTokenSnapshot => Boolean(snapshot?.address))
return { ok: data.length > 0, data }
},
getPriceAtSafe: async (
chainId: number,
address: string,
timestamp: string,
): Promise<{ ok: boolean; data: TokenAggregationHistoricalPriceSnapshot | null }> => {
try {
const response = await fetch(
`${getTokenAggregationBase()}/tokens/${address}/price-at?chainId=${chainId}&timestamp=${encodeURIComponent(timestamp)}`
)
if (!response.ok) {
return { ok: false, data: null }
}
const raw = (await response.json()) as RawTokenAggregationHistoricalPriceResponse
return { ok: true, data: normalizeHistoricalPriceSnapshot(raw) }
} catch {
return { ok: false, data: null }
}
},
}

View File

@@ -20,6 +20,25 @@ describe('tokensApi', () => {
total_supply: '1000', total_supply: '1000',
}), }),
}) })
.mockResolvedValueOnce({
ok: true,
json: async () => ({
token: {
chainId: 138,
address: '0xtoken',
symbol: 'cUSDT',
name: 'Tether USD (Compliant)',
decimals: 6,
totalSupply: '1000',
market: {
priceUsd: 1,
volume24h: 2500,
liquidityUsd: 500000,
lastUpdated: '2026-04-26T01:00:00.000Z',
},
},
}),
})
.mockResolvedValueOnce({ .mockResolvedValueOnce({
ok: true, ok: true,
json: async () => ({ json: async () => ({
@@ -61,6 +80,10 @@ describe('tokensApi', () => {
expect(token.ok).toBe(true) expect(token.ok).toBe(true)
expect(token.data?.symbol).toBe('cUSDT') expect(token.data?.symbol).toBe('cUSDT')
expect(token.data?.exchange_rate).toBe(1)
expect(token.data?.volume_24h).toBe(2500)
expect(token.data?.liquidity_usd).toBe(500000)
expect(token.data?.price_source).toBe('token-aggregation')
expect(holders.data[0].label).toBe('Treasury') expect(holders.data[0].label).toBe('Treasury')
expect(transfers.data[0].token_symbol).toBe('cUSDT') expect(transfers.data[0].token_symbol).toBe('cUSDT')
}) })

View File

@@ -1,6 +1,7 @@
import { fetchBlockscoutJson, normalizeAddressTokenTransfer, type BlockscoutTokenTransfer } from './blockscout' import { fetchBlockscoutJson, normalizeAddressTokenTransfer, type BlockscoutTokenTransfer } from './blockscout'
import { configApi, type TokenListToken } from './config' import { configApi, type TokenListToken } from './config'
import { routesApi, type MissionControlLiquidityPool } from './routes' import { routesApi, type MissionControlLiquidityPool } from './routes'
import { tokenAggregationApi } from './tokenAggregation'
import type { AddressTokenTransfer } from './addresses' import type { AddressTokenTransfer } from './addresses'
export interface TokenProfile { export interface TokenProfile {
@@ -15,6 +16,9 @@ export interface TokenProfile {
icon_url?: string | null icon_url?: string | null
circulating_market_cap?: string | number | null circulating_market_cap?: string | number | null
volume_24h?: string | number | null volume_24h?: string | number | null
liquidity_usd?: string | number | null
market_updated_at?: string | null
price_source?: 'blockscout' | 'token-aggregation' | 'derived'
} }
export interface TokenHolder { export interface TokenHolder {
@@ -45,6 +49,9 @@ function normalizeTokenProfile(raw: {
icon_url?: string | null icon_url?: string | null
circulating_market_cap?: string | number | null circulating_market_cap?: string | number | null
volume_24h?: string | number | null volume_24h?: string | number | null
liquidity_usd?: string | number | null
market_updated_at?: string | null
price_source?: 'blockscout' | 'token-aggregation' | 'derived'
}): TokenProfile { }): TokenProfile {
return { return {
address: raw.address, address: raw.address,
@@ -58,9 +65,68 @@ function normalizeTokenProfile(raw: {
icon_url: raw.icon_url ?? null, icon_url: raw.icon_url ?? null,
circulating_market_cap: raw.circulating_market_cap ?? null, circulating_market_cap: raw.circulating_market_cap ?? null,
volume_24h: raw.volume_24h ?? null, volume_24h: raw.volume_24h ?? null,
liquidity_usd: raw.liquidity_usd ?? null,
market_updated_at: raw.market_updated_at ?? null,
price_source: raw.price_source || 'blockscout',
} }
} }
function computeMarketCap(totalSupply: string | undefined, decimals: number, priceUsd: number | undefined): number | null {
if (!totalSupply || priceUsd == null || !Number.isFinite(priceUsd)) {
return null
}
const supplyNumeric = Number(totalSupply)
if (!Number.isFinite(supplyNumeric) || Math.abs(supplyNumeric) > Number.MAX_SAFE_INTEGER) {
return null
}
const normalizedSupply = supplyNumeric / 10 ** decimals
if (!Number.isFinite(normalizedSupply)) {
return null
}
return normalizedSupply * priceUsd
}
function mergeTokenProfileWithAggregation(
blockscoutToken: TokenProfile | null,
aggregationToken: Awaited<ReturnType<typeof tokenAggregationApi.getTokenSafe>>['data'],
): TokenProfile | null {
if (!blockscoutToken && !aggregationToken) {
return null
}
const priceUsd = aggregationToken?.market?.priceUsd
const merged: TokenProfile = {
address: blockscoutToken?.address || aggregationToken?.address || '',
name: blockscoutToken?.name || aggregationToken?.name,
symbol: blockscoutToken?.symbol || aggregationToken?.symbol,
decimals: blockscoutToken?.decimals || aggregationToken?.decimals || 0,
type: blockscoutToken?.type,
total_supply: blockscoutToken?.total_supply || aggregationToken?.totalSupply,
holders: blockscoutToken?.holders,
exchange_rate: priceUsd ?? blockscoutToken?.exchange_rate ?? null,
icon_url: blockscoutToken?.icon_url ?? null,
circulating_market_cap:
blockscoutToken?.circulating_market_cap ??
computeMarketCap(blockscoutToken?.total_supply || aggregationToken?.totalSupply, blockscoutToken?.decimals || aggregationToken?.decimals || 0, priceUsd),
volume_24h: aggregationToken?.market?.volume24h ?? blockscoutToken?.volume_24h ?? null,
liquidity_usd: aggregationToken?.market?.liquidityUsd ?? blockscoutToken?.liquidity_usd ?? null,
market_updated_at: aggregationToken?.market?.lastUpdated ?? blockscoutToken?.market_updated_at ?? null,
price_source:
priceUsd != null
? 'token-aggregation'
: blockscoutToken?.exchange_rate != null
? 'blockscout'
: blockscoutToken?.circulating_market_cap == null && blockscoutToken?.volume_24h == null && priceUsd == null
? 'derived'
: blockscoutToken?.price_source || 'blockscout',
}
return merged.address ? merged : null
}
function normalizeTokenHolder(raw: { function normalizeTokenHolder(raw: {
address?: { address?: {
hash?: string | null hash?: string | null
@@ -94,7 +160,8 @@ async function getTokenListLookup(): Promise<Map<string, TokenListToken>> {
export const tokensApi = { export const tokensApi = {
getSafe: async (address: string): Promise<{ ok: boolean; data: TokenProfile | null }> => { getSafe: async (address: string): Promise<{ ok: boolean; data: TokenProfile | null }> => {
try { try {
const raw = await fetchBlockscoutJson<{ const [blockscoutResult, aggregationResult] = await Promise.allSettled([
fetchBlockscoutJson<{
address: string address: string
name?: string | null name?: string | null
symbol?: string | null symbol?: string | null
@@ -106,8 +173,17 @@ export const tokensApi = {
icon_url?: string | null icon_url?: string | null
circulating_market_cap?: string | number | null circulating_market_cap?: string | number | null
volume_24h?: string | number | null volume_24h?: string | number | null
}>(`/api/v2/tokens/${address}`) }>(`/api/v2/tokens/${address}`),
return { ok: true, data: normalizeTokenProfile(raw) } tokenAggregationApi.getTokenSafe(138, address),
])
const blockscoutToken =
blockscoutResult.status === 'fulfilled' ? normalizeTokenProfile(blockscoutResult.value) : null
const aggregationToken =
aggregationResult.status === 'fulfilled' && aggregationResult.value.ok ? aggregationResult.value.data : null
const merged = mergeTokenProfileWithAggregation(blockscoutToken, aggregationToken)
return { ok: merged != null, data: merged }
} catch { } catch {
return { ok: false, data: null } return { ok: false, data: null }
} }

View File

@@ -33,7 +33,7 @@ if [ -n "${1:-}" ] && [[ "$1" =~ ^0x[0-9a-fA-F]{64}$ ]]; then
TX_HASH="$1" TX_HASH="$1"
echo "=== Checking Transaction: $TX_HASH ===" echo "=== Checking Transaction: $TX_HASH ==="
echo "" echo ""
echo "Explorer URL: $EXPLORER_URL/tx/$TX_HASH" echo "Explorer URL: $EXPLORER_URL/transactions/$TX_HASH"
echo "" echo ""
# Try to get receipt via RPC # Try to get receipt via RPC
@@ -84,7 +84,7 @@ if [ -n "${1:-}" ] && [[ "$1" =~ ^0x[0-9a-fA-F]{64}$ ]]; then
else else
echo "⚠ Transaction not found in RPC (may be pending or not yet indexed)" echo "⚠ Transaction not found in RPC (may be pending or not yet indexed)"
echo "" echo ""
echo "Check on explorer: $EXPLORER_URL/tx/$TX_HASH" echo "Check on explorer: $EXPLORER_URL/transactions/$TX_HASH"
fi fi
else else
# Check recent transactions for account # Check recent transactions for account
@@ -125,6 +125,6 @@ echo ""
echo "For detailed transaction information, please visit:" echo "For detailed transaction information, please visit:"
echo " Account: $EXPLORER_URL/address/$ACCOUNT" echo " Account: $EXPLORER_URL/address/$ACCOUNT"
if [ -n "${TX_HASH:-}" ]; then if [ -n "${TX_HASH:-}" ]; then
echo " Transaction: $EXPLORER_URL/tx/$TX_HASH" echo " Transaction: $EXPLORER_URL/transactions/$TX_HASH"
fi fi
echo "" echo ""

View File

@@ -6,7 +6,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "=== SolaceScan Tiered Architecture - Deployment & Testing ===" echo "=== DBIS Explorer Tiered Architecture - Deployment & Testing ==="
echo "" echo ""
# Colors # Colors

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Deploy the current Next.js standalone frontend to VMID 5000. # Deploy the current Next.js standalone frontend to VMID 5000.
# This is the canonical deployment path for the current SolaceScan frontend. # This is the canonical deployment path for the current DBIS Explorer frontend.
# It builds the local frontend, uploads the standalone bundle, installs a systemd # It builds the local frontend, uploads the standalone bundle, installs a systemd
# service, and starts the Node server on 127.0.0.1:3000 inside the container. # service, and starts the Node server on 127.0.0.1:3000 inside the container.
@@ -9,6 +9,7 @@ set -euo pipefail
VMID="${VMID:-5000}" VMID="${VMID:-5000}"
FRONTEND_PORT="${FRONTEND_PORT:-3000}" FRONTEND_PORT="${FRONTEND_PORT:-3000}"
FORCE_REMOTE_PCT="${FORCE_REMOTE_PCT:-0}"
SERVICE_NAME="solacescanscout-frontend" SERVICE_NAME="solacescanscout-frontend"
APP_ROOT="/opt/solacescanscout/frontend" APP_ROOT="/opt/solacescanscout/frontend"
PROXMOX_R630_02="${PROXMOX_HOST_R630_02:-192.168.11.12}" PROXMOX_R630_02="${PROXMOX_HOST_R630_02:-192.168.11.12}"
@@ -23,6 +24,7 @@ RELEASE_ID="$(date +%Y%m%d_%H%M%S)"
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
ARCHIVE_NAME="solacescanscout-next-${RELEASE_ID}.tar" ARCHIVE_NAME="solacescanscout-next-${RELEASE_ID}.tar"
BUILD_LOCK_DIR="${FRONTEND_ROOT}/.next-build-lock" BUILD_LOCK_DIR="${FRONTEND_ROOT}/.next-build-lock"
STANDALONE_ROOT="${FRONTEND_ROOT}/.next/standalone"
STATIC_SYNC_FILES=( STATIC_SYNC_FILES=(
"index.html" "index.html"
"docs.html" "docs.html"
@@ -53,7 +55,7 @@ push_into_vmid() {
local destination_path="$2" local destination_path="$2"
local perms="${3:-0644}" local perms="${3:-0644}"
if [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then if [[ "$FORCE_REMOTE_PCT" != "1" ]] && [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
install -D -m "$perms" "$source_path" "$destination_path" install -D -m "$perms" "$source_path" "$destination_path"
elif command -v pct >/dev/null 2>&1; then elif command -v pct >/dev/null 2>&1; then
pct push "$VMID" "$source_path" "$destination_path" --perms "$perms" pct push "$VMID" "$source_path" "$destination_path" --perms "$perms"
@@ -68,7 +70,7 @@ push_into_vmid() {
run_in_vmid() { run_in_vmid() {
local command="$1" local command="$1"
if [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then if [[ "$FORCE_REMOTE_PCT" != "1" ]] && [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
bash -lc "$command" bash -lc "$command"
elif command -v pct >/dev/null 2>&1; then elif command -v pct >/dev/null 2>&1; then
pct exec "$VMID" -- bash -lc "$command" pct exec "$VMID" -- bash -lc "$command"
@@ -79,7 +81,7 @@ run_in_vmid() {
} }
echo "==========================================" echo "=========================================="
echo "Deploying Next SolaceScan Frontend" echo "Deploying Next DBIS Explorer Frontend"
echo "==========================================" echo "=========================================="
echo "VMID: $VMID" echo "VMID: $VMID"
echo "Frontend root: $FRONTEND_ROOT" echo "Frontend root: $FRONTEND_ROOT"
@@ -109,21 +111,42 @@ if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
echo "" echo ""
fi fi
if [[ ! -f "${FRONTEND_ROOT}/.next/standalone/server.js" ]]; then APP_RELATIVE_DIR="."
APP_SERVER_PATH="${STANDALONE_ROOT}/server.js"
if [[ ! -f "${APP_SERVER_PATH}" ]]; then
ALT_SERVER_PATH="$(find "${STANDALONE_ROOT}" \
-path '*/node_modules' -prune -o \
-path '*/server.js' -print | head -n 1 || true)"
if [[ -n "${ALT_SERVER_PATH}" ]]; then
APP_SERVER_PATH="${ALT_SERVER_PATH}"
APP_RELATIVE_DIR="$(dirname "${ALT_SERVER_PATH#${STANDALONE_ROOT}/}")"
fi
fi
if [[ ! -f "${APP_SERVER_PATH}" ]]; then
echo "Missing standalone server build. Run \`npm run build\` in ${FRONTEND_ROOT} first." >&2 echo "Missing standalone server build. Run \`npm run build\` in ${FRONTEND_ROOT} first." >&2
exit 1 exit 1
fi fi
STAGE_DIR="${TMP_DIR}/stage" STAGE_DIR="${TMP_DIR}/stage"
mkdir -p "${STAGE_DIR}/.next" APP_STAGE_DIR="${STAGE_DIR}"
cp -R "${FRONTEND_ROOT}/.next/standalone/." "$STAGE_DIR/" if [[ "${APP_RELATIVE_DIR}" != "." ]]; then
cp -R "${FRONTEND_ROOT}/.next/static" "${STAGE_DIR}/.next/static" APP_STAGE_DIR="${STAGE_DIR}/${APP_RELATIVE_DIR}"
cp -R "${FRONTEND_ROOT}/public" "${STAGE_DIR}/public" fi
mkdir -p "${APP_STAGE_DIR}/.next"
cp -R "${STANDALONE_ROOT}/." "$STAGE_DIR/"
cp -R "${FRONTEND_ROOT}/.next/static" "${APP_STAGE_DIR}/.next/static"
cp -R "${FRONTEND_ROOT}/public" "${APP_STAGE_DIR}/public"
tar -C "$STAGE_DIR" -cf "${TMP_DIR}/${ARCHIVE_NAME}" . tar -C "$STAGE_DIR" -cf "${TMP_DIR}/${ARCHIVE_NAME}" .
cp "$SERVICE_TEMPLATE" "${TMP_DIR}/${SERVICE_NAME}.service" cp "$SERVICE_TEMPLATE" "${TMP_DIR}/${SERVICE_NAME}.service"
sed -i "s|/opt/solacescanscout/frontend/current|${APP_ROOT}/current|g" "${TMP_DIR}/${SERVICE_NAME}.service" sed -i "s|/opt/solacescanscout/frontend/current|${APP_ROOT}/current|g" "${TMP_DIR}/${SERVICE_NAME}.service"
sed -i "s|Environment=PORT=3000|Environment=PORT=${FRONTEND_PORT}|g" "${TMP_DIR}/${SERVICE_NAME}.service" sed -i "s|Environment=PORT=3000|Environment=PORT=${FRONTEND_PORT}|g" "${TMP_DIR}/${SERVICE_NAME}.service"
if [[ "${APP_RELATIVE_DIR}" != "." ]]; then
sed -i "s|WorkingDirectory=${APP_ROOT}/current|WorkingDirectory=${APP_ROOT}/current/${APP_RELATIVE_DIR}|g" "${TMP_DIR}/${SERVICE_NAME}.service"
sed -i "s|ExecStart=/usr/bin/node ${APP_ROOT}/current/server.js|ExecStart=/usr/bin/node ${APP_ROOT}/current/${APP_RELATIVE_DIR}/server.js|g" "${TMP_DIR}/${SERVICE_NAME}.service"
fi
cat > "${TMP_DIR}/install-next-frontend.sh" <<EOF cat > "${TMP_DIR}/install-next-frontend.sh" <<EOF
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -152,10 +175,10 @@ for attempt in \$(seq 1 45); do
sleep 1 sleep 1
done done
if ! grep -qiE "SolaceScan|Chain 138 Explorer by DBIS" /tmp/\${SERVICE_NAME}-health.out; then if ! grep -qiE "DBIS Explorer|Chain 138 Explorer by DBIS" /tmp/\${SERVICE_NAME}-health.out; then
systemctl status "\${SERVICE_NAME}.service" --no-pager || true systemctl status "\${SERVICE_NAME}.service" --no-pager || true
journalctl -u "\${SERVICE_NAME}.service" -n 50 --no-pager || true journalctl -u "\${SERVICE_NAME}.service" -n 50 --no-pager || true
echo "Frontend health check did not find the expected SolaceScan marker." >&2 echo "Frontend health check did not find the expected DBIS Explorer marker." >&2
exit 1 exit 1
fi fi
@@ -188,7 +211,7 @@ echo ""
echo "== Verification ==" echo "== Verification =="
run_in_vmid "systemctl is-active ${SERVICE_NAME}.service" run_in_vmid "systemctl is-active ${SERVICE_NAME}.service"
run_in_vmid "curl -fsS --max-time 5 http://127.0.0.1:${FRONTEND_PORT}/ | grep -qiE 'SolaceScan|Chain 138 Explorer by DBIS'" run_in_vmid "curl -fsS --max-time 5 http://127.0.0.1:${FRONTEND_PORT}/ | grep -qiE 'DBIS Explorer|Chain 138 Explorer by DBIS'"
echo "Service ${SERVICE_NAME} is running on 127.0.0.1:${FRONTEND_PORT}" echo "Service ${SERVICE_NAME} is running on 127.0.0.1:${FRONTEND_PORT}"
echo "" echo ""
echo "Nginx follow-up:" echo "Nginx follow-up:"

View File

@@ -6,7 +6,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "=== SolaceScan Tiered Architecture Deployment ===" echo "=== DBIS Explorer Tiered Architecture Deployment ==="
echo "" echo ""
# Step 1: Verify prerequisites # Step 1: Verify prerequisites

View File

@@ -30,7 +30,7 @@ function collectUnexpectedConsoleErrors(page: Page, allowlist: RegExp[] = []) {
test.describe('Explorer Frontend - Route Coverage', () => { test.describe('Explorer Frontend - Route Coverage', () => {
for (const route of [ for (const route of [
{ path: '/', heading: /SolaceScan/i }, { path: '/', heading: /DBIS Explorer/i },
{ path: '/blocks', heading: /^Blocks$/i }, { path: '/blocks', heading: /^Blocks$/i },
{ path: '/transactions', heading: /^Transactions$/i }, { path: '/transactions', heading: /^Transactions$/i },
{ path: '/addresses', heading: /^Addresses$/i }, { path: '/addresses', heading: /^Addresses$/i },

View File

@@ -36,7 +36,7 @@ test.describe('Explorer full-stack smoke', () => {
}) })
for (const route of [ for (const route of [
{ path: '/', heading: /SolaceScan/i, name: 'home' }, { path: '/', heading: /DBIS Explorer/i, name: 'home' },
{ path: '/blocks', heading: /^Blocks$/i, name: 'blocks' }, { path: '/blocks', heading: /^Blocks$/i, name: 'blocks' },
{ path: '/transactions', heading: /^Transactions$/i, name: 'transactions' }, { path: '/transactions', heading: /^Transactions$/i, name: 'transactions' },
{ path: '/addresses', heading: /^Addresses$/i, name: 'addresses' }, { path: '/addresses', heading: /^Addresses$/i, name: 'addresses' },

View File

@@ -141,7 +141,7 @@ echo "=== 2. Frontend Content Tests ==="
CONTENT_URL="$BASE_URL:80" CONTENT_URL="$BASE_URL:80"
# Test homepage content # Test homepage content
test_content "$CONTENT_URL" "SolaceScan" "Homepage contains SolaceScan title" test_content "$CONTENT_URL" "DBIS Explorer" "Homepage contains DBIS Explorer title"
# Test explorer branding # Test explorer branding
test_content "$CONTENT_URL" "Explorer" "Homepage contains explorer branding" test_content "$CONTENT_URL" "Explorer" "Homepage contains explorer branding"
@@ -347,12 +347,12 @@ PATH_CURL_EXTRA=""
# SPA serves index.html for all paths - verify path-based routing is present # SPA serves index.html for all paths - verify path-based routing is present
test_content "$PATH_TEST_BASE/address/0x99b3511a2d315a497c8112c1fdd8d508d4b1e506" "fromPath" "Path-based routing code present (address URL)" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/address/0x99b3511a2d315a497c8112c1fdd8d508d4b1e506" "fromPath" "Path-based routing code present (address URL)" "$PATH_CURL_EXTRA"
test_content "$PATH_TEST_BASE/address/0x99b3511a2d315a497c8112c1fdd8d508d4b1e506" "SolaceScan" "Address path serves SPA shell" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/address/0x99b3511a2d315a497c8112c1fdd8d508d4b1e506" "DBIS Explorer" "Address path serves SPA shell" "$PATH_CURL_EXTRA"
test_content "$PATH_TEST_BASE/blocks" "SolaceScan" "Blocks path serves SPA" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/blocks" "DBIS Explorer" "Blocks path serves SPA" "$PATH_CURL_EXTRA"
test_content "$PATH_TEST_BASE/transactions" "SolaceScan" "Transactions path serves SPA" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/transactions" "DBIS Explorer" "Transactions path serves SPA" "$PATH_CURL_EXTRA"
test_content "$PATH_TEST_BASE/bridge" "SolaceScan" "Bridge path serves SPA" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/bridge" "DBIS Explorer" "Bridge path serves SPA" "$PATH_CURL_EXTRA"
test_content "$PATH_TEST_BASE/weth" "SolaceScan" "WETH path serves SPA" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/weth" "DBIS Explorer" "WETH path serves SPA" "$PATH_CURL_EXTRA"
test_content "$PATH_TEST_BASE/watchlist" "SolaceScan" "Watchlist path serves SPA" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/watchlist" "DBIS Explorer" "Watchlist path serves SPA" "$PATH_CURL_EXTRA"
# Verify nav links exist in HTML (use same base) # Verify nav links exist in HTML (use same base)
test_content "$PATH_TEST_BASE/" "#/home" "Home nav link present" "$PATH_CURL_EXTRA" test_content "$PATH_TEST_BASE/" "#/home" "Home nav link present" "$PATH_CURL_EXTRA"

View File

@@ -388,7 +388,7 @@ sleep 2
if [ -f /var/www/html/index.html ]; then if [ -f /var/www/html/index.html ]; then
echo "✅ Custom frontend file exists" echo "✅ Custom frontend file exists"
if grep -qiE "SolaceScan|Chain 138 Explorer by DBIS" /var/www/html/index.html; then if grep -qiE "DBIS Explorer|Chain 138 Explorer by DBIS" /var/www/html/index.html; then
echo "✅ Custom frontend content verified" echo "✅ Custom frontend content verified"
else else
echo "⚠️ Frontend file exists but may not be the custom one" echo "⚠️ Frontend file exists but may not be the custom one"
@@ -405,7 +405,7 @@ fi
echo "" echo ""
echo "Testing HTTP endpoint:" echo "Testing HTTP endpoint:"
HTTP_RESPONSE=$(curl -s --max-time 5 http://localhost/ 2>/dev/null | head -5) || true HTTP_RESPONSE=$(curl -s --max-time 5 http://localhost/ 2>/dev/null | head -5) || true
if echo "$HTTP_RESPONSE" | grep -qiE "SolaceScan|Chain 138 Explorer by DBIS|<!DOCTYPE html"; then if echo "$HTTP_RESPONSE" | grep -qiE "DBIS Explorer|Chain 138 Explorer by DBIS|<!DOCTYPE html"; then
echo "✅ Custom frontend is accessible via HTTP" echo "✅ Custom frontend is accessible via HTTP"
else else
echo "⚠️ Frontend may not be accessible (check if file exists)" echo "⚠️ Frontend may not be accessible (check if file exists)"

View File

@@ -6,7 +6,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "=== SolaceScan Tiered Architecture Setup ===" echo "=== DBIS Explorer Tiered Architecture Setup ==="
echo "" echo ""
# Step 1: Run database migration # Step 1: Run database migration

View File

@@ -271,7 +271,7 @@ if echo "$SEND_TX" | grep -qE "transactionHash"; then
log_info " Recipient: $DEPLOYER" log_info " Recipient: $DEPLOYER"
log_info "" log_info ""
log_info "You can monitor the transaction at:" log_info "You can monitor the transaction at:"
log_info " https://explorer.d-bis.org/tx/$TX_HASH" log_info " https://explorer.d-bis.org/transactions/$TX_HASH"
log_info "" log_info ""
log_success "Process completed successfully!" log_success "Process completed successfully!"
log_info "" log_info ""