- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
316 lines
9.8 KiB
Go
316 lines
9.8 KiB
Go
package rest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
type testNetworksCatalog struct {
|
|
Name string `json:"name"`
|
|
DefaultChainID int `json:"defaultChainId"`
|
|
ExplorerURL string `json:"explorerUrl"`
|
|
TokenListURL string `json:"tokenListUrl"`
|
|
GeneratedBy string `json:"generatedBy"`
|
|
Chains []struct {
|
|
ChainID string `json:"chainId"`
|
|
ChainIDDecimal int `json:"chainIdDecimal"`
|
|
ChainName string `json:"chainName"`
|
|
ShortName string `json:"shortName"`
|
|
RPCURLs []string `json:"rpcUrls"`
|
|
BlockExplorerURL []string `json:"blockExplorerUrls"`
|
|
InfoURL string `json:"infoURL"`
|
|
ExplorerAPIURL string `json:"explorerApiUrl"`
|
|
Testnet bool `json:"testnet"`
|
|
} `json:"chains"`
|
|
}
|
|
|
|
type testTokenList struct {
|
|
Name string `json:"name"`
|
|
Keywords []string
|
|
Extensions struct {
|
|
DefaultChainID int `json:"defaultChainId"`
|
|
ExplorerURL string `json:"explorerUrl"`
|
|
NetworksConfigURL string `json:"networksConfigUrl"`
|
|
} `json:"extensions"`
|
|
Tokens []struct {
|
|
ChainID int `json:"chainId"`
|
|
Address string `json:"address"`
|
|
Symbol string `json:"symbol"`
|
|
Decimals int `json:"decimals"`
|
|
Extensions struct {
|
|
UnitOfAccount string `json:"unitOfAccount"`
|
|
UnitDescription string `json:"unitDescription"`
|
|
} `json:"extensions"`
|
|
} `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()
|
|
server.SetupRoutes(mux)
|
|
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)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
|
|
t.Fatal("expected CORS header on config endpoint")
|
|
}
|
|
|
|
var payload testNetworksCatalog
|
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("failed to parse networks payload: %v", err)
|
|
}
|
|
|
|
if payload.DefaultChainID != 138 {
|
|
t.Fatalf("expected defaultChainId 138, got %d", payload.DefaultChainID)
|
|
}
|
|
if payload.ExplorerURL == "" || payload.TokenListURL == "" || payload.GeneratedBy == "" {
|
|
t.Fatal("expected root metadata fields to be populated")
|
|
}
|
|
if len(payload.Chains) < 3 {
|
|
t.Fatalf("expected multiple chain entries, got %d", len(payload.Chains))
|
|
}
|
|
|
|
var foundChain138 bool
|
|
for _, chain := range payload.Chains {
|
|
if chain.ChainIDDecimal != 138 {
|
|
continue
|
|
}
|
|
foundChain138 = true
|
|
if chain.ShortName == "" || chain.InfoURL == "" || chain.ExplorerAPIURL == "" {
|
|
t.Fatal("expected Chain 138 optional metadata to be populated")
|
|
}
|
|
if len(chain.RPCURLs) == 0 || len(chain.BlockExplorerURL) == 0 {
|
|
t.Fatal("expected Chain 138 RPC and explorer URLs")
|
|
}
|
|
if chain.Testnet {
|
|
t.Fatal("expected Chain 138 to be marked as mainnet")
|
|
}
|
|
}
|
|
|
|
if !foundChain138 {
|
|
t.Fatal("expected Chain 138 entry in networks catalog")
|
|
}
|
|
}
|
|
|
|
func TestConfigTokenListEndpointProvidesOptionalMetadata(t *testing.T) {
|
|
handler := setupConfigHandler()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var payload testTokenList
|
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("failed to parse token list payload: %v", err)
|
|
}
|
|
|
|
if len(payload.Keywords) == 0 {
|
|
t.Fatal("expected token list keywords")
|
|
}
|
|
if payload.Extensions.DefaultChainID != 138 || payload.Extensions.ExplorerURL == "" || payload.Extensions.NetworksConfigURL == "" {
|
|
t.Fatal("expected root-level token list extensions")
|
|
}
|
|
|
|
var foundCXAUC bool
|
|
var foundCUSDT bool
|
|
for _, token := range payload.Tokens {
|
|
switch token.Symbol {
|
|
case "cXAUC":
|
|
foundCXAUC = true
|
|
if token.Extensions.UnitOfAccount == "" || token.Extensions.UnitDescription == "" {
|
|
t.Fatal("expected cXAUC optional unit metadata")
|
|
}
|
|
case "cUSDT":
|
|
foundCUSDT = true
|
|
if token.Decimals != 6 {
|
|
t.Fatalf("expected cUSDT decimals 6, got %d", token.Decimals)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !foundCXAUC || !foundCUSDT {
|
|
t.Fatal("expected cXAUC and cUSDT in token list")
|
|
}
|
|
}
|
|
|
|
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.SupportedMethods, "eth_maxPriorityFeePerGas") {
|
|
t.Fatal("expected eth_maxPriorityFeePerGas support to be documented")
|
|
}
|
|
if !containsString(payload.Tracing.SupportedMethods, "trace_block") {
|
|
t.Fatal("expected trace_block support to be documented")
|
|
}
|
|
}
|
|
|
|
func TestConfigTokenListEndpointReloadsRuntimeFileWithoutRestart(t *testing.T) {
|
|
dir := t.TempDir()
|
|
file := filepath.Join(dir, "token-list.json")
|
|
first := `{"name":"Runtime Token List v1","tokens":[{"chainId":138,"address":"0x1111111111111111111111111111111111111111","symbol":"RT1","name":"Runtime One","decimals":6}]}`
|
|
second := `{"name":"Runtime Token List v2","tokens":[{"chainId":138,"address":"0x2222222222222222222222222222222222222222","symbol":"RT2","name":"Runtime Two","decimals":6}]}`
|
|
|
|
if err := os.WriteFile(file, []byte(first), 0o644); err != nil {
|
|
t.Fatalf("failed to write initial runtime file: %v", err)
|
|
}
|
|
t.Setenv("CONFIG_TOKEN_LIST_JSON_PATH", file)
|
|
|
|
handler := setupConfigHandler()
|
|
|
|
req1 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
if w1.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w1.Code)
|
|
}
|
|
if got := w1.Header().Get("X-Config-Source"); got != "runtime-file" {
|
|
t.Fatalf("expected runtime-file config source, got %q", got)
|
|
}
|
|
etag1 := w1.Header().Get("ETag")
|
|
if etag1 == "" {
|
|
t.Fatal("expected ETag header on runtime-backed response")
|
|
}
|
|
|
|
var body1 testTokenList
|
|
if err := json.Unmarshal(w1.Body.Bytes(), &body1); err != nil {
|
|
t.Fatalf("failed to parse runtime token list v1: %v", err)
|
|
}
|
|
if body1.Name != "Runtime Token List v1" {
|
|
t.Fatalf("expected first runtime payload, got %q", body1.Name)
|
|
}
|
|
|
|
if err := os.WriteFile(file, []byte(second), 0o644); err != nil {
|
|
t.Fatalf("failed to write updated runtime file: %v", err)
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 after runtime update, got %d", w2.Code)
|
|
}
|
|
if got := w2.Header().Get("ETag"); got == "" || got == etag1 {
|
|
t.Fatalf("expected changed ETag after runtime update, got %q", got)
|
|
}
|
|
|
|
var body2 testTokenList
|
|
if err := json.Unmarshal(w2.Body.Bytes(), &body2); err != nil {
|
|
t.Fatalf("failed to parse runtime token list v2: %v", err)
|
|
}
|
|
if body2.Name != "Runtime Token List v2" {
|
|
t.Fatalf("expected updated runtime payload, got %q", body2.Name)
|
|
}
|
|
}
|
|
|
|
func TestConfigTokenListEndpointSupportsETagRevalidation(t *testing.T) {
|
|
handler := setupConfigHandler()
|
|
|
|
req1 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
if w1.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w1.Code)
|
|
}
|
|
|
|
etag := w1.Header().Get("ETag")
|
|
if etag == "" {
|
|
t.Fatal("expected ETag header")
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
|
|
req2.Header.Set("If-None-Match", etag)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusNotModified {
|
|
t.Fatalf("expected 304, got %d", w2.Code)
|
|
}
|
|
}
|
|
|
|
func TestConfigEndpointsSupportOptionsPreflight(t *testing.T) {
|
|
handler := setupConfigHandler()
|
|
req := httptest.NewRequest(http.MethodOptions, "/api/config/token-list", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 preflight response, got %d", w.Code)
|
|
}
|
|
if got := w.Header().Get("Access-Control-Allow-Methods"); got == "" {
|
|
t.Fatal("expected Access-Control-Allow-Methods header")
|
|
}
|
|
}
|