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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -49,6 +50,31 @@ func (s *Server) handleWalletConnectConnect(w http.ResponseWriter, r *http.Reque
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// handleWalletConnectSessionRegister handles POST /api/v1/walletconnect/session
|
||||
func (s *Server) handleWalletConnectSessionRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
Address string `json:"address"`
|
||||
ChainID int `json:"chainId"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
session, err := s.walletConnectHandler().RegisterSession(r.Context(), req.SessionID, req.Address, req.ChainID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, session)
|
||||
}
|
||||
|
||||
// handleWalletConnectSession handles GET /api/v1/walletconnect/session/{id}
|
||||
func (s *Server) handleWalletConnectSession(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
@@ -65,7 +91,7 @@ func (s *Server) handleWalletConnectSession(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
session, err := s.walletConnectHandler().GetSession(r.Context(), sessionID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotImplemented, session)
|
||||
writeJSON(w, http.StatusNotFound, session)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, session)
|
||||
@@ -83,6 +109,8 @@ func (s *Server) handleWalletConnectRoot(w http.ResponseWriter, r *http.Request)
|
||||
s.handleWalletConnectMetadata(w, r)
|
||||
case "connect":
|
||||
s.handleWalletConnectConnect(w, r)
|
||||
case "session":
|
||||
s.handleWalletConnectSessionRegister(w, r)
|
||||
case "":
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||||
@@ -93,6 +121,7 @@ func (s *Server) handleWalletConnectRoot(w http.ResponseWriter, r *http.Request)
|
||||
"/api/v1/walletconnect/config",
|
||||
"/api/v1/walletconnect/metadata",
|
||||
"/api/v1/walletconnect/connect",
|
||||
"POST /api/v1/walletconnect/session",
|
||||
"/api/v1/walletconnect/session/{sessionId}",
|
||||
},
|
||||
"fallbackAuth": "/api/v1/auth/wallet",
|
||||
|
||||
@@ -33,7 +33,8 @@ func TestHandleWalletConnectConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectConnectStub(t *testing.T) {
|
||||
func TestHandleWalletConnectConnectDisabled(t *testing.T) {
|
||||
t.Setenv("WALLETCONNECT_PROJECT_ID", "")
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/walletconnect/connect", strings.NewReader("{}"))
|
||||
@@ -43,25 +44,58 @@ func TestHandleWalletConnectConnectStub(t *testing.T) {
|
||||
if rec.Code != http.StatusNotImplemented {
|
||||
t.Fatalf("expected 501, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectConnectClientMode(t *testing.T) {
|
||||
t.Setenv("WALLETCONNECT_PROJECT_ID", "test-project-id")
|
||||
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.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["status"] != "stub" {
|
||||
t.Fatalf("expected stub status, got %#v", payload["status"])
|
||||
if payload["status"] != "client" {
|
||||
t.Fatalf("expected client status, got %#v", payload["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectSessionStub(t *testing.T) {
|
||||
func TestHandleWalletConnectSessionMissing(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)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWalletConnectSessionRegister(t *testing.T) {
|
||||
server := NewServer(nil, 138)
|
||||
|
||||
body := strings.NewReader(`{"sessionId":"wc-demo","address":"0x4A666F96fC8764181194447A7dFdb7d471b301C8","chainId":138}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/walletconnect/session", body)
|
||||
rec := httptest.NewRecorder()
|
||||
server.handleWalletConnectSessionRegister(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/walletconnect/session/wc-demo", nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
server.handleWalletConnectSession(getRec, getReq)
|
||||
if getRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected lookup 200, got %d", getRec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
const (
|
||||
WalletConnectStatusStub = "stub"
|
||||
WalletConnectStatusClient = "client"
|
||||
WalletConnectStatusDisabled = "disabled"
|
||||
)
|
||||
|
||||
@@ -75,9 +76,9 @@ func (wc *WalletConnect) enabled() bool {
|
||||
|
||||
// PublicConfig returns the read-only WalletConnect config surface for clients.
|
||||
func (wc *WalletConnect) PublicConfig() Config {
|
||||
status := WalletConnectStatusStub
|
||||
if !wc.enabled() {
|
||||
status = WalletConnectStatusDisabled
|
||||
status := WalletConnectStatusDisabled
|
||||
if wc.enabled() {
|
||||
status = WalletConnectStatusClient
|
||||
}
|
||||
return Config{
|
||||
Status: status,
|
||||
@@ -95,30 +96,55 @@ func (wc *WalletConnect) PublicConfig() Config {
|
||||
|
||||
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 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 initiates a wallet connection. Live QR sessions are not implemented yet.
|
||||
// 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: WalletConnectStatusStub,
|
||||
Enabled: wc.enabled(),
|
||||
Status: "client",
|
||||
Enabled: true,
|
||||
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")
|
||||
Message: "Initialize WalletConnect in the browser via /wallet using the published projectId; authenticate with /api/v1/auth/wallet after pairing.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSession gets a wallet session snapshot. Storage is not implemented yet.
|
||||
// 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: WalletConnectStatusStub,
|
||||
Message: "WalletConnect session lookup is stubbed.",
|
||||
}, fmt.Errorf("walletconnect session storage not implemented")
|
||||
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
|
||||
}
|
||||
|
||||
88
backend/wallet/walletconnect_sessions.go
Normal file
88
backend/wallet/walletconnect_sessions.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const walletConnectSessionTTL = 24 * time.Hour
|
||||
|
||||
type storedWalletConnectSession struct {
|
||||
SessionID string
|
||||
Address string
|
||||
ChainID int
|
||||
Connected bool
|
||||
Status string
|
||||
Message string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
walletConnectSessionMu sync.RWMutex
|
||||
walletConnectSessions = map[string]storedWalletConnectSession{}
|
||||
)
|
||||
|
||||
func purgeExpiredWalletConnectSessions(now time.Time) {
|
||||
for id, session := range walletConnectSessions {
|
||||
if now.Sub(session.UpdatedAt) > walletConnectSessionTTL {
|
||||
delete(walletConnectSessions, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterClientSession records a browser-paired WalletConnect session snapshot.
|
||||
func RegisterClientSession(sessionID, address string, chainID int) *Session {
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
address = strings.TrimSpace(address)
|
||||
now := time.Now().UTC()
|
||||
|
||||
walletConnectSessionMu.Lock()
|
||||
defer walletConnectSessionMu.Unlock()
|
||||
purgeExpiredWalletConnectSessions(now)
|
||||
|
||||
record := storedWalletConnectSession{
|
||||
SessionID: sessionID,
|
||||
Address: address,
|
||||
ChainID: chainID,
|
||||
Connected: true,
|
||||
Status: WalletConnectStatusClient,
|
||||
Message: "WalletConnect session registered by browser client after pairing.",
|
||||
UpdatedAt: now,
|
||||
}
|
||||
walletConnectSessions[sessionID] = record
|
||||
return sessionFromStored(record)
|
||||
}
|
||||
|
||||
func lookupWalletConnectSession(sessionID string) (*Session, bool) {
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
if sessionID == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
walletConnectSessionMu.Lock()
|
||||
defer walletConnectSessionMu.Unlock()
|
||||
purgeExpiredWalletConnectSessions(now)
|
||||
|
||||
record, ok := walletConnectSessions[sessionID]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if now.Sub(record.UpdatedAt) > walletConnectSessionTTL {
|
||||
delete(walletConnectSessions, sessionID)
|
||||
return nil, false
|
||||
}
|
||||
return sessionFromStored(record), true
|
||||
}
|
||||
|
||||
func sessionFromStored(record storedWalletConnectSession) *Session {
|
||||
return &Session{
|
||||
SessionID: record.SessionID,
|
||||
Address: record.Address,
|
||||
ChainID: record.ChainID,
|
||||
Connected: record.Connected,
|
||||
Status: record.Status,
|
||||
Message: record.Message,
|
||||
}
|
||||
}
|
||||
25
backend/wallet/walletconnect_sessions_test.go
Normal file
25
backend/wallet/walletconnect_sessions_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegisterAndLookupWalletConnectSession(t *testing.T) {
|
||||
sessionID := "wc-test-topic-123"
|
||||
address := "0x4A666F96fC8764181194447A7dFdb7d471b301C8"
|
||||
|
||||
registered := RegisterClientSession(sessionID, address, 138)
|
||||
if registered == nil || !registered.Connected {
|
||||
t.Fatalf("expected connected session, got %#v", registered)
|
||||
}
|
||||
|
||||
wc := NewWalletConnect(138)
|
||||
session, err := wc.GetSession(context.Background(), sessionID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSession: %v", err)
|
||||
}
|
||||
if session.Address != address {
|
||||
t.Fatalf("expected address %s, got %s", address, session.Address)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user