Publish Chain 138 RPC capability metadata
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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”
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user