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>
151 lines
5.1 KiB
Go
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
|
|
}
|