Files
explorer-monorepo/backend/wallet/walletconnect.go
defiQUG ab9c1f9f98
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 15s
Validate Explorer / frontend (push) Failing after 20s
Validate Explorer / smoke-e2e (push) Has been skipped
Ship bridge lanes, public API access doc, and WalletConnect client stack.
Align CCIP catalog UX with 11-lane config-ready routes, document the no-key public API decision, and enable browser WalletConnect pairing with backend session registration and deploy-time project ID wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 02:21:37 -07:00

151 lines
5.1 KiB
Go

package wallet
import (
"context"
"fmt"
"os"
"strings"
"time"
)
const (
WalletConnectStatusStub = "stub"
WalletConnectStatusClient = "client"
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 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,
}
}
func (wc *WalletConnect) enabled() bool {
return wc.projectID != ""
}
// PublicConfig returns the read-only WalletConnect config surface for clients.
func (wc *WalletConnect) PublicConfig() Config {
status := WalletConnectStatusDisabled
if wc.enabled() {
status = WalletConnectStatusClient
}
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),
}
}
func (wc *WalletConnect) publicMessage() string {
if wc.enabled() {
return "WalletConnect v2 is enabled. Use the WalletConnect button on /wallet for mobile QR pairing; browser extension wallets can continue using /api/v1/auth/wallet."
}
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 reports client-side WalletConnect posture. Pairing runs in the browser when projectId is published.
func (wc *WalletConnect) Connect(_ context.Context) (*ConnectResponse, error) {
if !wc.enabled() {
return &ConnectResponse{
Status: WalletConnectStatusDisabled,
Enabled: false,
FallbackAuth: "/api/v1/auth/wallet",
Message: wc.publicMessage(),
}, fmt.Errorf("walletconnect is disabled")
}
return &ConnectResponse{
Status: "client",
Enabled: true,
FallbackAuth: "/api/v1/auth/wallet",
Message: "Initialize WalletConnect in the browser via /wallet using the published projectId; authenticate with /api/v1/auth/wallet after pairing.",
}, nil
}
// GetSession returns a registered browser-paired WalletConnect session snapshot.
func (wc *WalletConnect) GetSession(_ context.Context, sessionID string) (*Session, error) {
if strings.TrimSpace(sessionID) == "" {
return nil, fmt.Errorf("session id is required")
}
if session, ok := lookupWalletConnectSession(sessionID); ok {
return session, nil
}
return &Session{
SessionID: sessionID,
Connected: false,
Status: WalletConnectStatusClient,
Message: "Session not registered yet. Pair on /wallet, then POST /api/v1/walletconnect/session with sessionId and address.",
}, fmt.Errorf("walletconnect session not found")
}
// RegisterSession stores a client-paired WalletConnect session for operator lookup.
func (wc *WalletConnect) RegisterSession(_ context.Context, sessionID, address string, chainID int) (*Session, error) {
if strings.TrimSpace(sessionID) == "" {
return nil, fmt.Errorf("session id is required")
}
if !strings.HasPrefix(strings.ToLower(address), "0x") || len(address) != 42 {
return nil, fmt.Errorf("valid address is required")
}
if chainID <= 0 {
chainID = wc.chainID
}
return RegisterClientSession(sessionID, address, chainID), nil
}