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 }