Add WalletConnect stub, track surfaces, legacy SPA retirement, and dual-domain checks.
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:
@@ -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)
|
||||
|
||||
107
backend/api/rest/walletconnect.go
Normal file
107
backend/api/rest/walletconnect.go
Normal 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")
|
||||
}
|
||||
}
|
||||
79
backend/api/rest/walletconnect_internal_test.go
Normal file
79
backend/api/rest/walletconnect_internal_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
1964
frontend/public/legacy/index.html
Normal file
1964
frontend/public/legacy/index.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
|
||||
20
frontend/src/components/explorer/OperationsTrackNote.tsx
Normal file
20
frontend/src/components/explorer/OperationsTrackNote.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
24
frontend/src/services/api/walletConnect.ts
Normal file
24
frontend/src/services/api/walletConnect.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ STATIC_SYNC_FILES=(
|
||||
"favicon.ico"
|
||||
"apple-touch-icon.png"
|
||||
"explorer-spa.js"
|
||||
"legacy/index.html"
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
|
||||
49
scripts/verify/check-explorer-dual-domain-parity.sh
Executable file
49
scripts/verify/check-explorer-dual-domain-parity.sh
Executable 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."
|
||||
Reference in New Issue
Block a user