diff --git a/backend/api/rest/routes.go b/backend/api/rest/routes.go index 8fac635..760b37b 100644 --- a/backend/api/rest/routes.go +++ b/backend/api/rest/routes.go @@ -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) diff --git a/backend/api/rest/walletconnect.go b/backend/api/rest/walletconnect.go new file mode 100644 index 0000000..a69ecb1 --- /dev/null +++ b/backend/api/rest/walletconnect.go @@ -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") + } +} diff --git a/backend/api/rest/walletconnect_internal_test.go b/backend/api/rest/walletconnect_internal_test.go new file mode 100644 index 0000000..3dbaf46 --- /dev/null +++ b/backend/api/rest/walletconnect_internal_test.go @@ -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) + } +} diff --git a/backend/wallet/walletconnect.go b/backend/wallet/walletconnect.go index ae087a3..f93a673 100644 --- a/backend/wallet/walletconnect.go +++ b/backend/wallet/walletconnect.go @@ -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") +} diff --git a/deployment/common/nginx-next-frontend-proxy.conf b/deployment/common/nginx-next-frontend-proxy.conf index cad8fd1..94d5cbe 100644 --- a/deployment/common/nginx-next-frontend-proxy.conf +++ b/deployment/common/nginx-next-frontend-proxy.conf @@ -12,6 +12,12 @@ # Include these locations after those API/static locations and before any legacy # catch-all that serves /var/www/html/index.html directly. +location ^~ /legacy/ { + alias /var/www/html/legacy/; + try_files $uri $uri/ /legacy/index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate" always; +} + location ^~ /_next/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; diff --git a/frontend/public/explorer-spa.js b/frontend/public/explorer-spa.js index 6a22d1e..67212d6 100644 --- a/frontend/public/explorer-spa.js +++ b/frontend/public/explorer-spa.js @@ -1,4 +1,7 @@ const API_BASE = '/api'; + if (typeof console !== 'undefined' && console.warn) { + console.warn('[DBIS Explorer] Legacy SPA bundle loaded from /legacy/. Use / for the current Next.js explorer UI.'); + } const EXPLORER_API_BASE = '/explorer-api'; const EXPLORER_API_V1_BASE = EXPLORER_API_BASE + '/v1'; const EXPLORER_TRACK1_BASE = EXPLORER_API_V1_BASE + '/track1'; diff --git a/frontend/public/index.html b/frontend/public/index.html index 7dbce06..1d0712a 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,1964 +1,43 @@
- - - - - - - -Add addresses from their detail page to track them here.
-Redirecting to the current Chain 138 explorer…
+ + +Add addresses from their detail page to track them here.
+{note}
+