diff --git a/app_session.go b/app_session.go index bc0b5a2..46b7d48 100644 --- a/app_session.go +++ b/app_session.go @@ -1,7 +1,10 @@ package main import ( + "fmt" + "github.com/lib/pq" + "gorm.io/gorm" ) // AppSession represents a virtual payment application session between participants @@ -21,3 +24,25 @@ type AppSession struct { func (AppSession) TableName() string { return "app_sessions" } + +// getAppSessionsForParticipant finds all channels for a participant +func getAppSessionsForParticipant(tx *gorm.DB, participant string, status string) ([]AppSession, error) { + var sessions []AppSession + switch tx.Dialector.Name() { + case "postgres": + tx = tx.Where("? = ANY(participants)", participant) + case "sqlite": + tx = tx.Where("instr(participants, ?) > 0", participant) + default: + return nil, fmt.Errorf("unsupported database driver: %s", tx.Dialector.Name()) + } + if status != "" { + tx = tx.Where("status = ?", status) + } + + if err := tx.Find(&sessions).Error; err != nil { + return nil, err + } + + return sessions, nil +} diff --git a/docs/API.md b/docs/API.md index 57c8089..29bedfe 100644 --- a/docs/API.md +++ b/docs/API.md @@ -11,6 +11,7 @@ | `get_config` | Retrieves broker configuration and supported networks | | `get_assets` | Retrieves all supported assets (optionally filtered by chain_id) | | `get_app_definition` | Retrieves application definition for a ledger account | +| `get_app_sessions` | Lists virtual applications for a participant with optional status filter | | `get_ledger_balances` | Lists participants and their balances for a ledger account | | `get_ledger_entries` | Retrieves detailed ledger entries for a participant | | `get_channels` | Lists all channels for a participant with their status across all chains | @@ -116,6 +117,60 @@ Retrieves the application definition for a specific ledger account. } ``` +### Get App Sessions + +Lists all virtual applications for a participant. Optionally, you can filter the results by status (open, closed). + +**Request:** + +```json +{ + "req": [1, "get_app_sessions", [{ + "participant": "0x1234567890abcdef...", + "status": "open" // Optional: filter by status + }], 1619123456789], + "sig": ["0x9876fedcba..."] +} +``` + +**Response:** + +```json +{ + "res": [1, "get_app_sessions", [[ + { + "app_session_id": "0x3456789012abcdef...", + "status": "open", + "participants": [ + "0x1234567890abcdef...", + "0x00112233445566778899AaBbCcDdEeFf00112233" + ], + "protocol": "NitroAura", + "challenge": 86400, + "weights": [50, 50], + "quorum": 100, + "version": 1, + "nonce": 123456789 + }, + { + "app_session_id": "0x7890123456abcdef...", + "status": "open", + "participants": [ + "0x1234567890abcdef...", + "0xAaBbCcDdEeFf0011223344556677889900aAbBcC" + ], + "protocol": "NitroSnake", + "challenge": 86400, + "weights": [70, 30], + "quorum": 100, + "version": 1, + "nonce": 123456790 + } + ]], 1619123456789], + "sig": ["0xabcd1234..."] +} +``` + ### Get Ledger Balances Retrieves the balances of all participants in a specific ledger account. diff --git a/handlers.go b/handlers.go index 98c2379..ce59986 100644 --- a/handlers.go +++ b/handlers.go @@ -72,8 +72,15 @@ func (r CloseAppSignData) MarshalJSON() ([]byte, error) { // AppSessionResponse represents response data for application operations type AppSessionResponse struct { - AppSessionID string `json:"app_session_id"` - Status string `json:"status"` + AppSessionID string `json:"app_session_id"` + Status string `json:"status"` + Participants []string `json:"participants,omitempty"` + Protocol string `json:"protocol,omitempty"` + Challenge uint64 `json:"challenge,omitempty"` + Weights []int64 `json:"weights,omitempty"` + Quorum uint64 `json:"quorum,omitempty"` + Version uint64 `json:"version,omitempty"` + Nonce uint64 `json:"nonce,omitempty"` } // ResizeChannelParams represents parameters needed for resizing a channel @@ -613,6 +620,48 @@ func HandleGetAppDefinition(rpc *RPCMessage, db *gorm.DB) (*RPCMessage, error) { return rpcResponse, nil } +func HandleGetAppSessions(rpc *RPCMessage, db *gorm.DB) (*RPCMessage, error) { + var participant string + var status string + + if len(rpc.Req.Params) > 0 { + paramsJSON, err := json.Marshal(rpc.Req.Params[0]) + if err == nil { + var params map[string]string + if err := json.Unmarshal(paramsJSON, ¶ms); err == nil { + participant = params["participant"] + status = params["status"] + } + } + } + + if participant == "" { + return nil, errors.New("missing participant") + } + + sessions, err := getAppSessionsForParticipant(db, participant, status) + if err != nil { + return nil, fmt.Errorf("failed to find application sessions: %w", err) + } + response := make([]AppSessionResponse, len(sessions)) + for i, session := range sessions { + response[i] = AppSessionResponse{ + AppSessionID: session.SessionID, + Status: string(session.Status), + Participants: session.Participants, + Protocol: session.Protocol, + Challenge: session.Challenge, + Weights: session.Weights, + Quorum: session.Quorum, + Version: session.Version, + Nonce: session.Nonce, + } + } + + rpcResponse := CreateResponse(rpc.Req.RequestID, rpc.Req.Method, []any{response}, time.Now()) + return rpcResponse, nil +} + // HandleResizeChannel processes a request to resize a payment channel func HandleResizeChannel(rpc *RPCMessage, db *gorm.DB, signer *Signer) (*RPCMessage, error) { if len(rpc.Req.Params) < 1 { diff --git a/handlers_test.go b/handlers_test.go index 0839396..9885a6c 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -917,6 +917,158 @@ func TestHandleGetAssets(t *testing.T) { assert.Len(t, assets4, 0, "Should return 0 assets for non-existent chain_id") } +// TestHandleGetAppSessions tests the get app sessions handler functionality +func TestHandleGetAppSessions(t *testing.T) { + rawKey, err := crypto.GenerateKey() + require.NoError(t, err) + signer := Signer{privateKey: rawKey} + participantAddr := signer.GetAddress().Hex() + + db, cleanup := setupTestDB(t) + defer cleanup() + + // Create some test app sessions + sessions := []AppSession{ + { + SessionID: "0xSession1", + Participants: []string{participantAddr, "0xParticipant2"}, + Status: ChannelStatusOpen, + Protocol: "test-app-1", + Challenge: 60, + Weights: []int64{50, 50}, + Quorum: 75, + Nonce: 1, + Version: 1, + }, + { + SessionID: "0xSession2", + Participants: []string{participantAddr, "0xParticipant3"}, + Status: ChannelStatusClosed, + Protocol: "test-app-2", + Challenge: 120, + Weights: []int64{30, 70}, + Quorum: 80, + Nonce: 2, + Version: 2, + }, + { + SessionID: "0xSession3", + Participants: []string{"0xParticipant4", "0xParticipant5"}, + Status: ChannelStatusOpen, + Protocol: "test-app-3", + Challenge: 90, + Weights: []int64{40, 60}, + Quorum: 60, + Nonce: 3, + Version: 3, + }, + } + + for _, session := range sessions { + require.NoError(t, db.Create(&session).Error) + } + + // Test Case 1: Get all app sessions for the participant + params1 := map[string]string{ + "participant": participantAddr, + } + paramsJSON1, err := json.Marshal(params1) + require.NoError(t, err) + + rpcRequest1 := &RPCMessage{ + Req: &RPCData{ + RequestID: 1, + Method: "get_app_sessions", + Params: []any{json.RawMessage(paramsJSON1)}, + Timestamp: uint64(time.Now().Unix()), + }, + Sig: []string{"dummy-signature"}, + } + + // Call the handler + resp1, err := HandleGetAppSessions(rpcRequest1, db) + require.NoError(t, err) + assert.NotNil(t, resp1) + + // Verify response format + assert.Equal(t, "get_app_sessions", resp1.Res.Method) + assert.Equal(t, uint64(1), resp1.Res.RequestID) + require.Len(t, resp1.Res.Params, 1, "Response should contain an array of AppSessionResponse objects") + + // Extract and verify app sessions + sessionResponses, ok := resp1.Res.Params[0].([]AppSessionResponse) + require.True(t, ok, "Response parameter should be a slice of AppSessionResponse") + assert.Len(t, sessionResponses, 2, "Should return 2 app sessions for the participant") + + // Verify the response contains the expected app sessions + foundSessions := make(map[string]bool) + for _, session := range sessionResponses { + foundSessions[session.AppSessionID] = true + + // Find the original session to compare with + var originalSession AppSession + for _, s := range sessions { + if s.SessionID == session.AppSessionID { + originalSession = s + break + } + } + + assert.Equal(t, string(originalSession.Status), session.Status, "Status should match") + } + + assert.True(t, foundSessions["0xSession1"], "Should include Session1") + assert.True(t, foundSessions["0xSession2"], "Should include Session2") + assert.False(t, foundSessions["0xSession3"], "Should not include Session3") + + // Test Case 2: Get open app sessions for the participant + params2 := map[string]string{ + "participant": participantAddr, + "status": string(ChannelStatusOpen), + } + paramsJSON2, err := json.Marshal(params2) + require.NoError(t, err) + + rpcRequest2 := &RPCMessage{ + Req: &RPCData{ + RequestID: 2, + Method: "get_app_sessions", + Params: []any{json.RawMessage(paramsJSON2)}, + Timestamp: uint64(time.Now().Unix()), + }, + Sig: []string{"dummy-signature"}, + } + + // Call the handler + resp2, err := HandleGetAppSessions(rpcRequest2, db) + require.NoError(t, err) + assert.NotNil(t, resp2) + + // Extract and verify filtered app sessions + sessionResponses2, ok := resp2.Res.Params[0].([]AppSessionResponse) + require.True(t, ok, "Response parameter should be a slice of AppSessionResponse") + assert.Len(t, sessionResponses2, 1, "Should return 1 open app session for the participant") + assert.Equal(t, "0xSession1", sessionResponses2[0].AppSessionID, "Should be Session1") + assert.Equal(t, string(ChannelStatusOpen), sessionResponses2[0].Status, "Status should be open") + + // Test Case 3: Error case - missing participant + rpcRequest3 := &RPCMessage{ + Req: &RPCData{ + RequestID: 3, + Method: "get_app_sessions", + Params: []any{json.RawMessage(`{}`)}, + Timestamp: uint64(time.Now().Unix()), + }, + Sig: []string{"dummy-signature"}, + } + + // Call with missing participant + resp3, err := HandleGetAppSessions(rpcRequest3, db) + assert.Error(t, err, "Should return error with missing participant") + assert.Nil(t, resp3) + assert.Contains(t, err.Error(), "missing participant", "Error should mention missing participant") +} + func TestHandleGetRPCHistory(t *testing.T) { rawKey, err := crypto.GenerateKey() require.NoError(t, err) diff --git a/ws.go b/ws.go index aef9dd1..dfebaef 100644 --- a/ws.go +++ b/ws.go @@ -290,6 +290,14 @@ func (h *UnifiedWSHandler) HandleConnection(w http.ResponseWriter, r *http.Reque } h.sendBalanceUpdate(address) + case "get_app_sessions": + rpcResponse, handlerErr = HandleGetAppSessions(&msg, h.db) + if handlerErr != nil { + log.Printf("Error handling get_app_sessions: %v", handlerErr) + h.sendErrorResponse(address, &msg, conn, "Failed to get app sessions: "+handlerErr.Error()) + continue + } + case "resize_channel": rpcResponse, handlerErr = HandleResizeChannel(&msg, h.db, h.signer) if handlerErr != nil {