diff --git a/backend/api/rest/config.go b/backend/api/rest/config.go index 220151c..47eb201 100644 --- a/backend/api/rest/config.go +++ b/backend/api/rest/config.go @@ -11,6 +11,9 @@ var dualChainNetworksJSON []byte //go:embed config/metamask/DUAL_CHAIN_TOKEN_LIST.tokenlist.json var dualChainTokenListJSON []byte +//go:embed config/metamask/CHAIN138_RPC_CAPABILITIES.json +var chain138RPCCapabilitiesJSON []byte + // handleConfigNetworks serves GET /api/config/networks (Chain 138 + Ethereum Mainnet params for wallet_addEthereumChain). func (s *Server) handleConfigNetworks(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { @@ -34,3 +37,15 @@ func (s *Server) handleConfigTokenList(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "public, max-age=3600") w.Write(dualChainTokenListJSON) } + +// handleConfigCapabilities serves GET /api/config/capabilities (Chain 138 wallet/RPC capability matrix). +func (s *Server) handleConfigCapabilities(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.Header().Set("Allow", "GET") + writeMethodNotAllowed(w) + return + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "public, max-age=900") + w.Write(chain138RPCCapabilitiesJSON) +} diff --git a/backend/api/rest/config/metamask/CHAIN138_RPC_CAPABILITIES.json b/backend/api/rest/config/metamask/CHAIN138_RPC_CAPABILITIES.json new file mode 100644 index 0000000..b133697 --- /dev/null +++ b/backend/api/rest/config/metamask/CHAIN138_RPC_CAPABILITIES.json @@ -0,0 +1,62 @@ +{ + "name": "Chain 138 RPC Capabilities", + "version": { + "major": 1, + "minor": 0, + "patch": 0 + }, + "generatedBy": "SolaceScanScout", + "timestamp": "2026-03-28T00:00:00Z", + "chainId": 138, + "chainName": "DeFi Oracle Meta Mainnet", + "rpcUrl": "https://rpc-http-pub.d-bis.org", + "explorerUrl": "https://explorer.d-bis.org", + "explorerApiUrl": "https://explorer.d-bis.org/api/v2", + "walletSupport": { + "walletAddEthereumChain": true, + "walletWatchAsset": true, + "notes": [ + "MetaMask primarily relies on JSON-RPC correctness for balances, gas estimation, calls, and transaction submission.", + "Explorer-served network metadata and token list metadata complement wallet UX but do not replace RPC method support." + ] + }, + "http": { + "supportedMethods": [ + "web3_clientVersion", + "net_version", + "eth_chainId", + "eth_blockNumber", + "eth_syncing", + "eth_gasPrice", + "eth_feeHistory", + "eth_estimateGas", + "eth_getCode" + ], + "unsupportedMethods": [ + "eth_maxPriorityFeePerGas" + ], + "notes": [ + "eth_feeHistory is available for wallet fee estimation.", + "eth_maxPriorityFeePerGas is currently not exposed on the public RPC, so some wallets will fall back to simpler fee heuristics." + ] + }, + "tracing": { + "supportedMethods": [ + "trace_block", + "trace_replayBlockTransactions" + ], + "unsupportedMethods": [ + "debug_traceBlockByNumber" + ], + "notes": [ + "TRACE support is enabled for explorer-grade indexing and internal transaction analysis.", + "Debug tracing is intentionally not enabled on the public RPC tier." + ] + }, + "ws": { + "notes": [ + "Use the dedicated public WebSocket endpoint for subscriptions and live wallet/provider events.", + "HTTP and WebSocket method coverage should be treated as separate operational surfaces." + ] + } +} diff --git a/backend/api/rest/config_test.go b/backend/api/rest/config_test.go index 4d5c921..4212729 100644 --- a/backend/api/rest/config_test.go +++ b/backend/api/rest/config_test.go @@ -46,6 +46,29 @@ type testTokenList struct { } `json:"tokens"` } +type testCapabilitiesCatalog struct { + Name string `json:"name"` + GeneratedBy string `json:"generatedBy"` + ChainID int `json:"chainId"` + ChainName string `json:"chainName"` + RPCURL string `json:"rpcUrl"` + HTTP struct { + SupportedMethods []string `json:"supportedMethods"` + UnsupportedMethods []string `json:"unsupportedMethods"` + Notes []string `json:"notes"` + } `json:"http"` + Tracing struct { + SupportedMethods []string `json:"supportedMethods"` + UnsupportedMethods []string `json:"unsupportedMethods"` + Notes []string `json:"notes"` + } `json:"tracing"` + WalletSupport struct { + WalletAddEthereumChain bool `json:"walletAddEthereumChain"` + WalletWatchAsset bool `json:"walletWatchAsset"` + Notes []string `json:"notes"` + } `json:"walletSupport"` +} + func setupConfigHandler() http.Handler { server := NewServer(nil, 138) mux := http.NewServeMux() @@ -53,6 +76,15 @@ func setupConfigHandler() http.Handler { return server.addMiddleware(mux) } +func containsString(items []string, want string) bool { + for _, item := range items { + if item == want { + return true + } + } + return false +} + func TestConfigNetworksEndpointProvidesWalletMetadata(t *testing.T) { handler := setupConfigHandler() req := httptest.NewRequest(http.MethodGet, "/api/config/networks", nil) @@ -148,6 +180,38 @@ func TestConfigTokenListEndpointProvidesOptionalMetadata(t *testing.T) { } } +func TestConfigCapabilitiesEndpointProvidesRPCCapabilityMatrix(t *testing.T) { + handler := setupConfigHandler() + req := httptest.NewRequest(http.MethodGet, "/api/config/capabilities", nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + var payload testCapabilitiesCatalog + if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil { + t.Fatalf("failed to parse capabilities payload: %v", err) + } + + if payload.ChainID != 138 || payload.ChainName == "" || payload.RPCURL == "" || payload.GeneratedBy == "" { + t.Fatal("expected populated chain-level capability metadata") + } + if !payload.WalletSupport.WalletAddEthereumChain || !payload.WalletSupport.WalletWatchAsset { + t.Fatal("expected wallet support flags to be true") + } + if !containsString(payload.HTTP.SupportedMethods, "eth_feeHistory") { + t.Fatal("expected eth_feeHistory support to be documented") + } + if !containsString(payload.HTTP.UnsupportedMethods, "eth_maxPriorityFeePerGas") { + t.Fatal("expected missing eth_maxPriorityFeePerGas support to be documented") + } + if !containsString(payload.Tracing.SupportedMethods, "trace_block") { + t.Fatal("expected trace_block support to be documented") + } +} + func TestConfigEndpointsSupportOptionsPreflight(t *testing.T) { handler := setupConfigHandler() req := httptest.NewRequest(http.MethodOptions, "/api/config/token-list", nil) diff --git a/backend/api/rest/routes.go b/backend/api/rest/routes.go index 8e1220f..5c38ed0 100644 --- a/backend/api/rest/routes.go +++ b/backend/api/rest/routes.go @@ -35,6 +35,7 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) { // MetaMask / dual-chain config (Chain 138 + Ethereum Mainnet) mux.HandleFunc("/api/config/networks", s.handleConfigNetworks) mux.HandleFunc("/api/config/token-list", s.handleConfigTokenList) + mux.HandleFunc("/api/config/capabilities", s.handleConfigCapabilities) // Feature flags endpoint mux.HandleFunc("/api/v1/features", s.handleFeatures) diff --git a/docs/EXPLORER_API_ACCESS.md b/docs/EXPLORER_API_ACCESS.md index 5346619..fd0b3f4 100644 --- a/docs/EXPLORER_API_ACCESS.md +++ b/docs/EXPLORER_API_ACCESS.md @@ -41,6 +41,45 @@ BASE_URL=http://127.0.0.1:3000 npm run smoke:routes - `smoke:routes` performs a lightweight route and content sweep against a running frontend instance. - `npm run dev` now binds to `127.0.0.1` by default, but still honors `HOST` and `PORT` overrides. +### RPC capability matrix for wallets and explorer services + +The explorer now publishes a wallet-facing capability catalog at: + +```text +https://explorer.d-bis.org/api/config/capabilities +``` + +That endpoint is the current source of truth for what the public Chain 138 RPC tier supports for: + +- MetaMask and other wallet basics +- fee-estimation compatibility +- explorer tracing compatibility +- known public-RPC omissions + +Current public-RPC summary: + +| Area | Status | Notes | +|------|--------|-------| +| `eth_chainId` / `eth_blockNumber` / `eth_syncing` | Supported | Core wallet/provider compatibility is present. | +| `eth_gasPrice` | Supported | Basic fee quote path works. | +| `eth_feeHistory` | Supported | Modern wallet fee estimation can use it. | +| `eth_estimateGas` / `eth_getCode` | Supported | Required for normal wallet and app flows. | +| `eth_maxPriorityFeePerGas` | Not supported on public RPC | Wallets may fall back to simpler heuristics. | +| `trace_block` / `trace_replayBlockTransactions` | Supported | Explorer/indexer tracing works on the public tier. | +| `debug_traceBlockByNumber` | Not enabled on public RPC | Intentional for the public tier; do not assume debug tracing is available. | + +MetaMask behavior to keep in mind: + +- MetaMask primarily depends on JSON-RPC correctness, not explorer richness. +- `wallet_addEthereumChain` and `wallet_watchAsset` help wallet UX, but they do not replace missing RPC methods. +- If `eth_maxPriorityFeePerGas` is unavailable, MetaMask can still work, but fee UX may be less polished than on top-tier public networks. + +You can verify the published matrix against live infrastructure with: + +```bash +bash scripts/verify/check-chain138-rpc-health.sh +``` + --- ## CSP blocks eval / “script-src blocked” diff --git a/frontend/src/components/wallet/AddToMetaMask.tsx b/frontend/src/components/wallet/AddToMetaMask.tsx index ce9b7ff..f81c162 100644 --- a/frontend/src/components/wallet/AddToMetaMask.tsx +++ b/frontend/src/components/wallet/AddToMetaMask.tsx @@ -52,6 +52,30 @@ type TokenListCatalog = { tokens?: TokenListToken[] } +type CapabilitiesCatalog = { + name?: string + chainId?: number + chainName?: string + rpcUrl?: string + explorerApiUrl?: string + generatedBy?: string + walletSupport?: { + walletAddEthereumChain?: boolean + walletWatchAsset?: boolean + notes?: string[] + } + http?: { + supportedMethods?: string[] + unsupportedMethods?: string[] + notes?: string[] + } + tracing?: { + supportedMethods?: string[] + unsupportedMethods?: string[] + notes?: string[] + } +} + type EthereumProvider = { request: (args: { method: string; params?: unknown[] }) => Promise } @@ -107,6 +131,7 @@ export function AddToMetaMask() { const [error, setError] = useState(null) const [networks, setNetworks] = useState(null) const [tokenList, setTokenList] = useState(null) + const [capabilities, setCapabilities] = useState(null) const ethereum = typeof window !== 'undefined' ? (window as unknown as { ethereum?: EthereumProvider }).ethereum @@ -115,29 +140,34 @@ export function AddToMetaMask() { const apiBase = getApiBase().replace(/\/$/, '') const tokenListUrl = `${apiBase}/api/config/token-list` const networksUrl = `${apiBase}/api/config/networks` + const capabilitiesUrl = `${apiBase}/api/config/capabilities` useEffect(() => { let active = true async function loadCatalogs() { try { - const [networksResponse, tokenListResponse] = await Promise.all([ + const [networksResponse, tokenListResponse, capabilitiesResponse] = await Promise.all([ fetch(networksUrl), fetch(tokenListUrl), + fetch(capabilitiesUrl), ]) - const [networksJson, tokenListJson] = await Promise.all([ + const [networksJson, tokenListJson, capabilitiesJson] = await Promise.all([ networksResponse.ok ? networksResponse.json() : null, tokenListResponse.ok ? tokenListResponse.json() : null, + capabilitiesResponse.ok ? capabilitiesResponse.json() : null, ]) if (!active) return setNetworks(networksJson) setTokenList(tokenListJson) + setCapabilities(capabilitiesJson) } catch { if (!active) return setNetworks(null) setTokenList(null) + setCapabilities(null) } } @@ -146,7 +176,7 @@ export function AddToMetaMask() { return () => { active = false } - }, [networksUrl, tokenListUrl]) + }, [capabilitiesUrl, networksUrl, tokenListUrl]) const chains = useMemo(() => { const chainMap = new Map() @@ -246,6 +276,9 @@ export function AddToMetaMask() { const tokenCount138 = (tokenList?.tokens || []).filter((token) => token.chainId === 138).length const metadataKeywordString = (tokenList?.keywords || []).join(', ') + const supportedHTTPMethods = capabilities?.http?.supportedMethods || [] + const unsupportedHTTPMethods = capabilities?.http?.unsupportedMethods || [] + const supportedTraceMethods = capabilities?.tracing?.supportedMethods || [] return (
@@ -301,6 +334,18 @@ export function AddToMetaMask() {
+
+

Capabilities URL

+ {capabilitiesUrl} +
+ + + Open JSON + +
+

Token list URL

{tokenListUrl} @@ -316,6 +361,42 @@ export function AddToMetaMask() {
+
+
Chain 138 RPC capabilities
+

+ This capability matrix documents what public RPC methods wallets can rely on today, what tracing the explorer + can use, and where MetaMask will still fall back because the public node does not expose every optional fee method. +

+
+

+ RPC endpoint:{' '} + + {capabilities?.rpcUrl || 'using published explorer fallback'} + +

+

+ HTTP methods: {supportedHTTPMethods.length > 0 ? supportedHTTPMethods.join(', ') : 'metadata unavailable'} +

+

+ Missing wallet-facing methods:{' '} + {unsupportedHTTPMethods.length > 0 ? unsupportedHTTPMethods.join(', ') : 'none listed'} +

+

+ Trace methods: {supportedTraceMethods.length > 0 ? supportedTraceMethods.join(', ') : 'metadata unavailable'} +

+ {capabilities?.walletSupport?.notes?.map((note) => ( +

+ {note} +

+ ))} + {capabilities?.http?.notes?.map((note) => ( +

+ {note} +

+ ))} +
+
+
Featured Chain 138 tokens