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

View File

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

View File

@@ -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';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import { Card } from '@/libs/frontend-ui-primitives'
import type { ExplorerFeaturePage } from '@/data/explorerOperations'
import OperationsSurfaceNav from './OperationsSurfaceNav'
import OperationsActionGrid from './OperationsActionGrid'
import OperationsTrackNote from './OperationsTrackNote'
export type StatusTone = 'normal' | 'warning' | 'danger'
@@ -108,6 +109,10 @@ export default function OperationsPageShell({
</Card>
) : null}
{page.accessTrack && page.accessNote ? (
<OperationsTrackNote track={page.accessTrack} note={page.accessNote} />
) : null}
<OperationsSurfaceNav />
{children}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/libs/frontend-ui-primitives'
export default function OperationsTrackNote({
track,
note,
className,
}: {
track: number
note: string
className?: string
}) {
return (
<Card className={className ?? 'mb-6 border border-violet-200 bg-violet-50/70 dark:border-violet-900/50 dark:bg-violet-950/20'}>
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-violet-700 dark:text-violet-200">
Track {track} public surface
</div>
<p className="mt-2 text-sm leading-6 text-violet-950 dark:text-violet-100">{note}</p>
</Card>
)
}

View File

@@ -11,6 +11,8 @@ export interface ExplorerFeaturePage {
title: string
description: string
note?: string
accessTrack?: number
accessNote?: string
actions: ExplorerFeatureAction[]
}
@@ -141,6 +143,9 @@ export const explorerFeaturePages = {
description:
'Use the public explorer pages and live monitoring endpoints as the visible analytics surface for chain activity, recent blocks, and transaction flow.',
note: sharedOperationsNote,
accessTrack: 3,
accessNote:
'This page is the public Track 3 analytics surface. Wallet-authenticated Track 3 APIs remain available after browser wallet sign-in.',
actions: [
{
title: 'Blocks',
@@ -175,6 +180,9 @@ export const explorerFeaturePages = {
description:
'Expose the public operator surface for bridge checks, route validation, planner providers, liquidity entry points, and documentation.',
note: sharedOperationsNote,
accessTrack: 4,
accessNote:
'This page is the public Track 4 operator surface. Sensitive operator write APIs remain gated behind wallet auth and operator policy.',
actions: [
{
title: 'Bridge monitoring',
@@ -334,6 +342,16 @@ export const explorerOperationsSurfaces: ExplorerOperationsSurface[] = [
label: 'System',
description: 'Networks, RPC methods, and topology inventory.',
},
{
href: '/analytics',
label: 'Analytics',
description: 'Track 3 activity summaries, trends, and freshness context.',
},
{
href: '/operator',
label: 'Operator',
description: 'Track 4 relay, route, and planner shortcuts.',
},
]
export const explorerPublicApiLinks = [
@@ -357,4 +375,9 @@ export const explorerPublicApiLinks = [
label: 'Wallet networks',
description: 'Published chain metadata for wallet onboarding.',
},
{
href: '/explorer-api/v1/walletconnect/config',
label: 'WalletConnect config',
description: 'Published WalletConnect v2 posture and browser-auth fallback.',
},
] as const

View File

@@ -0,0 +1,24 @@
import { fetchPublicJson } from '@/utils/publicExplorer'
const WALLETCONNECT_CONFIG_PATH = '/explorer-api/v1/walletconnect/config'
export interface WalletConnectConfigResponse {
status: 'stub' | 'disabled' | string
enabled: boolean
projectId: string
relayUrl: string
metadataUrl: string
requiredNamespaces: string[]
supportedChains: number[]
fallbackAuth: string
message: string
updatedAt: string
}
export async function getWalletConnectConfig(): Promise<WalletConnectConfigResponse | null> {
try {
return await fetchPublicJson<WalletConnectConfigResponse>(WALLETCONNECT_CONFIG_PATH)
} catch {
return null
}
}

View File

@@ -35,6 +35,7 @@ STATIC_SYNC_FILES=(
"favicon.ico"
"apple-touch-icon.png"
"explorer-spa.js"
"legacy/index.html"
)
cleanup() {

View File

@@ -50,4 +50,22 @@ test.describe('Explorer sprint smoke', () => {
await expect(page.getByRole('contentinfo').getByText(/Public APIs/i)).toBeVisible({ timeout: 10000 })
await expect(page.getByRole('contentinfo').getByRole('link', { name: /Blockscout stats/i })).toBeVisible({ timeout: 10000 })
})
test('analytics page shows track 3 surface note', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/analytics`, { waitUntil: 'domcontentloaded', timeout: 30000 })
await expect(page.getByRole('heading', { name: /Analytics & Network Activity/i })).toBeVisible({ timeout: 15000 })
await expect(page.getByText(/Track 3 public surface/i).first()).toBeVisible({ timeout: 10000 })
})
test('operator page shows track 4 surface note', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/operator`, { waitUntil: 'domcontentloaded', timeout: 30000 })
await expect(page.getByRole('heading', { name: /^Operator Surface$/i })).toBeVisible({ timeout: 15000 })
await expect(page.getByText(/Track 4 public surface/i).first()).toBeVisible({ timeout: 10000 })
})
test('legacy SPA fallback loads', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/legacy/index.html`, { waitUntil: 'domcontentloaded', timeout: 30000 })
await expect(page).toHaveTitle(/DBIS Explorer/i)
await expect(page.getByRole('heading', { name: /Latest Blocks/i })).toBeVisible({ timeout: 15000 })
})
})

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
PRIMARY_DOMAIN="${PRIMARY_DOMAIN:-https://explorer.d-bis.org}"
COMPANION_DOMAIN="${COMPANION_DOMAIN:-https://blockscout.defi-oracle.io}"
paths=(
"/"
"/wallet"
"/operations"
"/bridge"
"/api/v2/stats"
"/explorer-api/v1/track1/bridge/status"
"/token-aggregation/api/v1/routes/matrix?includeNonLive=true"
"/explorer-api/v1/walletconnect/config"
)
failures=0
check_path() {
local domain="$1"
local path="$2"
local code
code="$(curl -ksS -o /dev/null -w '%{http_code}' --max-time 20 "${domain}${path}")"
if [[ "$code" =~ ^(200|301|302|307|308)$ ]]; then
printf 'OK %3s %s%s\n' "$code" "$domain" "$path"
else
printf 'FAIL %3s %s%s\n' "$code" "$domain" "$path"
failures=$((failures + 1))
fi
}
echo "Checking explorer dual-domain parity"
echo "Primary: $PRIMARY_DOMAIN"
echo "Companion: $COMPANION_DOMAIN"
echo
for path in "${paths[@]}"; do
check_path "$PRIMARY_DOMAIN" "$path"
check_path "$COMPANION_DOMAIN" "$path"
done
echo
if (( failures > 0 )); then
echo "Dual-domain parity check failed ($failures mismatches/non-200)." >&2
exit 1
fi
echo "Dual-domain parity check passed."