Publish Chain 138 RPC capability metadata

This commit is contained in:
defiQUG
2026-03-28 15:56:42 -07:00
parent ff8d94383c
commit 630021c043
6 changed files with 265 additions and 3 deletions

View File

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

View File

@@ -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."
]
}
}

View File

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

View File

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

View File

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

View File

@@ -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<unknown>
}
@@ -107,6 +131,7 @@ export function AddToMetaMask() {
const [error, setError] = useState<string | null>(null)
const [networks, setNetworks] = useState<NetworksCatalog | null>(null)
const [tokenList, setTokenList] = useState<TokenListCatalog | null>(null)
const [capabilities, setCapabilities] = useState<CapabilitiesCatalog | null>(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<number, WalletChain>()
@@ -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 (
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
@@ -301,6 +334,18 @@ export function AddToMetaMask() {
</a>
</div>
</div>
<div>
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Capabilities URL</p>
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{capabilitiesUrl}</code>
<div className="mt-2 flex flex-wrap gap-2">
<button type="button" onClick={() => copyText(capabilitiesUrl, 'capabilities URL')} className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
Copy URL
</button>
<a href={capabilitiesUrl} target="_blank" rel="noopener noreferrer" className="rounded bg-gray-100 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-200 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-700">
Open JSON
</a>
</div>
</div>
<div>
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Token list URL</p>
<code className="block break-all rounded bg-gray-100 p-2 text-xs dark:bg-gray-900">{tokenListUrl}</code>
@@ -316,6 +361,42 @@ export function AddToMetaMask() {
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-700">
<div className="text-sm font-semibold text-gray-900 dark:text-white">Chain 138 RPC capabilities</div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
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.
</p>
<div className="mt-4 space-y-3 text-sm text-gray-600 dark:text-gray-400">
<p>
RPC endpoint:{' '}
<span className="font-medium text-gray-900 dark:text-white">
{capabilities?.rpcUrl || 'using published explorer fallback'}
</span>
</p>
<p>
HTTP methods: {supportedHTTPMethods.length > 0 ? supportedHTTPMethods.join(', ') : 'metadata unavailable'}
</p>
<p>
Missing wallet-facing methods:{' '}
{unsupportedHTTPMethods.length > 0 ? unsupportedHTTPMethods.join(', ') : 'none listed'}
</p>
<p>
Trace methods: {supportedTraceMethods.length > 0 ? supportedTraceMethods.join(', ') : 'metadata unavailable'}
</p>
{capabilities?.walletSupport?.notes?.map((note) => (
<p key={note} className="text-xs">
{note}
</p>
))}
{capabilities?.http?.notes?.map((note) => (
<p key={note} className="text-xs">
{note}
</p>
))}
</div>
</div>
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-700">
<div className="text-sm font-semibold text-gray-900 dark:text-white">Featured Chain 138 tokens</div>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">