Add WalletConnect stub, track surfaces, legacy SPA retirement, and dual-domain checks.
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 14s
Validate Explorer / frontend (push) Successful in 1m27s
Validate Explorer / smoke-e2e (push) Failing after 2m19s

Publish walletconnect config endpoints, Track 3/4 notes on analytics/operator pages, legacy SPA at /legacy/index.html with root redirect, and a parity verifier for explorer.d-bis.org vs blockscout.defi-oracle.io.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-22 21:55:42 -07:00
parent 991d1bb07c
commit f2ebe824bd
15 changed files with 2442 additions and 1975 deletions

View File

@@ -54,6 +54,8 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/auth/wallet", s.handleAuthWallet)
mux.HandleFunc("/api/v1/auth/refresh", s.handleAuthRefresh)
mux.HandleFunc("/api/v1/auth/logout", s.handleAuthLogout)
mux.HandleFunc("/api/v1/walletconnect/", s.handleWalletConnectRoot)
mux.HandleFunc("/api/v1/walletconnect", s.handleWalletConnectRoot)
mux.HandleFunc("/api/v1/auth/register", s.handleAuthRegister)
mux.HandleFunc("/api/v1/auth/login", s.handleAuthLogin)
mux.HandleFunc("/api/v1/access/me", s.handleAccessMe)

View File

@@ -0,0 +1,107 @@
package rest
import (
"net/http"
"strings"
"github.com/explorer/backend/wallet"
)
func (s *Server) walletConnectHandler() *wallet.WalletConnect {
return wallet.NewWalletConnect(s.chainID)
}
// handleWalletConnectConfig handles GET /api/v1/walletconnect/config
func (s *Server) handleWalletConnectConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
writeJSON(w, http.StatusOK, s.walletConnectHandler().PublicConfig())
}
// handleWalletConnectMetadata handles GET /api/v1/walletconnect/metadata
func (s *Server) handleWalletConnectMetadata(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"name": "DBIS Explorer",
"description": "Chain 138 explorer by DBIS",
"url": "https://explorer.d-bis.org",
"icons": []string{"https://explorer.d-bis.org/favicon.ico"},
})
}
// handleWalletConnectConnect handles POST /api/v1/walletconnect/connect
func (s *Server) handleWalletConnectConnect(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
response, err := s.walletConnectHandler().Connect(r.Context())
if err != nil {
writeJSON(w, http.StatusNotImplemented, response)
return
}
writeJSON(w, http.StatusOK, response)
}
// handleWalletConnectSession handles GET /api/v1/walletconnect/session/{id}
func (s *Server) handleWalletConnectSession(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
sessionID := strings.TrimPrefix(r.URL.Path, "/api/v1/walletconnect/session/")
sessionID = strings.Trim(sessionID, "/")
if sessionID == "" || strings.Contains(sessionID, "/") {
writeError(w, http.StatusBadRequest, "bad_request", "session id is required")
return
}
session, err := s.walletConnectHandler().GetSession(r.Context(), sessionID)
if err != nil {
writeJSON(w, http.StatusNotImplemented, session)
return
}
writeJSON(w, http.StatusOK, session)
}
// handleWalletConnectRoot dispatches walletconnect subroutes.
func (s *Server) handleWalletConnectRoot(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/walletconnect")
path = strings.Trim(path, "/")
switch path {
case "config":
s.handleWalletConnectConfig(w, r)
case "metadata":
s.handleWalletConnectMetadata(w, r)
case "connect":
s.handleWalletConnectConnect(w, r)
case "":
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"endpoints": []string{
"/api/v1/walletconnect/config",
"/api/v1/walletconnect/metadata",
"/api/v1/walletconnect/connect",
"/api/v1/walletconnect/session/{sessionId}",
},
"fallbackAuth": "/api/v1/auth/wallet",
})
default:
if strings.HasPrefix(path, "session/") {
s.handleWalletConnectSession(w, r)
return
}
writeError(w, http.StatusNotFound, "not_found", "walletconnect route not found")
}
}

View File

@@ -0,0 +1,79 @@
package rest
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
func TestHandleWalletConnectConfig(t *testing.T) {
t.Setenv("WALLETCONNECT_PROJECT_ID", "test-project-id")
server := NewServer(nil, 138)
req := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect/config", nil)
rec := httptest.NewRecorder()
server.handleWalletConnectConfig(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
var payload map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload["projectId"] != "test-project-id" {
t.Fatalf("expected project id, got %#v", payload["projectId"])
}
if payload["fallbackAuth"] != "/api/v1/auth/wallet" {
t.Fatalf("expected fallback auth path, got %#v", payload["fallbackAuth"])
}
}
func TestHandleWalletConnectConnectStub(t *testing.T) {
server := NewServer(nil, 138)
req := httptest.NewRequest(http.MethodPost, "/api/v1/walletconnect/connect", strings.NewReader("{}"))
rec := httptest.NewRecorder()
server.handleWalletConnectConnect(rec, req)
if rec.Code != http.StatusNotImplemented {
t.Fatalf("expected 501, got %d", rec.Code)
}
var payload map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload["status"] != "stub" {
t.Fatalf("expected stub status, got %#v", payload["status"])
}
}
func TestHandleWalletConnectSessionStub(t *testing.T) {
server := NewServer(nil, 138)
req := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect/session/demo-session", nil)
rec := httptest.NewRecorder()
server.handleWalletConnectSession(rec, req)
if rec.Code != http.StatusNotImplemented {
t.Fatalf("expected 501, got %d", rec.Code)
}
}
func TestHandleWalletConnectRootIndex(t *testing.T) {
_ = os.Setenv("WALLETCONNECT_PROJECT_ID", "")
server := NewServer(nil, 138)
req := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect", nil)
rec := httptest.NewRecorder()
server.handleWalletConnectRoot(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
}

View File

@@ -3,35 +3,122 @@ package wallet
import (
"context"
"fmt"
"os"
"strings"
"time"
)
// WalletConnect handles WalletConnect v2 integration
const (
WalletConnectStatusStub = "stub"
WalletConnectStatusDisabled = "disabled"
)
// Config describes the public WalletConnect v2 posture exposed to clients.
type Config struct {
Status string `json:"status"`
Enabled bool `json:"enabled"`
ProjectID string `json:"projectId"`
RelayURL string `json:"relayUrl"`
MetadataURL string `json:"metadataUrl"`
RequiredNamespaces []string `json:"requiredNamespaces"`
SupportedChains []int `json:"supportedChains"`
FallbackAuth string `json:"fallbackAuth"`
Message string `json:"message"`
UpdatedAt string `json:"updatedAt"`
}
// ConnectResponse is returned while WalletConnect session bridging remains a stub.
type ConnectResponse struct {
Status string `json:"status"`
Enabled bool `json:"enabled"`
URI string `json:"uri,omitempty"`
SessionID string `json:"sessionId,omitempty"`
ExpiresAt string `json:"expiresAt,omitempty"`
FallbackAuth string `json:"fallbackAuth"`
Message string `json:"message"`
}
// Session represents a wallet session snapshot for future WalletConnect integration.
type Session struct {
SessionID string `json:"sessionId"`
Address string `json:"address,omitempty"`
ChainID int `json:"chainId,omitempty"`
Connected bool `json:"connected"`
Status string `json:"status"`
Message string `json:"message"`
}
// WalletConnect handles WalletConnect v2 integration posture for the explorer API.
type WalletConnect struct {
projectID string
relayURL string
chainID int
}
// NewWalletConnect creates a new WalletConnect handler
func NewWalletConnect(projectID string) *WalletConnect {
return &WalletConnect{projectID: projectID}
// NewWalletConnect creates a WalletConnect handler using deployment env vars.
func NewWalletConnect(chainID int) *WalletConnect {
projectID := strings.TrimSpace(os.Getenv("WALLETCONNECT_PROJECT_ID"))
relayURL := strings.TrimSpace(os.Getenv("WALLETCONNECT_RELAY_URL"))
if relayURL == "" {
relayURL = "wss://relay.walletconnect.org"
}
return &WalletConnect{
projectID: projectID,
relayURL: relayURL,
chainID: chainID,
}
}
// Connect initiates a wallet connection
func (wc *WalletConnect) Connect(ctx context.Context) (string, error) {
// Implementation would use WalletConnect v2 SDK
// Returns connection URI for QR code display
return "", fmt.Errorf("not implemented - requires WalletConnect SDK")
func (wc *WalletConnect) enabled() bool {
return wc.projectID != ""
}
// Session represents a wallet session
type Session struct {
Address string
ChainID int
Connected bool
// PublicConfig returns the read-only WalletConnect config surface for clients.
func (wc *WalletConnect) PublicConfig() Config {
status := WalletConnectStatusStub
if !wc.enabled() {
status = WalletConnectStatusDisabled
}
return Config{
Status: status,
Enabled: wc.enabled(),
ProjectID: wc.projectID,
RelayURL: wc.relayURL,
MetadataURL: "/api/v1/walletconnect/metadata",
RequiredNamespaces: []string{"eip155"},
SupportedChains: []int{wc.chainID, 1},
FallbackAuth: "/api/v1/auth/wallet",
Message: wc.publicMessage(),
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
}
}
// GetSession gets current wallet session
func (wc *WalletConnect) GetSession(ctx context.Context, sessionID string) (*Session, error) {
// Implementation would retrieve session from WalletConnect
return nil, fmt.Errorf("not implemented")
func (wc *WalletConnect) publicMessage() string {
if wc.enabled() {
return "WalletConnect v2 config is published, but session bridging is still stubbed. Use browser wallet auth at /api/v1/auth/wallet until mobile QR sessions ship."
}
return "WalletConnect v2 is not configured. Set WALLETCONNECT_PROJECT_ID to publish relay config; browser wallet auth remains available at /api/v1/auth/wallet."
}
// Connect initiates a wallet connection. Live QR sessions are not implemented yet.
func (wc *WalletConnect) Connect(_ context.Context) (*ConnectResponse, error) {
return &ConnectResponse{
Status: WalletConnectStatusStub,
Enabled: wc.enabled(),
FallbackAuth: "/api/v1/auth/wallet",
Message: "WalletConnect session creation is stubbed. Use browser extension wallet auth until the relay bridge is enabled.",
}, fmt.Errorf("walletconnect session bridge not implemented")
}
// GetSession gets a wallet session snapshot. Storage is not implemented yet.
func (wc *WalletConnect) GetSession(_ context.Context, sessionID string) (*Session, error) {
if strings.TrimSpace(sessionID) == "" {
return nil, fmt.Errorf("session id is required")
}
return &Session{
SessionID: sessionID,
Connected: false,
Status: WalletConnectStatusStub,
Message: "WalletConnect session lookup is stubbed.",
}, fmt.Errorf("walletconnect session storage not implemented")
}