From 79a4fb092240319ed68f22ed2cb660fefb44238a Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Thu, 17 Jul 2025 15:13:34 +0300 Subject: [PATCH 1/2] refactor: API protocol --- clearnode/app_session_service_test.go | 19 +- clearnode/custody_test.go | 14 +- clearnode/docs/API.md | 796 ++++++++++-------- clearnode/docs/Clearnode.protocol.md | 40 +- clearnode/notification.go | 8 +- clearnode/reconcile_cli.go | 2 +- clearnode/rpc.go | 15 +- clearnode/rpc_node.go | 20 +- clearnode/rpc_node_test.go | 125 ++- clearnode/rpc_router.go | 34 +- clearnode/rpc_router_auth.go | 108 +-- clearnode/rpc_router_callback.go | 66 ++ clearnode/rpc_router_private.go | 45 +- clearnode/rpc_router_private_test.go | 74 +- clearnode/rpc_router_public.go | 68 +- clearnode/rpc_router_public_test.go | 96 +-- integration/common/auth.ts | 10 +- integration/common/nitroliteClient.ts | 9 +- integration/tests/challenge_channel.test.ts | 155 ++++ integration/tests/clearnode_auth.test.ts | 23 +- integration/tests/close_channel.test.ts | 4 +- integration/tests/create_channel.test.ts | 11 +- integration/tests/get_user_tag.test.ts | 10 +- integration/tests/ledger_transactions.test.ts | 86 +- integration/tests/lifecycle.test.ts | 119 +-- integration/tests/resize_channel.test.ts | 50 +- sdk/src/rpc/api.ts | 263 ++++-- sdk/src/rpc/nitrolite.ts | 140 +-- sdk/src/rpc/parse/app.ts | 185 ++-- sdk/src/rpc/parse/asset.ts | 52 +- sdk/src/rpc/parse/auth.ts | 53 +- sdk/src/rpc/parse/channel.ts | 199 +++-- sdk/src/rpc/parse/common.ts | 32 +- sdk/src/rpc/parse/ledger.ts | 157 ++-- sdk/src/rpc/parse/misc.ts | 147 ++-- sdk/src/rpc/parse/parse.ts | 134 ++- sdk/src/rpc/types/common.ts | 295 +++++++ sdk/src/rpc/types/filters.ts | 17 + sdk/src/rpc/types/index.ts | 155 +--- sdk/src/rpc/types/request.ts | 558 ++++++------ sdk/src/rpc/types/response.ts | 695 ++++++--------- sdk/src/utils/channel.ts | 6 +- sdk/test/unit/rpc/api.test.ts | 191 +++-- sdk/test/unit/rpc/nitrolite.test.ts | 152 +--- sdk/test/unit/rpc/utils.test.ts | 46 +- 45 files changed, 2872 insertions(+), 2612 deletions(-) create mode 100644 clearnode/rpc_router_callback.go create mode 100644 integration/tests/challenge_channel.test.ts create mode 100644 sdk/src/rpc/types/common.ts create mode 100644 sdk/src/rpc/types/filters.ts diff --git a/clearnode/app_session_service_test.go b/clearnode/app_session_service_test.go index f8a4f9f6a..e61e8d30e 100644 --- a/clearnode/app_session_service_test.go +++ b/clearnode/app_session_service_test.go @@ -36,7 +36,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { require.NoError(t, GetWalletLedger(db, userAddressB).Record(userAccountIDB, "usdc", decimal.NewFromInt(200))) capturedNotifications := make(map[string][]Notification) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) { + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -117,7 +117,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { require.NoError(t, db.Create(&SignerWallet{Signer: userAddressA.Hex(), Wallet: userAddressA.Hex()}).Error) require.NoError(t, GetWalletLedger(db, userAddressA).Record(userAccountIDA, "usdc", decimal.NewFromInt(50))) // Not enough - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) params := &CreateAppSessionParams{ Definition: AppDefinition{ Protocol: "test-proto", @@ -148,7 +148,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { Status: ChannelStatusChallenged, }).Error) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) params := &CreateAppSessionParams{ Definition: AppDefinition{ Protocol: "test-proto", @@ -175,7 +175,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { require.NoError(t, db.Create(&SignerWallet{Signer: userAddressA.Hex(), Wallet: userAddressA.Hex()}).Error) require.NoError(t, GetWalletLedger(db, userAddressA).Record(userAccountIDA, "usdc", decimal.NewFromInt(100))) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) params := &CreateAppSessionParams{ Definition: AppDefinition{ Protocol: "test-proto", @@ -208,7 +208,7 @@ func TestAppSessionService_SubmitAppState(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) session := &AppSession{ SessionID: "test-session", Protocol: "test-proto", @@ -258,7 +258,7 @@ func TestAppSessionService_SubmitAppState(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) session := &AppSession{ SessionID: "test-session-negative", Protocol: "test-proto", @@ -307,8 +307,7 @@ func TestAppSessionService_CloseApplication(t *testing.T) { defer cleanup() capturedNotifications := make(map[string][]Notification) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) { - + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, eventType: EventType(method), @@ -395,7 +394,7 @@ func TestAppSessionService_CloseApplication(t *testing.T) { defer cleanup() capturedNotifications := make(map[string][]Notification) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) { + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -457,7 +456,7 @@ func TestAppSessionService_CloseApplication(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) session := &AppSession{ SessionID: "test-session-close-negative", Protocol: "test-proto", diff --git a/clearnode/custody_test.go b/clearnode/custody_test.go index fb4940735..b16b3a34a 100644 --- a/clearnode/custody_test.go +++ b/clearnode/custody_test.go @@ -85,7 +85,7 @@ func setupMockCustody(t *testing.T) (*Custody, *gorm.DB, func()) { custody: contract, chainID: uint32(chainID.Int64()), adjudicatorAddress: newTestCommonAddress("0xAdjudicatorAddress"), - wsNotifier: NewWSNotifier(func(userID string, method string, params ...any) {}, logger), + wsNotifier: NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, logger), logger: logger, } @@ -310,7 +310,7 @@ func TestHandleCreatedEvent(t *testing.T) { } capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { if capturedNotifications[userID] == nil { capturedNotifications[userID] = make([]Notification, 0) } @@ -406,7 +406,7 @@ func TestHandleClosedEvent(t *testing.T) { _, mockEvent := createMockClosedEvent(t, custody.signer, tokenAddress, finalAmount) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -594,7 +594,7 @@ func TestHandleClosedEvent(t *testing.T) { _, mockEvent := createMockClosedEvent(t, custody.signer, tokenAddress, channelAmount.BigInt()) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -665,7 +665,7 @@ func TestHandleChallengedEvent(t *testing.T) { _, mockEvent := createMockChallengedEvent(t, custody.signer, tokenAddress, amount.BigInt()) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -741,7 +741,7 @@ func TestHandleResizedEvent(t *testing.T) { _, mockEvent := createMockResizedEvent(t, deltaAmount.BigInt()) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -901,7 +901,7 @@ func TestHandleResizedEvent(t *testing.T) { _, mockEvent := createMockResizedEvent(t, big.NewInt(500000)) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, diff --git a/clearnode/docs/API.md b/clearnode/docs/API.md index f2936294a..5a55be407 100644 --- a/clearnode/docs/API.md +++ b/clearnode/docs/API.md @@ -2,29 +2,29 @@ ## API Endpoints -| Method | Description | Access | -|--------|-------------|------------| -| `auth_request` | Initiates authentication with the server | Public | -| `auth_challenge` | Server response with authentication challenge | Public | -| `auth_verify` | Completes authentication with a challenge response | Public | -| `ping` | Simple connectivity check | Public | -| `get_config` | Retrieves broker configuration and supported networks | Public | -| `get_assets` | Retrieves all supported assets (optionally filtered by chain_id) | Public | -| `get_channels` | Lists all channels for a participant with their status across all chains | Public | -| `get_app_definition` | Retrieves application definition for a ledger account | Public | -| `get_app_sessions` | Lists virtual applications for a participant with optional status filter | Public | -| `get_ledger_entries` | Retrieves detailed ledger entries for a participant | Public | -| `get_ledger_transactions` | Retrieves transaction history with optional filtering | Public | -| `get_user_tag` | Retrieves user's tag| Private | -| `get_rpc_history` | Retrieves all RPC message history for a participant | Private | -| `get_ledger_balances` | Lists participants and their balances for a ledger account | Private | -| `transfer` | Transfers funds from user's unified balance to another account | Private | -| `create_app_session` | Creates a new virtual application on a ledger | Private | -| `submit_app_state` | Submits an intermediate state into a virtual application | Private | -| `close_app_session` | Closes a virtual application | Private | -| `create_channel` | Returns data and Broker signature to open a channel | Private | -| `close_channel` | Returns data and Broker signature to close a channel | Private | -| `resize_channel` | Returns data and Broker signature to adjust channel capacity | Private | +| Method | Description | Access | +| ------------------------- | ------------------------------------------------------------------------ | ------- | +| `auth_request` | Initiates authentication with the server | Public | +| `auth_challenge` | Server response with authentication challenge | Public | +| `auth_verify` | Completes authentication with a challenge response | Public | +| `ping` | Simple connectivity check | Public | +| `get_config` | Retrieves broker configuration and supported networks | Public | +| `get_assets` | Retrieves all supported assets (optionally filtered by chain_id) | Public | +| `get_channels` | Lists all channels for a participant with their status across all chains | Public | +| `get_app_definition` | Retrieves application definition for a ledger account | Public | +| `get_app_sessions` | Lists virtual applications for a participant with optional status filter | Public | +| `get_ledger_entries` | Retrieves detailed ledger entries for a participant | Public | +| `get_ledger_transactions` | Retrieves transaction history with optional filtering | Public | +| `get_user_tag` | Retrieves user's tag | Private | +| `get_rpc_history` | Retrieves all RPC message history for a participant | Private | +| `get_ledger_balances` | Lists participants and their balances for a ledger account | Private | +| `transfer` | Transfers funds from user's unified balance to another account | Private | +| `create_app_session` | Creates a new virtual application on a ledger | Private | +| `submit_app_state` | Submits an intermediate state into a virtual application | Private | +| `close_app_session` | Closes a virtual application | Private | +| `create_channel` | Returns data and Broker signature to open a channel | Private | +| `close_channel` | Returns data and Broker signature to close a channel | Private | +| `resize_channel` | Returns data and Broker signature to adjust channel capacity | Private | ## Authentication @@ -36,20 +36,20 @@ Initiates authentication with the server. ```json { - "req": [1, "auth_request", [{ + "req": [1, "auth_request", { "address": "0x1234567890abcdef...", "session_key": "0x9876543210fedcba...", // If specified, enables delegation to this key "app_name": "Example App", // Application name for analytics "allowances": [ // Asset allowances for the session - [ - "usdc", - "100.0" - ] + { + "asset": "usdc", + "amount": "100.0" + } ], "scope": "app.create", // Permission scope (e.g., "app.create", "ledger.readonly") "expire": "3600", // Session expiration "application": "0xApp1234567890abcdef..." // Application public address - }], 1619123456789], + }, 1619123456789], "sig": ["0x5432abcdef..."] // Client's signature of the entire 'req' object } ``` @@ -62,9 +62,9 @@ Server response with a challenge token for the client to sign. ```json { - "res": [1, "auth_challenge", [{ + "res": [1, "auth_challenge", { "challenge_message": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] // Server's signature of the entire 'res' object } ``` @@ -77,9 +77,9 @@ Completes authentication with a challenge response. ```json { - "req": [2, "auth_verify", [{ + "req": [2, "auth_verify", { "challenge": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x2345bcdef..."] // Client's EIP-712 signatures of the challenge data object } ``` @@ -88,11 +88,11 @@ Completes authentication with a challenge response. ```json { - "res": [2, "auth_verify", [{ + "res": [2, "auth_verify", { "address": "0x1234567890abcdef...", "success": true, "jwt_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." // JWT token for subsequent requests - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] // Server's signature of the entire 'res' object } ``` @@ -113,7 +113,7 @@ The JWT token has a default validity period of 24 hours and must be refreshed by ### Get Channels Retrieves all channels for a participant (both open, closed, and joining), ordered by creation date (newest first). This method returns channels across all supported chains. If no participant is specified, it returns all channels. -Supports pagination and sorting. +Supports pagination and sorting by providing optional request parameters and metadata fields in response. > Sorted descending by `created_at` by default. @@ -121,9 +121,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_channels", [{ - "participant": "0x1234567890abcdef...", - }], 1619123456789], + "req": [1, "get_channels", {}, 1619123456789], "sig": [] } ``` @@ -132,13 +130,13 @@ Supports pagination and sorting. ```json { - "req": [1, "get_channels", [{ - "participant": "0x1234567890abcdef...", + "req": [1, "get_channels", { + "participant": "0x1234567890abcdef...", // Optional: filter by participant "status":"open", // Optional filter "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of channels to return "sort": "desc" // Optional: sort asc or desc by created_at - }], 1619123456789], + }, 1619123456789], "sig": [] } ``` @@ -147,38 +145,46 @@ Supports pagination and sorting. ```json { - "res": [1, "get_channels", [[ - { - "channel_id": "0xfedcba9876543210...", - "participant": "0x1234567890abcdef...", - "wallet": "0x1234567890abcdef...", - "status": "open", - "token": "0xeeee567890abcdef...", - "amount": "100000", - "chain_id": 137, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 2, - "created_at": "2023-05-01T12:00:00Z", - "updated_at": "2023-05-01T12:30:00Z" - }, - { - "channel_id": "0xabcdef1234567890...", - "participant": "0x1234567890abcdef...", - "wallet": "0x1234567890abcdef...", - "status": "closed", - "token": "0xeeee567890abcdef...", - "amount": "50000", - "chain_id": 42220, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 3, - "created_at": "2023-04-15T10:00:00Z", - "updated_at": "2023-04-20T14:30:00Z" + "res": [1, "get_channels", { + "channels" : [ + { + "channel_id": "0xfedcba9876543210...", + "participant": "0x1234567890abcdef...", + "wallet": "0x1234567890abcdef...", + "status": "open", + "token": "0xeeee567890abcdef...", + "amount": "100000", + "chain_id": 137, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 2, + "created_at": "2023-05-01T12:00:00Z", + "updated_at": "2023-05-01T12:30:00Z" + }, + { + "channel_id": "0xabcdef1234567890...", + "participant": "0x1234567890abcdef...", + "wallet": "0x1234567890abcdef...", + "status": "closed", + "token": "0xeeee567890abcdef...", + "amount": "50000", + "chain_id": 42220, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 3, + "created_at": "2023-04-15T10:00:00Z", + "updated_at": "2023-04-20T14:30:00Z" + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -200,6 +206,13 @@ Each channel response includes: - `version`: Current version of the channel state - `created_at`: When the channel was created (ISO 8601 format) - `updated_at`: When the channel was last updated (ISO 8601 format) + +Metadata fields provide pagination information: + +- `page`: Current page number +- `per_page`: Number of channels per page +- `total_count`: Total number of channels available +- `page_count`: Total number of pages based on the `per_page` limit ### Get App Definition @@ -209,9 +222,9 @@ Retrieves the application definition for a specific ledger account. ```json { - "req": [1, "get_app_definition", [{ + "req": [1, "get_app_definition", { "app_session_id": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -220,19 +233,17 @@ Retrieves the application definition for a specific ledger account. ```json { - "res": [1, "get_app_definition", [ - { - "protocol": "NitroRPC/0.2", - "participants": [ - "0xAaBbCcDdEeFf0011223344556677889900aAbBcC", - "0x00112233445566778899AaBbCcDdEeFf00112233" - ], - "weights": [50, 50], - "quorum": 100, - "challenge": 86400, - "nonce": 1 - } - ], 1619123456789], + "res": [1, "get_app_definition", { + "protocol": "NitroRPC/0.2", + "participants": [ + "0xAaBbCcDdEeFf0011223344556677889900aAbBcC", + "0x00112233445566778899AaBbCcDdEeFf00112233" + ], + "weights": [50, 50], + "quorum": 100, + "challenge": 86400, + "nonce": 1 + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -248,9 +259,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_app_sessions", [{ - "participant": "0x1234567890abcdef..." - }], 1619123456789], + "req": [1, "get_app_sessions", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -259,13 +268,13 @@ Supports pagination and sorting. ```json { - "req": [1, "get_app_sessions", [{ - "participant": "0x1234567890abcdef...", + "req": [1, "get_app_sessions", { + "participant": "0x1234567890abcdef...", // Optional: filter by participant "status": "open", // Optional: filter by status "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of sessions to return "sort": "asc", // Optional: sort asc or desc - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -274,38 +283,46 @@ Supports pagination and sorting. ```json { - "res": [1, "get_app_sessions", [[ - { - "app_session_id": "0x3456789012abcdef...", - "status": "open", - "participants": [ - "0x1234567890abcdef...", - "0x00112233445566778899AaBbCcDdEeFf00112233" - ], - "session_data": "{\"gameType\":\"rps\",\"rounds\":5,\"currentRound\":3,\"scores\":{\"0x1234567890abcdef\":2,\"0x00112233445566778899AaBbCcDdEeFf00112233\":1}}", - "protocol": "NitroAura", - "challenge": 86400, - "weights": [50, 50], - "quorum": 100, - "version": 1, - "nonce": 123456789 - }, - { - "app_session_id": "0x7890123456abcdef...", - "status": "open", - "participants": [ - "0x1234567890abcdef...", - "0xAaBbCcDdEeFf0011223344556677889900aAbBcC" - ], - "session_data": "{\"gameType\":\"snake\",\"boardSize\":20,\"snakeLength\":5,\"score\":150,\"level\":3,\"gameState\":\"active\"}", - "protocol": "NitroSnake", - "challenge": 86400, - "weights": [70, 30], - "quorum": 100, - "version": 1, - "nonce": 123456790 + "res": [1, "get_app_sessions", { + "app_sessions" : [ + { + "app_session_id": "0x3456789012abcdef...", + "status": "open", + "participants": [ + "0x1234567890abcdef...", + "0x00112233445566778899AaBbCcDdEeFf00112233" + ], + "session_data": "{\"gameType\":\"rps\",\"rounds\":5,\"currentRound\":3,\"scores\":{\"0x1234567890abcdef\":2,\"0x00112233445566778899AaBbCcDdEeFf00112233\":1}}", + "protocol": "NitroAura", + "challenge": 86400, + "weights": [50, 50], + "quorum": 100, + "version": 1, + "nonce": 123456789 + }, + { + "app_session_id": "0x7890123456abcdef...", + "status": "open", + "participants": [ + "0x1234567890abcdef...", + "0xAaBbCcDdEeFf0011223344556677889900aAbBcC" + ], + "session_data": "{\"gameType\":\"snake\",\"boardSize\":20,\"snakeLength\":5,\"score\":150,\"level\":3,\"gameState\":\"active\"}", + "protocol": "NitroSnake", + "challenge": 86400, + "weights": [70, 30], + "quorum": 100, + "version": 1, + "nonce": 123456790 + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -318,11 +335,11 @@ Retrieves the balances of all participants in a specific ledger account. ```json { - "req": [1, "get_ledger_balances", [{ + "req": [1, "get_ledger_balances", { "participant": "0x1234567890abcdef...", // TO BE DEPRECATED // OR "account_id": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -334,16 +351,18 @@ To get balance in a specific virtual app session, specify `app_session_id` as ac ```json { - "res": [1, "get_ledger_balances", [[ - { - "asset": "usdc", - "amount": "100.0" - }, - { - "asset": "eth", - "amount": "0.5" - } - ]], 1619123456789], + "res": [1, "get_ledger_balances", { + "ledger_balances": [ + { + "asset": "usdc", + "amount": "100.0" + }, + { + "asset": "eth", + "amount": "0.5" + } + ], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -356,7 +375,7 @@ Retrieves the user's tag, which can be used for transfer operations. The tag is ```json { - "req": [1, "get_user_tag", [], 1619123456789], + "req": [1, "get_user_tag", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -365,13 +384,13 @@ Retrieves the user's tag, which can be used for transfer operations. The tag is ```json { - "res": [1, "get_user_tag", [ - { - "tag": "UX123D", - }], 1619123456789], + "res": [1, "get_user_tag", { + "tag": "UX123D", + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` + ### Transfer Funds This method allows a user to transfer assets from their unified balance to another account. The user must have sufficient funds for each asset being transferred. The operation will fail if any of the specified assets have insufficient funds. @@ -385,7 +404,7 @@ Currently, `Transfer` supports ledger account of another user as destination (wa ```json { - "req": [1, "transfer", [{ + "req": [1, "transfer", { "destination": "0x9876543210abcdef...", "allocations": [ { @@ -397,14 +416,14 @@ Currently, `Transfer` supports ledger account of another user as destination (wa "amount": "0.1" } ] - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } // OR { - "req": [1, "transfer", [{ + "req": [1, "transfer", { "destination_user_tag": "UX123D", "allocations": [ { @@ -416,7 +435,7 @@ Currently, `Transfer` supports ledger account of another user as destination (wa "amount": "0.1" } ] - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -425,34 +444,37 @@ Currently, `Transfer` supports ledger account of another user as destination (wa ```json { - "res": [1, "transfer", [[ - { - "id": "1", - "tx_type": "transfer", - "from_account": "0x1234567890abcdef...", - "from_account_tag": "NQKO7C", - "to_account": "0x9876543210abcdef...", - "to_account_tag": "UX123D", - "asset": "usdc", - "amount": "50.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": "2", - "tx_type": "transfer", - "from_account": "0x1234567890abcdef...", - "from_account_tag": "NQKO7C", - "to_account": "0x9876543210abcdef...", - "to_account_tag": "UX123D", - "asset": "eth", - "amount": "0.1", - "created_at": "2023-05-01T12:00:00Z" - } - ]], 1619123456789], + "res": [1, "transfer", { + "transactions" : [ + { + "id": "1", + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "usdc", + "amount": "50.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": "2", + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "eth", + "amount": "0.1", + "created_at": "2023-05-01T12:00:00Z" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` -The response returns an array of transaction objects, with one transaction for each asset being transferred. + +The response returns an array of transaction objects, with one transaction for each asset being transferred. Each transaction includes: @@ -477,7 +499,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_entries", [], 1619123456789], + "req": [1, "get_ledger_entries", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -486,14 +508,14 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_entries", [{ - "account_id": "0x1234567890abcdef...", + "req": [1, "get_ledger_entries", { + "account_id": "0x1234567890abcdef...", // Optional: filter by account ID "wallet": "0x1234567890abcdef...", // Optional: filter by participant "asset": "usdc", // Optional: filter by asset "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of entries to return "sort": "desc" // Optional: sort asc or desc by created_at - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -502,28 +524,36 @@ Supports pagination and sorting. ```json { - "res": [1, "get_ledger_entries", [[ - { - "id": 123, - "account_id": "0x1234567890abcdef...", - "account_type": 0, - "asset": "usdc", - "participant": "0x1234567890abcdef...", - "credit": "100.0", - "debit": "0.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": 124, - "account_id": "0x1234567890abcdef...", - "account_type": 0, - "asset": "usdc", - "participant": "0x1234567890abcdef...", - "credit": "0.0", - "debit": "25.0", - "created_at": "2023-05-01T14:30:00Z" + "res": [1, "get_ledger_entries", { + "ledger_entries": [ + { + "id": 123, + "account_id": "0x1234567890abcdef...", + "account_type": 0, + "asset": "usdc", + "participant": "0x1234567890abcdef...", + "credit": "100.0", + "debit": "0.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": 124, + "account_id": "0x1234567890abcdef...", + "account_type": 0, + "asset": "usdc", + "participant": "0x1234567890abcdef...", + "credit": "0.0", + "debit": "25.0", + "created_at": "2023-05-01T14:30:00Z" + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -536,6 +566,7 @@ Supports pagination and sorting. > Sorted descending by `created_at` by default. **Available Transaction Types:** + - `transfer`: Direct transfers between unified accounts - `deposit`: Funds deposited to a unified account - `withdrawal`: Funds withdrawn from a unified account @@ -546,11 +577,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_transactions", [{ - "account_id": "0x1234567890abcdef...", - "asset": "usdc", // Optional: filter by asset - "tx_type": "transfer" // Optional: filter by transaction type - }], 1619123456789], + "req": [1, "get_ledger_transactions", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -559,14 +586,14 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_transactions", [{ - "account_id": "0x1234567890abcdef...", + "req": [1, "get_ledger_transactions", { + "account_id": "0x1234567890abcdef...", // Optional: filter by account ID "asset": "usdc", // Optional: filter by asset "tx_type": "transfer", // Optional: filter by transaction type "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of transactions to return "sort": "desc" // Optional: sort asc or desc by created_at - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -575,35 +602,44 @@ Supports pagination and sorting. ```json { - "res": [1, "get_ledger_transactions", [[ - { - "id": "1", - "tx_type": "transfer", - "from_account": "0x1234567890abcdef...", - "from_account_tag": "NQKO7C", - "to_account": "0x9876543210abcdef...", - "to_account_tag": "UX123D", - "asset": "usdc", - "amount": "50.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": "2", - "tx_type": "deposit", - "from_account": "0x9876543210abcdef...", // Channel account - "from_account_tag": "", // Channel accounts does not have tags - "to_account": "0x1234567890abcdef...", - "to_account_tag": "UX123D", - "asset": "usdc", - "amount": "25.0", - "created_at": "2023-05-01T10:30:00Z" + "res": [1, "get_ledger_transactions", { + "ledger_transactions":[ + { + "id": "1", + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "usdc", + "amount": "50.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": "2", + "tx_type": "deposit", + "from_account": "0x9876543210abcdef...", // Channel account + "from_account_tag": "", // Channel accounts does not have tags + "to_account": "0x1234567890abcdef...", + "to_account_tag": "UX123D", + "asset": "usdc", + "amount": "25.0", + "created_at": "2023-05-01T10:30:00Z" + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` Each transaction response includes: + - `id`: Unique transaction id reference - `tx_type`: Transaction type (transfer/deposit/withdrawal/app_deposit/app_withdrawal) - `from_account`: The account that sent the funds @@ -624,7 +660,7 @@ Retrieves all RPC messages history for a participant, ordered by timestamp (newe ```json { - "req": [4, "get_rpc_history", [], 1619123456789], + "req": [4, "get_rpc_history", {}, 1619123456789], "sig": [] } ``` @@ -633,30 +669,32 @@ Retrieves all RPC messages history for a participant, ordered by timestamp (newe ```json { - "res": [4, "get_rpc_history", [[ - { - "id": 123, - "sender": "0x1234567890abcdef...", - "req_id": 42, - "method": "get_channels", - "params": "[{\"participant\":\"0x1234567890abcdef...\"}]", - "timestamp": 1619123456789, - "req_sig": ["0x9876fedcba..."], - "response": "{\"res\":[42,\"get_channels\",[[...]],1619123456799]}", - "res_sig": ["0xabcd1234..."] - }, - { - "id": 122, - "sender": "0x1234567890abcdef...", - "req_id": 41, - "method": "ping", - "params": "[null]", - "timestamp": 1619123446789, - "req_sig": ["0x8765fedcba..."], - "response": "{\"res\":[41,\"pong\",[],1619123446799]}", - "res_sig": ["0xdcba4321..."] - } - ]], 1619123456789], + "res": [4, "get_rpc_history", { + "rpc_entries": [ + { + "id": 123, + "sender": "0x1234567890abcdef...", + "req_id": 42, + "method": "get_channels", + "params": "[{\"participant\":\"0x1234567890abcdef...\"}]", + "timestamp": 1619123456789, + "req_sig": ["0x9876fedcba..."], + "response": "{\"res\":[42,\"get_channels\",[[...]],1619123456799]}", + "res_sig": ["0xabcd1234..."] + }, + { + "id": 122, + "sender": "0x1234567890abcdef...", + "req_id": 41, + "method": "ping", + "params": "[null]", + "timestamp": 1619123446789, + "req_sig": ["0x8765fedcba..."], + "response": "{\"res\":[41,\"pong\",[],1619123446799]}", + "res_sig": ["0xdcba4321..."] + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -674,7 +712,7 @@ The optional `session_data` field can be used to store application-specific data ```json { - "req": [1, "create_app_session", [{ + "req": [1, "create_app_session", { "definition": { "protocol": "NitroRPC/0.2", "participants": [ @@ -699,7 +737,7 @@ The optional `session_data` field can be used to store application-specific data } ], "session_data": "{\"gameType\":\"chess\",\"timeControl\":{\"initial\":600,\"increment\":5},\"maxPlayers\":2,\"gameState\":\"waiting\"}" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -708,14 +746,15 @@ The optional `session_data` field can be used to store application-specific data ```json { - "res": [1, "create_app_session", [{ + "res": [1, "create_app_session", { "app_session_id": "0x3456789012abcdef...", "version": "1", "status": "open" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` + ### Submit Application State Submits an intermediate state into a virtual application and redistributes funds in an app session. @@ -728,7 +767,7 @@ The optional `session_data` field can be used to update application-specific dat ```json { - "req": [1, "submit_app_state", [{ + "req": [1, "submit_app_state", { "app_session_id": "0x3456789012abcdef...", "allocations": [ { @@ -743,7 +782,7 @@ The optional `session_data` field can be used to update application-specific dat } ], "session_data": "{\"gameType\":\"chess\",\"timeControl\":{\"initial\":600,\"increment\":5},\"maxPlayers\":2,\"gameState\":\"finished\",\"winner\":\"0x00112233445566778899AaBbCcDdEeFf00112233\",\"endCondition\":\"checkmate\"}" // Optional - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba...", "0x8765fedcba..."] } ``` @@ -752,11 +791,11 @@ The optional `session_data` field can be used to update application-specific dat ```json { - "res": [1, "submit_app_state", [{ + "res": [1, "submit_app_state", { "app_session_id": "0x3456789012abcdef...", "version": "567", "status": "open" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -773,7 +812,7 @@ The optional `session_data` field can be used to provide final application-speci ```json { - "req": [1, "close_app_session", [{ + "req": [1, "close_app_session", { "app_session_id": "0x3456789012abcdef...", "allocations": [ { @@ -788,7 +827,7 @@ The optional `session_data` field can be used to provide final application-speci } ], "session_data": "{\"gameType\":\"chess\",\"timeControl\":{\"initial\":600,\"increment\":5},\"maxPlayers\":2,\"gameState\":\"closed\",\"winner\":\"0x00112233445566778899AaBbCcDdEeFf00112233\",\"endCondition\":\"checkmate\",\"moveHistory\":[\"e2e4\",\"e7e5\",\"Nf3\",\"Nc6\"]}" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba...", "0x8765fedcba..."] } ``` @@ -797,11 +836,11 @@ The optional `session_data` field can be used to provide final application-speci ```json { - "res": [1, "close_app_session", [{ + "res": [1, "close_app_session", { "app_session_id": "0x3456789012abcdef...", "version": "3", "status": "closed" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -876,16 +915,16 @@ The response includes: ### Close Channel To close a channel, the user must request the final state signed by the broker and then submit it to the smart contract. -Only an open channel can be closed. In case the user does not agree with the final state provided by the broker, they can call the `challenge` method directly on the smart contract. +Only an open channel can be closed. In case the user does not agree with the final state provided by the broker, they can call the `challenge` method directly on the smart contract. **Request:** ```json { - "req": [1, "close_channel", [{ + "req": [1, "close_channel", { "channel_id": "0x4567890123abcdef...", "funds_destination": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -896,7 +935,7 @@ In the request, the user must specify funds destination. After the channel is cl ```json { - "res": [1, "close_channel", [{ + "res": [1, "close_channel", { "channel_id": "0x4567890123abcdef...", "state": { "intent": 3, // IntentFINALIZE - constant specifying that this is a final state @@ -916,7 +955,7 @@ In the request, the user must specify funds destination. After the channel is cl ] }, "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -929,20 +968,21 @@ Adjusts the capacity of a channel. ```json { - "req": [1, "resize_channel", [{ + "req": [1, "resize_channel", { "channel_id": "0x4567890123abcdef...", "allocate_amount": "200000000", "resize_amount": "1000000000", "funds_destination": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` `allocate_amount` is how much more token user wants to allocate to this token-network specific channel from his unified balance. -`resize_amount` is how much user wants to deposit or withdraw from a token-network speecific channel. +`resize_amount` is how much user wants to deposit or withdraw from a token-network specific channel. Example: + - Initial state: user an open channel on Polygon with 20 usdc, and a channel on Celo with 5 usdc. - User wants to deposit 75 usdc on Celo. User calls `resize_channel`, with `allocate_amount=0` and `resize_amount=75`. - Now user's unified balance is 100 usdc (20 on Polygon and 80 on Celo). @@ -953,7 +993,7 @@ Example: ```json { - "res": [1, "resize_channel", [{ + "res": [1, "resize_channel", { "channel_id": "0x4567890123abcdef...", "state": { "intent": 2, // IntentRESIZE @@ -973,7 +1013,7 @@ Example: ] }, "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -990,9 +1030,9 @@ Sends a message to all participants in a virtual app session. ```json { - "req": [1, "your_custom_method", [{ + "req": [1, "your_custom_method", { "your_custom_field": "Hello, application participants!" - }], 1619123456789], + }, 1619123456789], "sid": "0x3456789012abcdef...", // Virtual App Session ID "sig": ["0x9876fedcba..."] } @@ -1004,9 +1044,9 @@ Responses can also be forwarded to all participants in a virtual application by ```json { - "res": [1, "your_custom_method", [{ + "res": [1, "your_custom_method", { "your_custom_field": "I confirm that I have received your message!" - }], 1619123456789], + }, 1619123456789], "sid": "0x3456789012abcdef...", // Virtual App Session ID "sig": ["0x9876fedcba..."] } @@ -1022,7 +1062,7 @@ Simple ping to check connectivity. ```json { - "req": [1, "ping", [], 1619123456789], + "req": [1, "ping", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -1031,7 +1071,7 @@ Simple ping to check connectivity. ```json { - "res": [1, "pong", [], 1619123456789], + "res": [1, "pong", {}, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1039,6 +1079,7 @@ Simple ping to check connectivity. ### Balance Updates The server automatically sends balance updates to clients in these scenarios: + 1. After successful authentication (as a welcome message) 2. After channel operations (open, close, resize) 3. After application operations (create, close) @@ -1047,16 +1088,18 @@ Balance updates are sent as unsolicited server messages with the "bu" method: ```json { - "res": [1234567890123, "bu", [[ - { - "asset": "usdc", - "amount": "100.0" - }, - { - "asset": "eth", - "amount": "0.5" - } - ]], 1619123456789], + "res": [1234567890123, "bu", { + "balance_updates": [ + { + "asset": "usdc", + "amount": "100.0" + }, + { + "asset": "eth", + "amount": "0.5" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1069,36 +1112,38 @@ The server automatically sends all open channels as a batch update to clients af ```json { - "res": [1234567890123, "channels", [[ - { - "channel_id": "0xfedcba9876543210...", - "participant": "0x1234567890abcdef...", - "status": "open", - "token": "0xeeee567890abcdef...", - "amount": "100000", - "chain_id": 137, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 2, - "created_at": "2023-05-01T12:00:00Z", - "updated_at": "2023-05-01T12:30:00Z" - }, - { - "channel_id": "0xabcdef1234567890...", - "participant": "0x1234567890abcdef...", - "status": "open", - "token": "0xeeee567890abcdef...", - "amount": "50000", - "chain_id": 42220, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 3, - "created_at": "2023-04-15T10:00:00Z", - "updated_at": "2023-04-20T14:30:00Z" - } - ]], 1619123456789], + "res": [1234567890123, "channels", { + "channels": [ + { + "channel_id": "0xfedcba9876543210...", + "participant": "0x1234567890abcdef...", + "status": "open", + "token": "0xeeee567890abcdef...", + "amount": "100000", + "chain_id": 137, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 2, + "created_at": "2023-05-01T12:00:00Z", + "updated_at": "2023-05-01T12:30:00Z" + }, + { + "channel_id": "0xabcdef1234567890...", + "participant": "0x1234567890abcdef...", + "status": "open", + "token": "0xeeee567890abcdef...", + "amount": "50000", + "chain_id": 42220, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 3, + "created_at": "2023-04-15T10:00:00Z", + "updated_at": "2023-04-20T14:30:00Z" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1106,6 +1151,7 @@ The server automatically sends all open channels as a batch update to clients af ### Channel Updates For channel updates, the server sends them in these scenarios: + 1. When a channel is created 2. When a channel's status changes (open, joined, closed) 3. When a channel is resized @@ -1114,7 +1160,7 @@ Individual channel updates are sent as unsolicited server messages with the "cu" ```json { - "res": [1234567890123, "cu", [{ + "res": [1234567890123, "cu", { "channel_id": "0xfedcba9876543210...", "participant": "0x1234567890abcdef...", "status": "open", @@ -1127,7 +1173,7 @@ Individual channel updates are sent as unsolicited server messages with the "cu" "version": 2, "created_at": "2023-05-01T12:00:00Z", "updated_at": "2023-05-01T12:30:00Z" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1142,30 +1188,32 @@ Transfer notifications are sent as unsolicited server messages with the "transfe ```json { - "res": [1234567890123, "tr", [[ - { - "id": "1", - "tx_type": "transfer", - "from_account": "0x9876543210abcdef...", - "from_account_tag": "ABC123", - "to_account": "0x1234567890abcdef...", - "to_account_tag": "XYZ789", - "asset": "usdc", - "amount": "50.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": "2", - "tx_type": "transfer", - "from_account": "0x9876543210abcdef...", - "from_account_tag": "ABC123", - "to_account": "0x1234567890abcdef...", - "to_account_tag": "XYZ789", - "asset": "weth", - "amount": "0.1", - "created_at": "2023-05-01T12:00:00Z" - } - ]], 1619123456789], + "res": [1234567890123, "tr", { + "transactions": [ + { + "id": "1", + "tx_type": "transfer", + "from_account": "0x9876543210abcdef...", + "from_account_tag": "ABC123", + "to_account": "0x1234567890abcdef...", + "to_account_tag": "XYZ789", + "asset": "usdc", + "amount": "50.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": "2", + "tx_type": "transfer", + "from_account": "0x9876543210abcdef...", + "from_account_tag": "ABC123", + "to_account": "0x1234567890abcdef...", + "to_account_tag": "XYZ789", + "asset": "weth", + "amount": "0.1", + "created_at": "2023-05-01T12:00:00Z" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1190,7 +1238,7 @@ Retrieves broker configuration information including supported networks. ```json { - "req": [1, "get_config", [], 1619123456789], + "req": [1, "get_config", {}, 1619123456789], "sig": [] } ``` @@ -1199,7 +1247,7 @@ Retrieves broker configuration information including supported networks. ```json { - "res": [1, "get_config", [{ + "res": [1, "get_config", { "broker_address": "0xbbbb567890abcdef...", "networks": [ { @@ -1218,7 +1266,7 @@ Retrieves broker configuration information including supported networks. "adjudicator_address":"0xCustodyContractAddress1..." } ] - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1233,7 +1281,7 @@ Retrieves all supported assets. Optionally, you can filter the assets by chain_i ```json { - "req": [1, "get_assets", [], 1619123456789], + "req": [1, "get_assets", {}, 1619123456789], "sig": [] } ``` @@ -1242,9 +1290,9 @@ Retrieves all supported assets. Optionally, you can filter the assets by chain_i ```json { - "req": [1, "get_assets", [{ + "req": [1, "get_assets", { "chain_id": 137 - }], 1619123456789], + }, 1619123456789], "sig": [] } ``` @@ -1253,24 +1301,28 @@ Retrieves all supported assets. Optionally, you can filter the assets by chain_i ```json { - "res": [1, "get_assets", [[{ - "token": "0xeeee567890abcdef...", - "chain_id": 137, - "symbol": "usdc", - "decimals": 6 - }, - { - "token": "0xffff567890abcdef...", - "chain_id": 137, - "symbol": "weth", - "decimals": 18 - }, - { - "token": "0xaaaa567890abcdef...", - "chain_id": 42220, - "symbol": "celo", - "decimals": 18 - }]], 1619123456789], + "res": [1, "get_assets", { + "assets": [ + { + "token": "0xeeee567890abcdef...", + "chain_id": 137, + "symbol": "usdc", + "decimals": 6 + }, + { + "token": "0xffff567890abcdef...", + "chain_id": 137, + "symbol": "weth", + "decimals": 18 + }, + { + "token": "0xaaaa567890abcdef...", + "chain_id": 42220, + "symbol": "celo", + "decimals": 18 + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1281,9 +1333,9 @@ When an error occurs, the server responds with an error message: ```json { - "res": [REQUEST_ID, "error", [{ + "res": [REQUEST_ID, "error", { "error": "Error message describing what went wrong" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` diff --git a/clearnode/docs/Clearnode.protocol.md b/clearnode/docs/Clearnode.protocol.md index 0c3545eaf..200d12842 100644 --- a/clearnode/docs/Clearnode.protocol.md +++ b/clearnode/docs/Clearnode.protocol.md @@ -1,11 +1,13 @@ # Clearnode Protocol Specification ## Overview + The Clearnode protocol is a system for managing payment channels and virtual applications between participants. It provides a secure, efficient way to conduct transactions off-chain while retaining the ability to settle on-chain when required, with support for multiple blockchain networks. ## Protocol Flow ### 1. Blockchain Channels and Credit + - The protocol accepts blockchain channels to credit participants' balances in the database ledger - Participants create on-chain channels through custody contracts (supported on multiple chains including Polygon, Celo, and Base) - Channel creation events from the blockchain are received through webhooks and processed by the `EventHandler` @@ -13,6 +15,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app - Each participant has an `Account` in the ledger tied to their address. ### 2. Virtual Application Creation + - After being credited from on-chain channels, participants can create virtual applications with other participants - Virtual applications allow participants to allocate a portion of their balance for peer-to-peer transactions without requiring on-chain operations - The broker validates that: @@ -25,11 +28,13 @@ The Clearnode protocol is a system for managing payment channels and virtual app - The broker sets up message routing between participants ### 3. Virtual Application Operations + - Participants send both requests and responses to each other through virtual applications using WebSocket connections - Any message (request or response) with an AppID specified is forwarded to all other participants - The broker maintains a real-time bidirectional communication layer for message routing ### 4. Virtual Application Closure and Settlement + - When participants wish to close a virtual application, authorized signers must provide signatures that meet the quorum threshold - The broker validates the signatures against the list of authorized signers and their weights registered during application creation - The broker validates the final allocation of funds between participants @@ -43,6 +48,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app ## Security Features ### Authentication and Authorization + - All operations are authenticated using cryptographic signatures - The system uses ECDSA signatures compatible with Ethereum accounts - Virtual applications implement a multi-signature scheme: @@ -61,6 +67,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app - Tokens use ES256 signatures for verification ### Multi-Chain Support + - The system supports multiple blockchain networks (currently Polygon, Celo, and Base) - Each network has its own custody contract address and connection details - Chain IDs are tracked with channels to ensure proper chain association @@ -72,6 +79,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app - The `get_channels` method returns all channels for a participant across all supported chains ## Benefits + - Efficient, low-cost transactions by keeping most operations off-chain - Security guarantees of blockchain when needed - Participants can freely transact within their allocated funds in virtual applications @@ -89,7 +97,7 @@ All messages exchanged between clients and clearnodes follow this standardized f ```json { - "req": [REQUEST_ID, METHOD, [PARAMETERS], TIMESTAMP], + "req": [REQUEST_ID, METHOD, PARAMETERS, TIMESTAMP], "sid": "APP_SESSION_ID", // AppId for Virtual Ledgers for Internal Communication "sig": ["SIGNATURE"] // Client's signature of the entire "req" object } @@ -102,7 +110,7 @@ All messages exchanged between clients and clearnodes follow this standardized f ```json { - "res": [REQUEST_ID, METHOD, [RESPONSE_DATA], TIMESTAMP], + "res": [REQUEST_ID, METHOD, RESPONSE_DATA, TIMESTAMP], "sid": "APP_SESSION_ID", // AppId for Virtual Ledgers for Internal Communication "sig": ["SIGNATURE"] } @@ -111,14 +119,13 @@ All messages exchanged between clients and clearnodes follow this standardized f - The `sid` field serves as both the subject and destination pubsub topic for the message. There is a one-to-one mapping between topics and ledger accounts. - The `sig` field contains one or more signatures, of the `res` data. - The structure breakdown: - `REQUEST_ID`: A unique identifier for the request/response pair (`uint64`) - `METHOD`: The name of the method being called (`string`) -- `PARAMETERS`/`RESPONSE_DATA`: An array of parameters/response data (`[]any`) +- `PARAMETERS`/`RESPONSE_DATA`: An object of parameters/response data (`map[string]any`) - `TIMESTAMP`: Unix timestamp of the request/response in milliseconds (`uint64`) -- `APP_SESSION_ID` (`sid`): If specified, the message gets forwarded to all participants of a virtual app with thosn AppSessionID. +- `APP_SESSION_ID` (`sid`): If specified, the message gets forwarded to all participants of a virtual app with those AppSessionID. - `SIGNATURE`: Cryptographic signatures of the message (`[]string`). Multiple signatures may be required for certain operations. ## Data Types @@ -149,7 +156,7 @@ The client initiates authentication by sending an `auth_request` request with th ```json { - "req": [1, "auth_request", [{ + "req": [1, "auth_request", { "address": "0x1234567890abcdef...", "session_key": "0x9876543210fedcba...", // Optional: If specified, enables delegation "app_name": "Example App", // Optional: Application name @@ -162,7 +169,7 @@ The client initiates authentication by sending an `auth_request` request with th "scope": "app.create", // Optional: Permission scope "expire": "24h", // Optional: Session expiration time "application": "0xApplication1234..." // Optional: Application public address - }], 1619123456789], + }, 1619123456789], "sig": ["0x5432abcdef..."] } ``` @@ -173,9 +180,9 @@ The server responds with a random string challenge token. ```json { - "res": [1, "auth_challenge", [{ + "res": [1, "auth_challenge", { "challenge_message": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -186,10 +193,10 @@ The client sends a verification request with the challenge token signed by the c ```json { - "req": [2, "auth_verify", [{ + "req": [2, "auth_verify", { "address": "0x1234567890abcdef...", "challenge": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x2345bcdef..."] } ``` @@ -206,16 +213,17 @@ If authentication is successful, the server responds with a success confirmation ```json { - "res": [2, "auth_verify", [{ + "res": [2, "auth_verify", { "address": "0x1234567890abcdef...", "success": true, "jwt_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." // JWT token for session management - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` The JWT token contains: + - Policy information (wallet, participant, scope, application) - Asset allowances (if specified) - Standard JWT claims (expiration, issuer, etc.) @@ -255,9 +263,9 @@ When an error occurs, the server responds with an error message: ```json { - "res": [REQUEST_ID, "error", [{ + "res": [REQUEST_ID, "error", { "error": "Error message describing what went wrong" - }], TIMESTAMP], + }, TIMESTAMP], "sig": ["SIGNATURE"] } ``` @@ -304,4 +312,4 @@ When an error occurs, the server responds with an error message: - Never reuse signatures across different sessions or services - Verify all message signatures from the server before processing - Ensure your private key is securely stored and never exposed - - Generate a fresh unique identifier client-side for each request ID \ No newline at end of file + - Generate a fresh unique identifier client-side for each request ID diff --git a/clearnode/notification.go b/clearnode/notification.go index 1a6eed696..6d7569343 100644 --- a/clearnode/notification.go +++ b/clearnode/notification.go @@ -9,11 +9,11 @@ import ( ) type WSNotifier struct { - notify func(userID string, method string, params ...any) + notify func(userID string, method string, params RPCDataParams) logger Logger } -func NewWSNotifier(notifyFunc func(userID string, method string, params ...any), logger Logger) *WSNotifier { +func NewWSNotifier(notifyFunc func(userID string, method string, params RPCDataParams), logger Logger) *WSNotifier { return &WSNotifier{ notify: notifyFunc, logger: logger, @@ -55,7 +55,7 @@ func NewBalanceNotification(wallet string, db *gorm.DB) *Notification { return &Notification{ userID: wallet, eventType: BalanceUpdateEventType, - data: balances, + data: BalanceUpdatesResponse{BalanceUpdates: balances}, } } @@ -82,7 +82,7 @@ func NewChannelNotification(channel Channel) *Notification { } // NewTransferNotification creates a notification for a transfer event -func NewTransferNotification(wallet string, transferredAllocations []TransactionResponse) *Notification { +func NewTransferNotification(wallet string, transferredAllocations TransferResponse) *Notification { return &Notification{ userID: wallet, eventType: TransferEventType, diff --git a/clearnode/reconcile_cli.go b/clearnode/reconcile_cli.go index b3b546248..230adee07 100644 --- a/clearnode/reconcile_cli.go +++ b/clearnode/reconcile_cli.go @@ -60,7 +60,7 @@ func runReconcileCli(logger Logger) { custody, err := NewCustody( signer, db, - NewWSNotifier(func(userID, method string, params ...any) {}, logger), + NewWSNotifier(func(userID, method string, params RPCDataParams) {}, logger), network.InfuraURL, network.CustodyAddress, network.AdjudicatorAddress, diff --git a/clearnode/rpc.go b/clearnode/rpc.go index 9e373853c..3a4d8686b 100644 --- a/clearnode/rpc.go +++ b/clearnode/rpc.go @@ -38,13 +38,16 @@ func (r RPCMessage) GetRequestSignersMap() (map[string]struct{}, error) { return recoveredAddresses, nil } +// TODO: ensure that it accepts only structs or maps, and prevent passing primitive (and other DS) types +type RPCDataParams = any + // RPCData represents the common structure for both requests and responses // Format: [request_id, method, params, ts] type RPCData struct { - RequestID uint64 `json:"request_id" validate:"required"` - Method string `json:"method" validate:"required"` - Params []any `json:"params" validate:"required"` - Timestamp uint64 `json:"ts" validate:"required"` + RequestID uint64 `json:"request_id" validate:"required"` + Method string `json:"method" validate:"required"` + Params RPCDataParams `json:"params" validate:"required"` + Timestamp uint64 `json:"ts" validate:"required"` rawBytes []byte } @@ -66,7 +69,7 @@ func (m *RPCData) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(rawArr[1], &m.Method); err != nil { return fmt.Errorf("invalid method: %w", err) } - // Element 2: []any Params + // Element 2: RPCDataParams Params if err := json.Unmarshal(rawArr[2], &m.Params); err != nil { return fmt.Errorf("invalid params: %w", err) } @@ -92,7 +95,7 @@ func (m RPCData) MarshalJSON() ([]byte, error) { } // CreateResponse is unchanged. It simply constructs an RPCMessage with a "res" array. -func CreateResponse(id uint64, method string, responseParams []any) *RPCMessage { +func CreateResponse(id uint64, method string, responseParams RPCDataParams) *RPCMessage { return &RPCMessage{ Res: &RPCData{ RequestID: id, diff --git a/clearnode/rpc_node.go b/clearnode/rpc_node.go index 81256de77..45618aedb 100644 --- a/clearnode/rpc_node.go +++ b/clearnode/rpc_node.go @@ -240,7 +240,7 @@ type RPCHandler func(c *RPCContext) // SendRPCMessageFunc is a function type for sending RPC notifications to a connection. // It's provided to event handlers to allow server-initiated messages. -type SendRPCMessageFunc func(method string, params ...any) +type SendRPCMessageFunc func(method string, params RPCDataParams) // RPCContext contains all the information about an RPC request and provides // methods for handlers to process and respond to the request. @@ -274,7 +274,7 @@ func (c *RPCContext) Next() { // Succeed sets a successful response with the given method and parameters. // This should be called by handlers to indicate successful processing. -func (c *RPCContext) Succeed(method string, params ...any) { +func (c *RPCContext) Succeed(method string, params RPCDataParams) { c.Message.Res = &RPCData{ RequestID: c.Message.Req.RequestID, Method: method, @@ -324,7 +324,7 @@ func (c *RPCContext) Fail(err error, fallbackMessage string) { c.Message.Res = &RPCData{ RequestID: c.Message.Req.RequestID, Method: "error", - Params: []any{message}, + Params: ErrorResponse{Error: message}, Timestamp: uint64(time.Now().UnixMilli()), } } @@ -366,9 +366,9 @@ func prepareRawRPCResponse(signer *Signer, data *RPCData) ([]byte, error) { // prepareRawNotification creates a signed server-initiated notification message. // Unlike responses, notifications don't correspond to a specific request. -func prepareRawNotification(signer *Signer, method string, params ...any) ([]byte, error) { +func prepareRawNotification(signer *Signer, method string, params RPCDataParams) ([]byte, error) { if params == nil { - params = []any{} + params = struct{}{} } data := &RPCData{ @@ -463,8 +463,8 @@ func (wn *RPCNode) OnAuthenticated(handler func(userID string, send SendRPCMessa // Notify sends a server-initiated notification to a specific authenticated user. // If the user is not connected, the notification is silently dropped. -func (wn *RPCNode) Notify(userID, method string, params ...any) { - message, err := prepareRawNotification(wn.signer, method, params...) +func (wn *RPCNode) Notify(userID, method string, params RPCDataParams) { + message, err := prepareRawNotification(wn.signer, method, params) if err != nil { wn.logger.Error("failed to prepare notification message", "error", err, "userID", userID, "method", method) return @@ -476,8 +476,8 @@ func (wn *RPCNode) Notify(userID, method string, params ...any) { // getSendMessageFunc creates a SendRPCMessageFunc for a specific connection. // The returned function can be used to send notifications to that connection. func (wn *RPCNode) getSendMessageFunc(conn *RPCConnection) SendRPCMessageFunc { - return func(method string, params ...any) { - message, err := prepareRawNotification(wn.signer, method, params...) + return func(method string, params RPCDataParams) { + message, err := prepareRawNotification(wn.signer, method, params) if err != nil { wn.logger.Error("failed to prepare notification message", "error", err, "method", method) return @@ -506,7 +506,7 @@ func (wn *RPCNode) sendErrorResponse(conn *RPCConnection, requestID uint64, mess data := &RPCData{ RequestID: requestID, Method: "error", - Params: []any{message}, + Params: ErrorResponse{Error: message}, Timestamp: uint64(time.Now().UnixMilli()), } diff --git a/clearnode/rpc_node_test.go b/clearnode/rpc_node_test.go index bd013069c..e33fbabb9 100644 --- a/clearnode/rpc_node_test.go +++ b/clearnode/rpc_node_test.go @@ -37,6 +37,15 @@ func TestRPCNode(t *testing.T) { previousExecMethodKey := "previous_exec_method" authMethod := "auth.test" + paramsUserIDKey := "userID" + paramsPrevMethodKey := "prev" + paramsRootMwKey := "rootMw" + paramsGroupAMwKey := "groupA" + paramsGroupBMwKey := "groupB" + paramsErrorKey := "error" + paramsOnConnectCounts := "onConnectCounts" + paramsOnAuthCounts := "onAuthCounts" + onConnectMethod := "on_connect.test" onConnectCounts := 0 node.OnConnect(func(send SendRPCMessageFunc) { @@ -44,7 +53,10 @@ func TestRPCNode(t *testing.T) { defer mu.Unlock() onConnectCounts++ - send(onConnectMethod, onConnectCounts) + params := map[string]any{ + paramsOnConnectCounts: onConnectCounts, + } + send(onConnectMethod, params) }) onDisconnectSignal := newTestSignal() @@ -65,7 +77,11 @@ func TestRPCNode(t *testing.T) { defer mu.Unlock() onAuthenticatedCounts++ - send(onAuthenticatedMethod, onAuthenticatedCounts, userID) + params := map[string]any{ + paramsOnAuthCounts: onAuthenticatedCounts, + paramsUserIDKey: userID, + } + send(onAuthenticatedMethod, params) }) onMessageSentSignal := newTestSignal() @@ -110,7 +126,14 @@ func TestRPCNode(t *testing.T) { groupBMwValue = false } } - c.Succeed(method, c.UserID, prevMethod, rootMwValue, groupAMwValue, groupBMwValue) + params := map[string]any{ + paramsUserIDKey: c.UserID, + paramsPrevMethodKey: prevMethod, + paramsRootMwKey: rootMwValue, + paramsGroupAMwKey: groupAMwValue, + paramsGroupBMwKey: groupBMwValue, + } + c.Succeed(method, params) c.Storage.Set(previousExecMethodKey, method) } } @@ -128,7 +151,10 @@ func TestRPCNode(t *testing.T) { node.Handle(rootMethod, createDummyHandler(rootMethod)) node.Handle(authMethod, func(c *RPCContext) { logger.Debug("executing auth handler") - c.Succeed(authMethod, authenticatedUserID) + params := map[string]any{ + paramsUserIDKey: authenticatedUserID, + } + c.Succeed(authMethod, params) c.UserID = authenticatedUserID // Simulate authenticated user }) @@ -177,7 +203,7 @@ func TestRPCNode(t *testing.T) { } // Helper function to send request and receive response - sendAndReceive := func(t *testing.T, RequestID uint64, method string, params ...interface{}) *RPCMessage { + sendAndReceive := func(t *testing.T, RequestID uint64, method string, params RPCDataParams) *RPCMessage { if params == nil { params = []interface{}{} } @@ -218,7 +244,7 @@ func TestRPCNode(t *testing.T) { // Test root handler t.Run("root handler", func(t *testing.T) { - resp := sendAndReceive(t, 1, rootMethod) + resp := sendAndReceive(t, 1, rootMethod, nil) mu.Lock() defer mu.Unlock() @@ -227,17 +253,21 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, rootMethod, resp.Res.Method) assert.Len(t, resp.Res.Params, 5) assert.Len(t, resp.Sig, 1) - assert.Equal(t, "", resp.Res.Params[0]) // not authenticated - assert.Equal(t, "", resp.Res.Params[1]) // previous dummy method empty - assert.Equal(t, true, resp.Res.Params[2]) // root middleware executed - assert.Equal(t, false, resp.Res.Params[3]) // group A middleware not executed - assert.Equal(t, false, resp.Res.Params[4]) // group B middleware not executed - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, "", params[paramsUserIDKey]) // not authenticated + assert.Equal(t, "", params[paramsPrevMethodKey]) // previous dummy method empty + assert.Equal(t, true, params[paramsRootMwKey]) // root middleware executed + assert.Equal(t, false, params[paramsGroupAMwKey]) // group A middleware not executed + assert.Equal(t, false, params[paramsGroupBMwKey]) // group B middleware not executed + assert.True(t, onMessageSentSignal.await()) // on message sent signal }) // Test auth handler t.Run("auth handler", func(t *testing.T) { - resp := sendAndReceive(t, 1, authMethod) + resp := sendAndReceive(t, 1, authMethod, nil) // So we definitely receive both authMethod and onAuthenticatedMethod time.Sleep(100 * time.Millisecond) @@ -247,8 +277,12 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, authMethod, resp.Res.Method) assert.Len(t, resp.Res.Params, 1) assert.Len(t, resp.Sig, 1) - assert.Equal(t, authenticatedUserID, resp.Res.Params[0]) // authenticated user ID - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // authenticated user ID + assert.True(t, onMessageSentSignal.await()) // on message sent signal mu.Unlock() // on authenticated method executed @@ -259,15 +293,20 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, onAuthenticatedMethod, resp.Res.Method) assert.Len(t, resp.Res.Params, 2) assert.Len(t, resp.Sig, 1) - assert.Equal(t, 1, onAuthenticatedCounts) // on authenticated counts - assert.Equal(t, authenticatedUserID, resp.Res.Params[1]) // authenticated user ID - assert.True(t, onMessageSentSignal.await()) // on message sent signal + assert.Equal(t, 1, onAuthenticatedCounts) // on authenticated counts + + params, ok = resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // authenticated user ID + assert.True(t, onMessageSentSignal.await()) // on message sent signal + mu.Unlock() }) // Test group handler 1 t.Run("group handler 1", func(t *testing.T) { - resp := sendAndReceive(t, 2, groupMethodA) + resp := sendAndReceive(t, 2, groupMethodA, nil) mu.Lock() defer mu.Unlock() @@ -276,17 +315,21 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, groupMethodA, resp.Res.Method) assert.Len(t, resp.Res.Params, 5) assert.Len(t, resp.Sig, 1) - assert.Equal(t, authenticatedUserID, resp.Res.Params[0]) // this method - assert.Equal(t, rootMethod, resp.Res.Params[1]) // previous dummy method root - assert.Equal(t, true, resp.Res.Params[2]) // root middleware executed - assert.Equal(t, true, resp.Res.Params[3]) // group A middleware executed - assert.Equal(t, false, resp.Res.Params[4]) // group B middleware not executed - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // this method + assert.Equal(t, rootMethod, params[paramsPrevMethodKey]) // previous dummy method root + assert.Equal(t, true, params[paramsRootMwKey]) // root middleware executed + assert.Equal(t, true, params[paramsGroupAMwKey]) // group A middleware executed + assert.Equal(t, false, params[paramsGroupBMwKey]) // group B middleware not executed + assert.True(t, onMessageSentSignal.await()) // on message sent signal }) // Test group handler 2 t.Run("group handler 2", func(t *testing.T) { - resp := sendAndReceive(t, 3, groupMethodB) + resp := sendAndReceive(t, 3, groupMethodB, nil) mu.Lock() defer mu.Unlock() @@ -295,17 +338,21 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, groupMethodB, resp.Res.Method) assert.Len(t, resp.Res.Params, 5) assert.Len(t, resp.Sig, 1) - assert.Equal(t, authenticatedUserID, resp.Res.Params[0]) // this method - assert.Equal(t, groupMethodA, resp.Res.Params[1]) // previous dummy method root - assert.Equal(t, true, resp.Res.Params[2]) // root middleware executed - assert.Equal(t, true, resp.Res.Params[3]) // group A middleware executed - assert.Equal(t, true, resp.Res.Params[4]) // group B middleware executed - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // this method + assert.Equal(t, groupMethodA, params[paramsPrevMethodKey]) // previous dummy method root + assert.Equal(t, true, params[paramsRootMwKey]) // root middleware executed + assert.Equal(t, true, params[paramsGroupAMwKey]) // group A middleware executed + assert.Equal(t, true, params[paramsGroupBMwKey]) // group B middleware executed + assert.True(t, onMessageSentSignal.await()) // on message sent signal }) // Test unknown method t.Run("unknown method", func(t *testing.T) { - resp := sendAndReceive(t, 4, "unknown.method") + resp := sendAndReceive(t, 4, "unknown.method", nil) mu.Lock() defer mu.Unlock() @@ -313,7 +360,11 @@ func TestRPCNode(t *testing.T) { require.NotNil(t, resp.Res) assert.Equal(t, "error", resp.Res.Method) assert.Len(t, resp.Res.Params, 1) - assert.Contains(t, resp.Res.Params[0], "unknown method") + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Contains(t, params[paramsErrorKey], "unknown method") assert.True(t, onMessageSentSignal.await()) // on message sent signal }) @@ -333,7 +384,11 @@ func TestRPCNode(t *testing.T) { require.NotNil(t, respMsg.Res) assert.Equal(t, "error", respMsg.Res.Method) - assert.Contains(t, respMsg.Res.Params[0], "invalid message format") + + params, ok := respMsg.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Contains(t, params[paramsErrorKey], "invalid message format") assert.True(t, onMessageSentSignal.await()) // on message sent signal }) diff --git a/clearnode/rpc_router.go b/clearnode/rpc_router.go index 7f0d13996..c4d2fbbb1 100644 --- a/clearnode/rpc_router.go +++ b/clearnode/rpc_router.go @@ -109,12 +109,12 @@ func (r *RPCRouter) HandleConnect(send SendRPCMessageFunc) { } // Convert to AssetResponse format - response := make([]GetAssetsResponse, 0, len(assets)) + respAssets := make([]AssetResponse, 0, len(assets)) for _, asset := range assets { - response = append(response, GetAssetsResponse(asset)) + respAssets = append(respAssets, AssetResponse(asset)) } - send("assets", response) + send("assets", AssetsResponse{Assets: respAssets}) } func (r *RPCRouter) HandleDisconnect(userID string) { @@ -130,9 +130,9 @@ func (r *RPCRouter) HandleAuthenticated(userID string, send SendRPCMessageFunc) r.lg.Error("error retrieving channels for participant", "error", err) } - resp := []ChannelResponse{} + respChannels := []ChannelResponse{} for _, ch := range channels { - resp = append(resp, ChannelResponse{ + respChannels = append(respChannels, ChannelResponse{ ChannelID: ch.ChannelID, Participant: ch.Participant, Status: ch.Status, @@ -149,7 +149,7 @@ func (r *RPCRouter) HandleAuthenticated(userID string, send SendRPCMessageFunc) } // Send channel updates - send("channels", resp) + send("channels", ChannelsResponse{Channels: respChannels}) // Send initial balances balances, err := GetWalletLedger(r.DB, common.HexToAddress(walletAddress)).GetBalances(NewAccountID(walletAddress)) @@ -157,7 +157,7 @@ func (r *RPCRouter) HandleAuthenticated(userID string, send SendRPCMessageFunc) r.lg.Error("error getting balances", "sender", walletAddress, "error", err) return } - send("bu", balances) + send("bu", BalanceUpdatesResponse{BalanceUpdates: balances}) } func (r *RPCRouter) HandleMessageSent() { @@ -236,16 +236,16 @@ func (r *RPCRouter) HistoryMiddleware(c *RPCContext) { } } -func parseParams(params []any, unmarshalTo any) error { - if len(params) > 0 { - paramsJSON, err := json.Marshal(params[0]) - if err != nil { - return fmt.Errorf("failed to parse parameters: %w", err) - } - err = json.Unmarshal(paramsJSON, &unmarshalTo) - if err != nil { - return err - } +func parseParams(params RPCDataParams, unmarshalTo any) error { + paramsJSON, err := json.Marshal(params) + if err != nil { + return fmt.Errorf("failed to parse parameters: %w", err) } + + err = json.Unmarshal(paramsJSON, &unmarshalTo) + if err != nil { + return err + } + return getValidator().Struct(unmarshalTo) } diff --git a/clearnode/rpc_router_auth.go b/clearnode/rpc_router_auth.go index 16fd29e9b..7e2fa0c47 100644 --- a/clearnode/rpc_router_auth.go +++ b/clearnode/rpc_router_auth.go @@ -10,13 +10,13 @@ import ( ) type AuthRequestParams struct { - Address string `json:"address"` // The wallet address requesting authentication - SessionKey string `json:"session_key"` // The session key for the authentication - AppName string `json:"app_name"` // The name of the application requesting authentication - Allowances []Allowance `json:"allowances"` // Allowances for the application - Expire string `json:"expire"` // Expiration time for the authentication - Scope string `json:"scope"` // Scope of the authentication - ApplicationAddress string `json:"application_address"` // The address of the application requesting authentication + Address string `json:"address"` // The wallet address requesting authentication + SessionKey string `json:"session_key"` // The session key for the authentication + AppName string `json:"app_name"` // The name of the application requesting authentication + Allowances []Allowance `json:"allowances"` // Allowances for the application + Expire string `json:"expire"` // Expiration time for the authentication + Scope string `json:"scope"` // Scope of the authentication + ApplicationAddress string `json:"application"` // The address of the application requesting authentication } // AuthResponse represents the server's challenge response @@ -40,69 +40,11 @@ func (r *RPCRouter) HandleAuthRequest(c *RPCContext) { // Parse the parameters var authParams AuthRequestParams - // TODO: change params format to a single object and use the commented code below - // if err := parseParams(req.Params, &authParams); err != nil { - // c.Fail(err.Error()) - // return - // } - - if len(req.Params) < 7 { - c.Fail(nil, "invalid parameters: expected 7 parameters") - return - } - - addr, ok := req.Params[0].(string) - if !ok || addr == "" { - c.Fail(nil, fmt.Sprintf("invalid address: %v", req.Params[0])) - return - } - - sessionKey, ok := req.Params[1].(string) - if !ok || sessionKey == "" { - c.Fail(nil, fmt.Sprintf("invalid session key: %v", req.Params[1])) - return - } - - appName, ok := req.Params[2].(string) - if !ok || appName == "" { - c.Fail(nil, fmt.Sprintf("invalid application name: %v", req.Params[2])) - return - } - - rawAllowances := req.Params[3] - allowances, err := parseAllowances(rawAllowances) - if err != nil { - c.Fail(err, fmt.Sprintf("invalid allowances: %s", err.Error())) - return - } - - expire, ok := req.Params[4].(string) - if !ok { - c.Fail(nil, fmt.Sprintf("invalid expiration time: %v", req.Params[4])) - return - } - - scope, ok := req.Params[5].(string) - if !ok { - c.Fail(nil, fmt.Sprintf("invalid scope: %v", req.Params[5])) + if err := parseParams(req.Params, &authParams); err != nil { + c.Fail(err, "failed to parse auth parameters") return } - applicationAddress, ok := req.Params[6].(string) - if !ok { - c.Fail(nil, fmt.Sprintf("invalid application address: %v", req.Params[6])) - } - - authParams = AuthRequestParams{ - Address: addr, - SessionKey: sessionKey, - AppName: appName, - Allowances: allowances, - Expire: expire, - Scope: scope, - ApplicationAddress: applicationAddress, - } - logger.Debug("incoming auth request", "addr", authParams.Address, "sessionKey", authParams.SessionKey, @@ -309,35 +251,3 @@ func ValidateTimestamp(ts uint64, expirySeconds int) error { } return nil } - -func parseAllowances(rawAllowances any) ([]Allowance, error) { - outerSlice, ok := rawAllowances.([]interface{}) - if !ok { - return nil, fmt.Errorf("input is not a list of allowances") - } - - result := make([]Allowance, len(outerSlice)) - - for i, item := range outerSlice { - innerSlice, ok := item.([]interface{}) - if !ok { - return nil, fmt.Errorf("allowance at index %d is not a list", i) - } - if len(innerSlice) != 2 { - return nil, fmt.Errorf("allowance at index %d must have exactly 2 elements (asset, amount)", i) - } - - asset, ok1 := innerSlice[0].(string) - amount, ok2 := innerSlice[1].(string) - if !ok1 || !ok2 { - return nil, fmt.Errorf("allowance at index %d has non-string asset or amount", i) - } - - result[i] = Allowance{ - Asset: asset, - Amount: amount, - } - } - - return result, nil -} diff --git a/clearnode/rpc_router_callback.go b/clearnode/rpc_router_callback.go new file mode 100644 index 000000000..ce5c29dfd --- /dev/null +++ b/clearnode/rpc_router_callback.go @@ -0,0 +1,66 @@ +package main + +import ( + "time" + + "github.com/ethereum/go-ethereum/common" +) + +type BalanceUpdatesResponse struct { + BalanceUpdates []Balance `json:"balance_updates"` +} + +type ChannelsResponse struct { + Channels []ChannelResponse `json:"channels"` +} + +type AssetsResponse struct { + Assets []AssetResponse `json:"assets"` +} + +// SendBalanceUpdate sends balance updates to the client +func (r *RPCRouter) SendBalanceUpdate(destinationWallet string) { + senderAddress := common.HexToAddress(destinationWallet) + senderAccountID := NewAccountID(destinationWallet) + balances, err := GetWalletLedger(r.DB, senderAddress).GetBalances(senderAccountID) + if err != nil { + r.lg.Error("error getting balances", "userID", destinationWallet, "error", err) + return + } + + r.Node.Notify(destinationWallet, "bu", BalanceUpdatesResponse{BalanceUpdates: balances}) + r.lg.Info("balance update sent", "userID", destinationWallet, "balances", balances) +} + +// SendChannelUpdate sends a single channel update to the client +func (r *RPCRouter) SendChannelUpdate(channel Channel) { + channelResponse := ChannelResponse{ + ChannelID: channel.ChannelID, + Participant: channel.Participant, + Status: channel.Status, + Token: channel.Token, + RawAmount: channel.RawAmount, + ChainID: channel.ChainID, + Adjudicator: channel.Adjudicator, + Challenge: channel.Challenge, + Nonce: channel.Nonce, + Version: channel.Version, + CreatedAt: channel.CreatedAt.Format(time.RFC3339), + UpdatedAt: channel.UpdatedAt.Format(time.RFC3339), + } + + r.Node.Notify(channel.Wallet, "cu", channelResponse) + r.lg.Info("channel update sent", + "userID", channel.Wallet, + "channelID", channel.ChannelID, + "participant", channel.Participant, + "status", channel.Status, + ) +} + +// TODO: make adequate notifications response/type +// SendTransferNotification sends a transfer notification to the client +func (r *RPCRouter) SendTransferNotification(destinationWallet string, transferredAllocations TransferResponse) { + r.Node.Notify(destinationWallet, "tr", transferredAllocations) + r.lg.Info("transfer notification sent", "userID", destinationWallet, "transferred allocations", transferredAllocations) +} diff --git a/clearnode/rpc_router_private.go b/clearnode/rpc_router_private.go index ed9ca5e27..09f351fcd 100644 --- a/clearnode/rpc_router_private.go +++ b/clearnode/rpc_router_private.go @@ -10,8 +10,7 @@ import ( ) type GetLedgerBalancesParams struct { - Participant string `json:"participant,omitempty"` // Optional participant address to filter balances - AccountID string `json:"account_id,omitempty"` // Optional account ID to filter balances + AccountID string `json:"account_id,omitempty"` // Optional account ID to filter balances } type GetRPCHistoryParams struct { @@ -137,6 +136,18 @@ type Balance struct { Amount decimal.Decimal `json:"amount"` } +type GetLedgerBalancesResponse struct { + LedgerBalances []Balance `json:"ledger_balances"` +} + +type TransferResponse struct { + Transactions []TransactionResponse `json:"transactions"` +} + +type GetRPCHistoryResponse struct { + RPCEntries []RPCEntry `json:"rpc_entries"` +} + func (r *RPCRouter) BalanceUpdateMiddleware(c *RPCContext) { logger := LoggerFromContext(c.Context) userAddress := common.HexToAddress(c.UserID) @@ -150,7 +161,7 @@ func (r *RPCRouter) BalanceUpdateMiddleware(c *RPCContext) { logger.Error("error getting balances", "sender", userAddress.Hex(), "error", err) return } - r.Node.Notify(c.UserID, "bu", balances) + r.Node.Notify(c.UserID, "bu", BalanceUpdatesResponse{BalanceUpdates: balances}) // TODO: notify other participants } @@ -171,8 +182,6 @@ func (r *RPCRouter) HandleGetLedgerBalances(c *RPCContext) { userAccountID := NewAccountID(c.UserID) if params.AccountID != "" { userAccountID = NewAccountID(params.AccountID) - } else if params.Participant != "" { - userAccountID = NewAccountID(params.Participant) } ledger := GetWalletLedger(r.DB, userAddress) @@ -183,7 +192,11 @@ func (r *RPCRouter) HandleGetLedgerBalances(c *RPCContext) { return } - c.Succeed(req.Method, balances) + resp := GetLedgerBalancesResponse{ + LedgerBalances: balances, + } + + c.Succeed(req.Method, resp) logger.Info("ledger balances retrieved", "userID", c.UserID, "accountID", userAccountID) } @@ -271,7 +284,7 @@ func (r *RPCRouter) HandleTransfer(c *RPCContext) { return } - var resp []TransactionResponse + var respTransactions []TransactionResponse err = r.DB.Transaction(func(tx *gorm.DB) error { if wallet := GetWalletBySigner(fromWallet); wallet != "" { fromWallet = wallet @@ -322,7 +335,7 @@ func (r *RPCRouter) HandleTransfer(c *RPCContext) { if err != nil { return fmt.Errorf("failed to format transactions: %w", err) } - resp = formattedTransactions + respTransactions = formattedTransactions return nil }) if err != nil { @@ -332,6 +345,10 @@ func (r *RPCRouter) HandleTransfer(c *RPCContext) { return } + resp := TransferResponse{ + Transactions: respTransactions, + } + r.wsNotifier.Notify( NewBalanceNotification(fromWallet, r.DB), NewTransferNotification(fromWallet, resp), @@ -597,7 +614,7 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { return } - response := make([]RPCEntry, 0, len(rpcHistory)) + respRPCEntries := make([]RPCEntry, 0, len(rpcHistory)) for _, record := range rpcHistory { reqSigs, err := nitrolite.SignaturesFromStrings(record.ReqSig) if err != nil { @@ -613,7 +630,7 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { return } - response = append(response, RPCEntry{ + respRPCEntries = append(respRPCEntries, RPCEntry{ ID: record.ID, Sender: record.Sender, ReqID: record.ReqID, @@ -626,8 +643,12 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { }) } - c.Succeed(req.Method, response) - logger.Info("RPC history retrieved", "userID", c.UserID, "entryCount", len(response)) + resp := GetRPCHistoryResponse{ + RPCEntries: respRPCEntries, + } + + c.Succeed(req.Method, resp) + logger.Info("RPC history retrieved", "userID", c.UserID, "entryCount", len(respRPCEntries)) } func verifyAllocations(appSessionBalance, allocationSum map[string]decimal.Decimal) error { diff --git a/clearnode/rpc_router_private_test.go b/clearnode/rpc_router_private_test.go index a09a2e1ac..d2b94efc9 100644 --- a/clearnode/rpc_router_private_test.go +++ b/clearnode/rpc_router_private_test.go @@ -44,8 +44,11 @@ func assertErrorResponse(t *testing.T, ctx *RPCContext, expectedContains string) res := ctx.Message.Res require.NotNil(t, res) require.Equal(t, "error", res.Method) - require.Len(t, res.Params, 1) - require.Contains(t, res.Params[0], expectedContains) + + errorParams, ok := res.Params.(ErrorResponse) + require.True(t, ok, "Response parameter should be an ErrorResponse") + + require.Contains(t, errorParams.Error, expectedContains) } func TestRPCRouterHandleGetRPCHistory(t *testing.T) { @@ -157,13 +160,12 @@ func TestRPCRouterHandleGetRPCHistory(t *testing.T) { res := assertResponse(t, ctx, "get_rpc_history") require.Equal(t, uint64(idx+100), res.RequestID) - require.Len(t, res.Params, 1, "Response should contain an array") - rpcHistory, ok := res.Params[0].([]RPCEntry) - require.True(t, ok, "Response parameter should be a slice of RPCEntry") - assert.Len(t, rpcHistory, tc.expectedRecordCount, "Should return expected number of records") + rpcHistory, ok := res.Params.(GetRPCHistoryResponse) + require.True(t, ok, "Response parameter should be a GetRPCHistoryResponse") + assert.Len(t, rpcHistory.RPCEntries, tc.expectedRecordCount, "Should return expected number of records") // Check records are in expected order - for i, record := range rpcHistory { + for i, record := range rpcHistory.RPCEntries { if i < len(tc.expectedReqIDs) { assert.Equal(t, tc.expectedReqIDs[i], record.ReqID, "Record %d should have expected ReqID", i) assert.Equal(t, userAddress, record.Sender, "All records should belong to the requesting participant") @@ -192,7 +194,8 @@ func TestRPCRouterHandleGetLedgerBalances(t *testing.T) { router.HandleGetLedgerBalances(ctx) res := assertResponse(t, ctx, "get_ledger_balances") - balancesArray, ok := res.Params[0].([]Balance) + balancesResp, ok := res.Params.(GetLedgerBalancesResponse) + balancesArray := balancesResp.LedgerBalances require.True(t, ok) require.Len(t, balancesArray, 1) require.Equal(t, "usdc", balancesArray[0].Asset) @@ -219,7 +222,7 @@ func TestRPCRouterHandleGetUserTag(t *testing.T) { router.HandleGetUserTag(ctx) assertResponse(t, ctx, "get_user_tag") - getTagResponse, ok := ctx.Message.Res.Params[0].(GetUserTagResponse) + getTagResponse, ok := ctx.Message.Res.Params.(GetUserTagResponse) require.True(t, ok, "Response should be a GetUserTagResponse") require.Equal(t, userTag.Tag, getTagResponse.Tag) }) @@ -236,6 +239,7 @@ func TestRPCRouterHandleGetUserTag(t *testing.T) { assertErrorResponse(t, ctx, "failed to get user tag") }) } + func TestRPCRouterHandleTransfer(t *testing.T) { senderKey, _ := crypto.GenerateKey() senderSigner := Signer{privateKey: senderKey} @@ -269,18 +273,21 @@ func TestRPCRouterHandleTransfer(t *testing.T) { router.HandleTransfer(ctx) res := assertResponse(t, ctx, "transfer") - transferResp, ok := res.Params[0].([]TransactionResponse) + transferResp, ok := res.Params.(TransferResponse) + require.Len(t, transferResp.Transactions, 2, "Response should contain 2 transaction objects") + + transferTransaction := transferResp.Transactions[0] require.True(t, ok, "Response should be a slice of TransactionResponse") - require.Equal(t, senderAddr.Hex(), transferResp[0].FromAccount) - require.Equal(t, recipientAddr.Hex(), transferResp[0].ToAccount) - require.False(t, transferResp[0].CreatedAt.IsZero(), "CreatedAt should be set") + require.Equal(t, senderAddr.Hex(), transferTransaction.FromAccount) + require.Equal(t, recipientAddr.Hex(), transferTransaction.ToAccount) + require.False(t, transferTransaction.CreatedAt.IsZero(), "CreatedAt should be set") // Verify user tags are empty (since no tags were created for these wallets) - require.Empty(t, transferResp[0].FromAccountTag, "FromAccountTag should be empty when no tag exists") - require.Empty(t, transferResp[0].ToAccountTag, "ToAccountTag should be empty when no tag exists") + require.Empty(t, transferTransaction.FromAccountTag, "FromAccountTag should be empty when no tag exists") + require.Empty(t, transferTransaction.ToAccountTag, "ToAccountTag should be empty when no tag exists") // Verify that all transactions in response have the tag fields - for _, tx := range transferResp { + for _, tx := range transferResp.Transactions { require.Equal(t, senderAddr.Hex(), tx.FromAccount) require.Equal(t, recipientAddr.Hex(), tx.ToAccount) require.Empty(t, tx.FromAccountTag, "FromAccountTag should be empty when no tag exists") @@ -331,8 +338,7 @@ func TestRPCRouterHandleTransfer(t *testing.T) { } // Verify response transactions match database transactions - require.Len(t, transferResp, 2, "Response should contain 2 transaction objects") - for _, responseTx := range transferResp { + for _, responseTx := range transferResp.Transactions { // Find matching transaction in database var dbTx LedgerTransaction err = db.Where("id = ?", responseTx.Id).First(&dbTx).Error @@ -374,12 +380,12 @@ func TestRPCRouterHandleTransfer(t *testing.T) { router.HandleTransfer(ctx) res := assertResponse(t, ctx, "transfer") - transactionResponse, ok := res.Params[0].([]TransactionResponse) + transactionResponse, ok := res.Params.(TransferResponse) require.True(t, ok, "Response should be a TransactionResponse") + require.Len(t, transactionResponse.Transactions, 2, "Should have 2 transaction entries for the transfer") - targetTransaction := transactionResponse[0] + targetTransaction := transactionResponse.Transactions[0] - require.Len(t, transactionResponse, 2, "Should have 2 transaction entries for the transfer") require.Equal(t, senderAddr.Hex(), targetTransaction.FromAccount) require.Equal(t, recipientAddr.Hex(), targetTransaction.ToAccount) require.False(t, targetTransaction.CreatedAt.IsZero(), "CreatedAt should be set") @@ -389,7 +395,7 @@ func TestRPCRouterHandleTransfer(t *testing.T) { require.Equal(t, recipientTag.Tag, targetTransaction.ToAccountTag, "ToAccountTag should match recipient's tag") // Verify all transactions have correct tag information - for _, tx := range transactionResponse { + for _, tx := range transactionResponse.Transactions { require.Equal(t, senderAddr.Hex(), tx.FromAccount) require.Equal(t, recipientAddr.Hex(), tx.ToAccount) require.Empty(t, tx.FromAccountTag, "FromAccountTag should be empty since sender has no tag") @@ -647,7 +653,7 @@ func TestRPCRouterHandleCreateAppSession(t *testing.T) { router.HandleCreateApplication(ctx) res := assertResponse(t, ctx, "create_app_session") - appResp, ok := res.Params[0].(AppSessionResponse) + appResp, ok := res.Params.(AppSessionResponse) require.True(t, ok) require.Equal(t, string(ChannelStatusOpen), appResp.Status) require.Equal(t, uint64(1), appResp.Version) @@ -784,7 +790,7 @@ func TestRPCRouterHandleSubmitAppState(t *testing.T) { router.HandleSubmitAppState(ctx) res := assertResponse(t, ctx, "submit_app_state") - appResp, ok := res.Params[0].(AppSessionResponse) + appResp, ok := res.Params.(AppSessionResponse) require.True(t, ok) require.Equal(t, string(ChannelStatusOpen), appResp.Status) require.Equal(t, uint64(2), appResp.Version) @@ -871,7 +877,7 @@ func TestRPCRouterHandleCloseAppSession(t *testing.T) { router.HandleCloseApplication(ctx) res := assertResponse(t, ctx, "close_app_session") - appResp, ok := res.Params[0].(AppSessionResponse) + appResp, ok := res.Params.(AppSessionResponse) require.True(t, ok) require.Equal(t, string(ChannelStatusClosed), appResp.Status) require.Equal(t, uint64(3), appResp.Version) @@ -950,8 +956,8 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) - require.True(t, ok, "Response should be ResizeChannelResponse") + resObj, ok := res.Params.(ChannelOperationResponse) + require.True(t, ok, "Response should be ChannelOperationResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) @@ -1010,7 +1016,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Channel amount should decrease @@ -1248,7 +1254,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Should be initial amount (1000) + allocate amount (0) + resize amount (100) = 1100 @@ -1295,7 +1301,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Should be initial amount (1000) + allocate amount (0) - resize amount (100) = 900 @@ -1414,7 +1420,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Verify the large allocation was processed correctly @@ -1469,7 +1475,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be ResizeChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) @@ -1539,7 +1545,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be ResizeChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) @@ -1609,7 +1615,7 @@ func TestRPCRouterHandleCloseChannel(t *testing.T) { router.HandleCloseChannel(ctx) res := assertResponse(t, ctx, "close_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be CloseChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) diff --git a/clearnode/rpc_router_public.go b/clearnode/rpc_router_public.go index f78a32d07..166ef22df 100644 --- a/clearnode/rpc_router_public.go +++ b/clearnode/rpc_router_public.go @@ -11,13 +11,17 @@ type GetAssetsParams struct { ChainID *uint32 `json:"chain_id,omitempty"` // Optional chain ID to filter assets } -type GetAssetsResponse struct { +type AssetResponse struct { Token string `json:"token"` ChainID uint32 `json:"chain_id"` Symbol string `json:"symbol"` Decimals uint8 `json:"decimals"` } +type GetAssetsResponse struct { + Assets []AssetResponse `json:"assets"` +} + type GetChannelsParams struct { ListOptions Participant string `json:"participant,omitempty"` // Optional participant wallet to filter channels @@ -90,8 +94,28 @@ type TransactionResponse struct { CreatedAt time.Time `json:"created_at"` } +type GetChannelsResponse struct { + Channels []ChannelResponse `json:"channels"` +} + +type GetAppSessionsResponse struct { + AppSessions []AppSessionResponse `json:"app_sessions"` +} + +type GetLedgerEntriesResponse struct { + LedgerEntries []LedgerEntryResponse `json:"ledger_entries"` +} + +type GetLedgerTransactionsResponse struct { + LedgerTransactions []TransactionResponse `json:"ledger_transactions"` +} + +type ErrorResponse struct { + Error string `json:"error"` // The error message to send back to the client +} + func (r *RPCRouter) HandlePing(c *RPCContext) { - c.Succeed("pong") + c.Succeed("pong", nil) } // HandleGetConfig returns the broker configuration @@ -134,9 +158,13 @@ func (r *RPCRouter) HandleGetAssets(c *RPCContext) { return } - resp := make([]GetAssetsResponse, 0, len(assets)) + respAssets := make([]AssetResponse, 0, len(assets)) for _, asset := range assets { - resp = append(resp, GetAssetsResponse(asset)) + respAssets = append(respAssets, AssetResponse(asset)) + } + + resp := GetAssetsResponse{ + Assets: respAssets, } c.Succeed(req.Method, resp) @@ -166,9 +194,9 @@ func (r *RPCRouter) HandleGetChannels(c *RPCContext) { return } - response := make([]ChannelResponse, 0, len(channels)) + respChannels := make([]ChannelResponse, 0, len(channels)) for _, channel := range channels { - response = append(response, ChannelResponse{ + respChannels = append(respChannels, ChannelResponse{ ChannelID: channel.ChannelID, Participant: channel.Participant, Status: channel.Status, @@ -185,7 +213,11 @@ func (r *RPCRouter) HandleGetChannels(c *RPCContext) { }) } - c.Succeed(req.Method, response) + resp := GetChannelsResponse{ + Channels: respChannels, + } + + c.Succeed(req.Method, resp) logger.Info("channels retrieved", "participant", params.Participant, "status", params.Status) } @@ -243,9 +275,9 @@ func (r *RPCRouter) HandleGetAppSessions(c *RPCContext) { } // TODO: update response format accordingly to create struct - resp := make([]AppSessionResponse, len(sessions)) + respAppSessions := make([]AppSessionResponse, len(sessions)) for i, session := range sessions { - resp[i] = AppSessionResponse{ + respAppSessions[i] = AppSessionResponse{ AppSessionID: session.SessionID, Status: string(session.Status), ParticipantWallets: session.ParticipantWallets, @@ -261,6 +293,10 @@ func (r *RPCRouter) HandleGetAppSessions(c *RPCContext) { } } + resp := GetAppSessionsResponse{ + AppSessions: respAppSessions, + } + c.Succeed(req.Method, resp) logger.Info("application sessions retrieved", "participant", params.Participant, "status", params.Status) } @@ -293,9 +329,9 @@ func (r *RPCRouter) HandleGetLedgerEntries(c *RPCContext) { return } - resp := make([]LedgerEntryResponse, len(entries)) + respLedgerEntries := make([]LedgerEntryResponse, len(entries)) for i, entry := range entries { - resp[i] = LedgerEntryResponse{ + respLedgerEntries[i] = LedgerEntryResponse{ ID: entry.ID, AccountID: entry.AccountID, AccountType: entry.AccountType, @@ -307,6 +343,10 @@ func (r *RPCRouter) HandleGetLedgerEntries(c *RPCContext) { } } + resp := GetLedgerEntriesResponse{ + LedgerEntries: respLedgerEntries, + } + c.Succeed(req.Method, resp) logger.Info("ledger entries retrieved", "accountID", userAccountID, "asset", params.Asset, "wallet", userAddress) } @@ -341,13 +381,17 @@ func (r *RPCRouter) HandleGetLedgerTransactions(c *RPCContext) { return } - resp, err := FormatTransactions(r.DB, transactions) + respLedgerTransactions, err := FormatTransactions(r.DB, transactions) if err != nil { logger.Error("failed to format transactions", "error", err) c.Fail(err, "failed to return transactions") return } + resp := GetLedgerTransactionsResponse{ + LedgerTransactions: respLedgerTransactions, + } + c.Succeed(req.Method, resp) logger.Info("transactions retrieved", "count", len(transactions), "accountID", params.AccountID, "asset", params.Asset, "txType", params.TxType) } diff --git a/clearnode/rpc_router_public_test.go b/clearnode/rpc_router_public_test.go index fa962d36c..0b3860694 100644 --- a/clearnode/rpc_router_public_test.go +++ b/clearnode/rpc_router_public_test.go @@ -12,11 +12,9 @@ import ( "github.com/stretchr/testify/require" ) -func createRPCContext(id int, method string, params any) *RPCContext { - var rpcParams []any - if params != nil { - paramsJSON, _ := json.Marshal(params) - rpcParams = []any{json.RawMessage(paramsJSON)} +func createRPCContext(id int, method string, params RPCDataParams) *RPCContext { + if params == nil { + params = struct{}{} } return &RPCContext{ @@ -25,7 +23,7 @@ func createRPCContext(id int, method string, params any) *RPCContext { Req: &RPCData{ RequestID: uint64(id), Method: method, - Params: rpcParams, + Params: params, Timestamp: uint64(time.Now().Unix()), }, Sig: []Signature{Signature([]byte("dummy-signature"))}, @@ -63,7 +61,7 @@ func TestRPCRouterHandleGetConfig(t *testing.T) { router.HandleGetConfig(ctx) res := assertResponse(t, ctx, "get_config") - configMap, ok := res.Params[0].(BrokerConfig) + configMap, ok := res.Params.(BrokerConfig) require.True(t, ok, "Response should contain a BrokerConfig") assert.Equal(t, router.Signer.GetAddress().Hex(), configMap.BrokerAddress) require.Len(t, configMap.Networks, 3, "Should have 3 supported networks") @@ -116,12 +114,11 @@ func TestRPCRouterHandleGetAssets(t *testing.T) { router.HandleGetAssets(ctx) res := assertResponse(t, ctx, "get_assets") - require.Len(t, res.Params, 1, "Response should contain an array of AssetResponse") - responseAssets, ok := res.Params[0].([]GetAssetsResponse) - require.True(t, ok, "Response parameter should be a slice of AssetResponse") - assert.Len(t, responseAssets, len(tc.expectedTokenNames), "Should return expected number of assets") + responseAssets, ok := res.Params.(GetAssetsResponse) + require.True(t, ok, "Response parameter should be a GetAssetsResponse") + assert.Len(t, responseAssets.Assets, len(tc.expectedTokenNames), "Should return expected number of assets") - for idx, asset := range responseAssets { + for idx, asset := range responseAssets.Assets { assert.True(t, asset.Token == tc.expectedTokenNames[idx], "Should include token %s", tc.expectedTokenNames[idx]) } }) @@ -236,12 +233,11 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { router.HandleGetChannels(ctx) res := assertResponse(t, ctx, "get_channels") - require.Len(t, res.Params, 1, "Response should contain a slice of ChannelResponse") - responseChannels, ok := res.Params[0].([]ChannelResponse) - require.True(t, ok, "Response parameter should be a slice of ChannelResponse") - assert.Len(t, responseChannels, len(tc.expectedChannelIDs), "Should return expected number of channels") + responseChannels, ok := res.Params.(GetChannelsResponse) + require.True(t, ok, "Response parameter should be a GetChannelsResponse") + assert.Len(t, responseChannels.Channels, len(tc.expectedChannelIDs), "Should return expected number of channels") - for idx, channel := range responseChannels { + for idx, channel := range responseChannels.Channels { assert.True(t, channel.ChannelID == tc.expectedChannelIDs[idx], "%d-th result (%s) should equal %s", idx, channel.ChannelID, tc.expectedChannelIDs[idx]) } }) @@ -319,13 +315,12 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { router.HandleGetChannels(ctx) res := assertResponse(t, ctx, "get_channels") - require.Len(t, res.Params, 1, "Response should contain an array of ChannelResponse") - responseChannels, ok := res.Params[0].([]ChannelResponse) - require.True(t, ok, "Response parameter should be a slice of ChannelResponse") - assert.Len(t, responseChannels, len(tc.expectedChannelIDs), "Should return expected number of channels") + responseChannels, ok := res.Params.(GetChannelsResponse) + require.True(t, ok, "Response parameter should be a GetChannelsResponse") + assert.Len(t, responseChannels.Channels, len(tc.expectedChannelIDs), "Should return expected number of channels") // Check channel IDs are included in expected order - for idx, channel := range responseChannels { + for idx, channel := range responseChannels.Channels { assert.Equal(t, tc.expectedChannelIDs[idx], channel.ChannelID, "Should include channel %s at position %d", tc.expectedChannelIDs[idx], idx) } }) @@ -356,7 +351,7 @@ func TestRPCRouterHandleGetAppDefinition(t *testing.T) { router.HandleGetAppDefinition(ctx) res := assertResponse(t, ctx, "get_app_definition") - def, ok := res.Params[0].(AppDefinition) + def, ok := res.Params.(AppDefinition) require.True(t, ok) assert.Equal(t, session.Protocol, def.Protocol) assert.EqualValues(t, session.ParticipantWallets, def.ParticipantWallets) @@ -503,13 +498,12 @@ func TestRPCRouterHandleGetAppSessions(t *testing.T) { res := assertResponse(t, ctx, "get_app_sessions") assert.Equal(t, uint64(idx), res.RequestID) - require.Len(t, res.Params, 1, "Response should contain an array of AppSessionResponse") - sessionResponses, ok := res.Params[0].([]AppSessionResponse) - require.True(t, ok, "Response parameter should be a slice of AppSessionResponse") - assert.Len(t, sessionResponses, len(tc.expectedSessionIDs), "Should return expected number of app sessions") + sessionResponses, ok := res.Params.(GetAppSessionsResponse) + require.True(t, ok, "Response parameter should be a GetAppSessionsResponse") + assert.Len(t, sessionResponses.AppSessions, len(tc.expectedSessionIDs), "Should return expected number of app sessions") - for idx, sessionResponse := range sessionResponses { + for idx, sessionResponse := range sessionResponses.AppSessions { assert.True(t, sessionResponse.AppSessionID == tc.expectedSessionIDs[idx], "Should include session %s", tc.expectedSessionIDs[idx]) } }) @@ -591,13 +585,12 @@ func TestRPCRouterHandleGetAppSessions(t *testing.T) { router.HandleGetAppSessions(ctx) res := assertResponse(t, ctx, "get_app_sessions") - require.Len(t, res.Params, 1, "Response should contain an array of AppSessionResponse") - responseSessions, ok := res.Params[0].([]AppSessionResponse) - require.True(t, ok, "Response parameter should be a slice of AppSessionResponse") - assert.Len(t, responseSessions, len(tc.expectedSessionIDs), "Should return expected number of sessions") + responseSessions, ok := res.Params.(GetAppSessionsResponse) + require.True(t, ok, "Response parameter should be a GetAppSessionsResponse") + assert.Len(t, responseSessions.AppSessions, len(tc.expectedSessionIDs), "Should return expected number of sessions") // Check session IDs are in expected order - for idx, session := range responseSessions { + for idx, session := range responseSessions.AppSessions { assert.True(t, session.AppSessionID == tc.expectedSessionIDs[idx], "Retrieved %d-th session ID should be equal %s", idx, tc.expectedSessionIDs[idx]) } }) @@ -741,13 +734,12 @@ func TestRPCRouterHandleGetLedgerEntries(t *testing.T) { res := assertResponse(t, ctx, "get_ledger_entries") assert.Equal(t, uint64(idx+1), res.RequestID) - require.Len(t, res.Params, 1, "Response should contain an array of Entry objects") - entries, ok := res.Params[0].([]LedgerEntryResponse) - require.True(t, ok, "Response parameter should be a slice of Entry") - assert.Len(t, entries, tc.expectedCount, "Should return expected number of entries") + entries, ok := res.Params.(GetLedgerEntriesResponse) + require.True(t, ok, "Response parameter should be a GetLedgerEntriesResponse") + assert.Len(t, entries.LedgerEntries, tc.expectedCount, "Should return expected number of entries") - tc.validateFunc(t, entries) + tc.validateFunc(t, entries.LedgerEntries) }) } }) @@ -825,13 +817,12 @@ func TestRPCRouterHandleGetLedgerEntries(t *testing.T) { router.HandleGetLedgerEntries(c) res := assertResponse(t, c, "get_ledger_entries") - require.Len(t, res.Params, 1, "Response should contain an array of LedgerEntryResponse") - responseEntries, ok := res.Params[0].([]LedgerEntryResponse) - require.True(t, ok, "Response parameter should be a slice of LedgerEntryResponse") - assert.Len(t, responseEntries, len(tc.expectedToken), "Should return expected number of entries") + responseEntries, ok := res.Params.(GetLedgerEntriesResponse) + require.True(t, ok, "Response parameter should be a GetLedgerEntriesResponse") + assert.Len(t, responseEntries.LedgerEntries, len(tc.expectedToken), "Should return expected number of entries") // Check token names are included in expected order - for idx, entry := range responseEntries { + for idx, entry := range responseEntries.LedgerEntries { assert.Equal(t, tc.expectedToken[idx], entry.Asset, "Should include token %s at position %d", tc.expectedToken[idx], idx) } }) @@ -992,16 +983,17 @@ func TestRPCRouterHandleGetTransactions(t *testing.T) { router.HandleGetLedgerTransactions(c) res := assertResponse(t, c, "get_ledger_transactions") - require.Len(t, res.Params, 1, "Response should contain one parameter") // Unmarshal the actual transaction data - var transactions []TransactionResponse + var resp GetLedgerTransactionsResponse + require.NotNil(t, res.Params, "Response parameter should not be nil") // We need to marshal the interface{} back to JSON, then unmarshal into our concrete type. - respBytes, err := json.Marshal(res.Params[0]) + respBytes, err := json.Marshal(res.Params) require.NoError(t, err) - err = json.Unmarshal(respBytes, &transactions) - require.NoError(t, err, "Response parameter should be a slice of TransactionResponse") + err = json.Unmarshal(respBytes, &resp) + require.NoError(t, err, "Response parameter should be a GetLedgerTransactionsResponse") + transactions := resp.LedgerTransactions // Assert the expected number of transactions were returned assert.Len(t, transactions, tc.expectedLen) @@ -1114,14 +1106,14 @@ func TestRPCRouterHandleGetTransactions(t *testing.T) { router.HandleGetLedgerTransactions(ctx) res := assertResponse(t, ctx, "get_ledger_transactions") - require.Len(t, res.Params, 1, "Response should contain an array of TransactionResponse") - var transactions []TransactionResponse - respBytes, err := json.Marshal(res.Params[0]) + var resp GetLedgerTransactionsResponse + respBytes, err := json.Marshal(res.Params) require.NoError(t, err) - err = json.Unmarshal(respBytes, &transactions) + err = json.Unmarshal(respBytes, &resp) require.NoError(t, err) + transactions := resp.LedgerTransactions assert.Len(t, transactions, tc.expectedCount, "Should return expected number of transactions") // For non-filter tests, verify order diff --git a/integration/common/auth.ts b/integration/common/auth.ts index 8b71a5900..fb3e1bbca 100644 --- a/integration/common/auth.ts +++ b/integration/common/auth.ts @@ -3,7 +3,7 @@ import { createAuthRequestMessage, createAuthVerifyMessage, AuthRequestParams, - rpcResponseParser, + parseAuthChallengeResponse, } from '@erc7824/nitrolite'; import { Identity } from './identity'; import { getAuthChallengePredicate, getAuthVerifyPredicate, TestWebSocket } from './ws'; @@ -14,8 +14,8 @@ export const createAuthSessionWithClearnode = async ( authRequestParams?: AuthRequestParams ) => { authRequestParams = authRequestParams || { - wallet: identity.walletAddress, - participant: identity.sessionAddress, + address: identity.walletAddress, + session_key: identity.sessionAddress, app_name: 'Test Domain', expire: String(Math.floor(Date.now() / 1000) + 3600), // 1 hour expiration scope: 'console', @@ -28,7 +28,7 @@ export const createAuthSessionWithClearnode = async ( { scope: authRequestParams.scope, application: authRequestParams.application, - participant: authRequestParams.participant, + participant: authRequestParams.session_key, expire: authRequestParams.expire, allowances: authRequestParams.allowances, }, @@ -40,7 +40,7 @@ export const createAuthSessionWithClearnode = async ( const authRequestMsg = await createAuthRequestMessage(authRequestParams); const authRequestResponse = await ws.sendAndWaitForResponse(authRequestMsg, getAuthChallengePredicate(), 1000); - const authRequestParsedResponse = rpcResponseParser.authChallenge(authRequestResponse); + const authRequestParsedResponse = parseAuthChallengeResponse(authRequestResponse); const authVerifyMsg = await createAuthVerifyMessage(eip712MessageSigner, authRequestParsedResponse); await ws.sendAndWaitForResponse(authVerifyMsg, getAuthVerifyPredicate(), 1000); diff --git a/integration/common/nitroliteClient.ts b/integration/common/nitroliteClient.ts index d7e316377..b54d16f51 100644 --- a/integration/common/nitroliteClient.ts +++ b/integration/common/nitroliteClient.ts @@ -5,8 +5,9 @@ import { createCloseChannelMessage, createCreateChannelMessage, NitroliteClient, + parseChannelUpdateResponse, + parseCloseChannelResponse, RPCChannelStatus, - rpcResponseParser, } from '@erc7824/nitrolite'; import { Identity } from './identity'; import { Address, createPublicClient, Hex, http } from 'viem'; @@ -74,7 +75,7 @@ export class TestNitroliteClient extends NitroliteClient { const openResponse = await openChannelPromise; - const openParsedResponse = rpcResponseParser.channelUpdate(openResponse); + const openParsedResponse = parseChannelUpdateResponse(openResponse); const responseChannel = openParsedResponse.params; return { params: responseChannel, initialState }; @@ -88,7 +89,7 @@ export class TestNitroliteClient extends NitroliteClient { ); const closeResponse = await ws.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); - const closeParsedResponse = rpcResponseParser.closeChannel(closeResponse); + const closeParsedResponse = parseCloseChannelResponse(closeResponse); const closeChannelUpdateChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Closed), @@ -120,7 +121,7 @@ export class TestNitroliteClient extends NitroliteClient { }); const closeChannelUpdateResponse = await closeChannelUpdateChannelPromise; - const closeChannelUpdateParsedResponse = rpcResponseParser.channelUpdate(closeChannelUpdateResponse); + const closeChannelUpdateParsedResponse = parseChannelUpdateResponse(closeChannelUpdateResponse); const responseChannel = closeChannelUpdateParsedResponse.params; return { params: responseChannel }; diff --git a/integration/tests/challenge_channel.test.ts b/integration/tests/challenge_channel.test.ts new file mode 100644 index 000000000..bdc98085b --- /dev/null +++ b/integration/tests/challenge_channel.test.ts @@ -0,0 +1,155 @@ +import { createAuthSessionWithClearnode } from '@/auth'; +import { BlockchainUtils } from '@/blockchainUtils'; +import { DatabaseUtils } from '@/databaseUtils'; +import { Identity } from '@/identity'; +import { TestNitroliteClient } from '@/nitroliteClient'; +import { CONFIG } from '@/setup'; +import { getChannelUpdatePredicateWithStatus, TestWebSocket, getGetLedgerEntriesPredicate } from '@/ws'; +import { createGetLedgerEntriesMessage, parseGetLedgerEntriesResponse, RPCChannelStatus } from '@erc7824/nitrolite'; +import { parseUnits, GetTxpoolContentReturnType, Hash } from 'viem'; + +// TODO: this test could stuck anvil if is not gracefully closed +describe.skip('Close channel', () => { + const depositAmount = parseUnits('100', 6); // 100 USDC (decimals = 6) + const decimalDepositAmount = 100; + + let ws: TestWebSocket; + let identity: Identity; + let client: TestNitroliteClient; + let blockUtils: BlockchainUtils; + let databaseUtils: DatabaseUtils; + + beforeAll(async () => { + blockUtils = new BlockchainUtils(); + databaseUtils = new DatabaseUtils(); + identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); + ws = new TestWebSocket(CONFIG.CLEARNODE_URL, CONFIG.DEBUG_MODE); + + await blockUtils.resumeMining(); + }); + + beforeEach(async () => { + await ws.connect(); + await createAuthSessionWithClearnode(ws, identity); + await blockUtils.makeSnapshot(); + }); + + afterEach(async () => { + ws.close(); + await databaseUtils.cleanupDatabaseData(); + await blockUtils.resetSnapshot(); + }); + + afterAll(async () => { + databaseUtils.close(); + + await blockUtils.resumeMining(); + }); + + it('should create nitrolite client to challenge channels', async () => { + client = new TestNitroliteClient(identity); + + expect(client).toBeDefined(); + expect(client).toHaveProperty('depositAndCreateChannel'); + expect(client).toHaveProperty('challengeChannel'); + }); + + it('should challenge channel in joining state', async () => { + const joiningChannelPromise = ws.waitForMessage( + getChannelUpdatePredicateWithStatus(RPCChannelStatus.Joining), + undefined, + 5000 + ); + + const hash = await client.approveTokens(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, depositAmount); + await blockUtils.waitForTransaction(hash); + + await blockUtils.pauseMining(); + + const { channelId, txHash: createTxHash } = await client.depositAndCreateChannel( + CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, + depositAmount, + { + initialAllocationAmounts: [depositAmount, BigInt(0)], + stateData: '0x', + } + ); + + // Mine exactly one block to ensure the transaction is processed and join is not mined + const depositTxPromise = blockUtils.waitForTransaction(createTxHash); + await blockUtils.mineBlock(); + await depositTxPromise; + + const { lastValidState } = await client.getChannelData(channelId); + const poolWithJoin: GetTxpoolContentReturnType = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + clearInterval(interval); + reject(new Error('Timed out waiting for pending transaction in txpool')); + }, 5000); + + const interval = setInterval(async () => { + const pool = await blockUtils.readTxPool(); + if (Object.keys(pool.pending).length > 0) { + clearInterval(interval); + clearTimeout(timeout); + resolve(pool); + } + }, 200); + }); + + // TODO: this approach is very brittle, and could fail if there are multiple pending transactions + // which usually doesn't happen in tests, but still + const txKey = Object.keys(poolWithJoin.pending)[0]; + const txIndex = Object.keys(poolWithJoin.pending[txKey])[0]; + const joinTx = poolWithJoin.pending[txKey][txIndex]; + + await blockUtils.dropTxFromPool(joinTx.hash as Hash); + + const challengeTxHash = await client.challengeChannel({ + channelId, + candidateState: lastValidState, + }); + + const challengeTxPromise = blockUtils.waitForTransaction(challengeTxHash); + await blockUtils.mineBlock(); + await challengeTxPromise; + + const joinTxHash = await blockUtils.sendRawTransactionAs( + CONFIG.IDENTITIES[0].WALLET_PK, + { + chainId: Number(BigInt(joinTx.chainId)), + nonce: Number(BigInt(joinTx.nonce)), + gasPrice: BigInt(joinTx.gasPrice), + gas: BigInt(joinTx.gas), + to: joinTx.to, + value: BigInt(joinTx.value), + data: joinTx.input, + }, + { + v: BigInt(joinTx.v), + r: joinTx.r, + s: joinTx.s, + } + ); + + const joinTxPromise = blockUtils.waitForTransaction(joinTxHash); + await blockUtils.mineBlock(); + await joinTxPromise; + + const channelData = await client.getChannelData(channelId); + expect(channelData).toBeDefined(); + + const joiningResponse = await joiningChannelPromise; + expect(joiningResponse).toBeDefined(); + + const msg = await createGetLedgerEntriesMessage(identity.messageSigner, channelId); + const response = await ws.sendAndWaitForResponse(msg, getGetLedgerEntriesPredicate(), 5000); + + const { params: parsedResponseParams } = parseGetLedgerEntriesResponse(response); + expect(parsedResponseParams).toBeDefined(); + + expect(parsedResponseParams).toHaveLength(2); + expect(+parsedResponseParams[0].debit + +parsedResponseParams[1].debit).toEqual(decimalDepositAmount); + expect(+parsedResponseParams[0].credit + +parsedResponseParams[1].credit).toEqual(decimalDepositAmount); + }); +}); diff --git a/integration/tests/clearnode_auth.test.ts b/integration/tests/clearnode_auth.test.ts index 8fbc338c9..df0baf8e0 100644 --- a/integration/tests/clearnode_auth.test.ts +++ b/integration/tests/clearnode_auth.test.ts @@ -9,7 +9,8 @@ import { createAuthVerifyMessage, createAuthVerifyMessageWithJWT, createEIP712AuthMessageSigner, - rpcResponseParser, + parseAuthChallengeResponse, + parseAuthVerifyResponse, } from '@erc7824/nitrolite'; describe('Clearnode Authentication', () => { @@ -25,8 +26,8 @@ describe('Clearnode Authentication', () => { const identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); const authRequestParams: AuthRequestParams = { - wallet: identity.walletAddress, - participant: identity.sessionAddress, + address: identity.walletAddress, + session_key: identity.sessionAddress, app_name: 'Test Domain', expire: String(Math.floor(Date.now() / 1000) + 3600), // 1 hour expiration scope: 'console', @@ -39,7 +40,7 @@ describe('Clearnode Authentication', () => { { scope: authRequestParams.scope, application: authRequestParams.application, - participant: authRequestParams.participant, + participant: authRequestParams.session_key, expire: authRequestParams.expire, allowances: authRequestParams.allowances, }, @@ -59,7 +60,7 @@ describe('Clearnode Authentication', () => { const response = await ws.sendAndWaitForResponse(msg, getAuthChallengePredicate(), 1000); expect(response).toBeDefined(); - parsedChallengeResponse = rpcResponseParser.authChallenge(response); + parsedChallengeResponse = parseAuthChallengeResponse(response); expect(parsedChallengeResponse.params.challengeMessage).toBeDefined(); }); @@ -81,11 +82,11 @@ describe('Clearnode Authentication', () => { const response = await ws.sendAndWaitForResponse(msg, getAuthVerifyPredicate(), 1000); expect(response).toBeDefined(); - const parsedAuthVerifyResponse = rpcResponseParser.authVerify(response); + const parsedAuthVerifyResponse = parseAuthVerifyResponse(response); expect(parsedAuthVerifyResponse.params.success).toBe(true); - expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.participant); - expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.wallet); + expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.session_key); + expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.address); expect(parsedAuthVerifyResponse.params.jwtToken).toBeDefined(); jwtToken = parsedAuthVerifyResponse.params.jwtToken; @@ -101,11 +102,11 @@ describe('Clearnode Authentication', () => { const response = await ws.sendAndWaitForResponse(msg, getAuthVerifyPredicate(), 1000); expect(response).toBeDefined(); - const parsedAuthVerifyResponse = rpcResponseParser.authVerify(response); + const parsedAuthVerifyResponse = parseAuthVerifyResponse(response); expect(parsedAuthVerifyResponse.params.success).toBe(true); - expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.participant); - expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.wallet); + expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.session_key); + expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.address); expect(parsedAuthVerifyResponse.params.jwtToken).toBeUndefined(); }); }); diff --git a/integration/tests/close_channel.test.ts b/integration/tests/close_channel.test.ts index 40e8934b8..d168f57c6 100644 --- a/integration/tests/close_channel.test.ts +++ b/integration/tests/close_channel.test.ts @@ -5,7 +5,7 @@ import { Identity } from '@/identity'; import { TestNitroliteClient } from '@/nitroliteClient'; import { CONFIG } from '@/setup'; import { getCloseChannelPredicate, TestWebSocket } from '@/ws'; -import { createCloseChannelMessage, rpcResponseParser } from '@erc7824/nitrolite'; +import { createCloseChannelMessage, parseCloseChannelResponse } from '@erc7824/nitrolite'; import { Hex, parseUnits } from 'viem'; describe('Close channel', () => { @@ -71,7 +71,7 @@ describe('Close channel', () => { const closeResponse = await ws.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); expect(closeResponse).toBeDefined(); - const closeParsedResponse = rpcResponseParser.closeChannel(closeResponse); + const closeParsedResponse = parseCloseChannelResponse(closeResponse); const closeChannelTxHash = await client.closeChannel({ finalState: { diff --git a/integration/tests/create_channel.test.ts b/integration/tests/create_channel.test.ts index 01365fb40..1294e8dc4 100644 --- a/integration/tests/create_channel.test.ts +++ b/integration/tests/create_channel.test.ts @@ -9,8 +9,9 @@ import { convertRPCToClientChannel, convertRPCToClientState, createCreateChannelMessage, + parseChannelUpdateResponse, + parseCreateChannelResponse, RPCChannelStatus, - rpcResponseParser, } from '@erc7824/nitrolite'; import { parseUnits } from 'viem'; @@ -71,7 +72,7 @@ describe('Create channel', () => { const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); expect(createResponse).toBeDefined(); - const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const { params: createParsedResponseParams } = parseCreateChannelResponse(createResponse); const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), @@ -102,7 +103,7 @@ describe('Create channel', () => { const openResponse = await openChannelPromise; expect(openResponse).toBeDefined(); - const openParsedResponse = rpcResponseParser.channelUpdate(openResponse); + const openParsedResponse = parseChannelUpdateResponse(openResponse); const responseChannel = openParsedResponse.params; expect(responseChannel.adjudicator).toBe(CONFIG.ADDRESSES.DUMMY_ADJUDICATOR_ADDRESS); @@ -155,7 +156,7 @@ describe('Create channel', () => { const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); expect(createResponse).toBeDefined(); - const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const { params: createParsedResponseParams } = parseCreateChannelResponse(createResponse); const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), @@ -186,7 +187,7 @@ describe('Create channel', () => { const openResponse = await openChannelPromise; expect(openResponse).toBeDefined(); - const openParsedResponse = rpcResponseParser.channelUpdate(openResponse); + const openParsedResponse = parseChannelUpdateResponse(openResponse); const responseChannel = openParsedResponse.params; expect(responseChannel.adjudicator).toBe(CONFIG.ADDRESSES.DUMMY_ADJUDICATOR_ADDRESS); diff --git a/integration/tests/get_user_tag.test.ts b/integration/tests/get_user_tag.test.ts index 661afeb90..716915663 100644 --- a/integration/tests/get_user_tag.test.ts +++ b/integration/tests/get_user_tag.test.ts @@ -3,7 +3,7 @@ import { DatabaseUtils } from '@/databaseUtils'; import { Identity } from '@/identity'; import { CONFIG } from '@/setup'; import { getGetUserTagPredicate, TestWebSocket } from "@/ws"; -import { createGetUserTagMessage, rpcResponseParser } from "@erc7824/nitrolite"; +import { createGetUserTagMessage, parseGetUserTagResponse } from "@erc7824/nitrolite"; describe('Get User Tag Integration', () => { let ws: TestWebSocket; @@ -43,7 +43,7 @@ describe('Get User Tag Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getUserTag(response); + const parsedResponse = parseGetUserTagResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); expect(parsedResponse.params.tag).toBeDefined(); @@ -55,12 +55,12 @@ describe('Get User Tag Integration', () => { // First request const msg1 = await createGetUserTagMessage(identity.messageSigner); const response1 = await ws.sendAndWaitForResponse(msg1, getGetUserTagPredicate(), 5000); - const parsedResponse1 = rpcResponseParser.getUserTag(response1); + const parsedResponse1 = parseGetUserTagResponse(response1); // Second request const msg2 = await createGetUserTagMessage(identity.messageSigner); const response2 = await ws.sendAndWaitForResponse(msg2, getGetUserTagPredicate(), 5000); - const parsedResponse2 = rpcResponseParser.getUserTag(response2); + const parsedResponse2 = parseGetUserTagResponse(response2); expect(parsedResponse1.params.tag).toBe(parsedResponse2.params.tag); }); @@ -68,7 +68,7 @@ describe('Get User Tag Integration', () => { it('should return valid tag format', async () => { const msg = await createGetUserTagMessage(identity.messageSigner); const response = await ws.sendAndWaitForResponse(msg, getGetUserTagPredicate(), 5000); - const parsedResponse = rpcResponseParser.getUserTag(response); + const parsedResponse = parseGetUserTagResponse(response); // Verify the tag format matches expected pattern (e.g., 'UX123D8C') expect(parsedResponse.params.tag).toMatch(/^[A-Z0-9]+$/); diff --git a/integration/tests/ledger_transactions.test.ts b/integration/tests/ledger_transactions.test.ts index 325049ad8..f2d0d5c08 100644 --- a/integration/tests/ledger_transactions.test.ts +++ b/integration/tests/ledger_transactions.test.ts @@ -3,7 +3,12 @@ import { DatabaseUtils } from '@/databaseUtils'; import { Identity } from '@/identity'; import { CONFIG } from '@/setup'; import { getGetLedgerTransactionsPredicate, TestWebSocket } from '@/ws'; -import { createGetLedgerTransactionsMessage, rpcResponseParser, GetLedgerTransactionsFilters, TxType } from '@erc7824/nitrolite'; +import { + createGetLedgerTransactionsMessage, + GetLedgerTransactionsFilters, + parseGetLedgerTransactionsResponse, + RPCTxType, +} from '@erc7824/nitrolite'; describe('Ledger Transactions Integration', () => { let ws: TestWebSocket; @@ -44,10 +49,9 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); }); it('should successfully request ledger transactions with asset filter', async () => { @@ -62,14 +66,16 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // If there are transactions, they should all be for usdc - if (parsedResponse.params.length > 0) { - parsedResponse.params.forEach((transaction) => { + if (ledgerTransactions.length > 0) { + ledgerTransactions.forEach((transaction) => { expect(transaction.asset).toBe('usdc'); }); } @@ -78,7 +84,7 @@ describe('Ledger Transactions Integration', () => { it('should successfully request ledger transactions with tx_type filter', async () => { const accountId = identity.walletAddress; const filters: GetLedgerTransactionsFilters = { - tx_type: TxType.Deposit, + tx_type: RPCTxType.Deposit, }; const msg = await createGetLedgerTransactionsMessage(identity.messageSigner, accountId, filters); @@ -87,15 +93,17 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // If there are transactions, they should all be of type 'deposit' - if (parsedResponse.params.length > 0) { - parsedResponse.params.forEach((transaction) => { - expect(transaction.txType).toBe(TxType.Deposit); + if (ledgerTransactions.length > 0) { + ledgerTransactions.forEach((transaction) => { + expect(transaction.txType).toBe(RPCTxType.Deposit); }); } }); @@ -113,13 +121,15 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // Should not return more than the limit - expect(parsedResponse.params.length).toBeLessThanOrEqual(5); + expect(ledgerTransactions.length).toBeLessThanOrEqual(5); }); it('should successfully request ledger transactions with sort order', async () => { @@ -135,16 +145,18 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // If there are multiple transactions, they should be sorted by createdAt in descending order - if (parsedResponse.params.length > 1) { - for (let i = 0; i < parsedResponse.params.length - 1; i++) { - const currentDate = parsedResponse.params[i].createdAt; - const nextDate = parsedResponse.params[i + 1].createdAt; + if (ledgerTransactions.length > 1) { + for (let i = 0; i < ledgerTransactions.length - 1; i++) { + const currentDate = ledgerTransactions[i].createdAt; + const nextDate = ledgerTransactions[i + 1].createdAt; expect(currentDate.getTime()).toBeGreaterThanOrEqual(nextDate.getTime()); } } @@ -154,7 +166,7 @@ describe('Ledger Transactions Integration', () => { const accountId = identity.walletAddress; const filters: GetLedgerTransactionsFilters = { asset: 'usdc', - tx_type: TxType.Deposit, + tx_type: RPCTxType.Deposit, offset: 0, limit: 3, sort: 'desc', @@ -166,18 +178,20 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // Should not return more than the limit - expect(parsedResponse.params.length).toBeLessThanOrEqual(3); + expect(ledgerTransactions.length).toBeLessThanOrEqual(3); // All transactions should match the filters - parsedResponse.params.forEach((transaction) => { + ledgerTransactions.forEach((transaction) => { expect(transaction.asset).toBe('usdc'); - expect(transaction.txType).toBe(TxType.Deposit); + expect(transaction.txType).toBe(RPCTxType.Deposit); }); }); @@ -193,11 +207,13 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); - expect(parsedResponse.params.length).toBe(0); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); + expect(ledgerTransactions.length).toBe(0); }); it('should handle invalid account ID gracefully', async () => { @@ -209,12 +225,14 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // Should return empty array for invalid/non-existent account - expect(parsedResponse.params.length).toBe(0); + expect(ledgerTransactions.length).toBe(0); }); }); }); diff --git a/integration/tests/lifecycle.test.ts b/integration/tests/lifecycle.test.ts index caabce687..f479f642d 100644 --- a/integration/tests/lifecycle.test.ts +++ b/integration/tests/lifecycle.test.ts @@ -15,8 +15,8 @@ import { getCloseChannelPredicate, } from '@/ws'; import { - AppDefinition, - AppSessionAllocation, + RPCAppDefinition, + RPCAppSessionAllocation, createAppSessionMessage, createCloseAppSessionMessage, createCloseChannelMessage, @@ -25,7 +25,13 @@ import { createResizeChannelMessage, createSubmitAppStateMessage, RPCChannelStatus, - rpcResponseParser, + parseCloseAppSessionResponse, + parseCloseChannelResponse, + parseCreateAppSessionResponse, + parseGetAppSessionsResponse, + parseGetLedgerBalancesResponse, + parseResizeChannelResponse, + parseSubmitAppStateResponse, } from '@erc7824/nitrolite'; import { Hex, parseUnits } from 'viem'; @@ -63,11 +69,11 @@ describe('Close channel', () => { 1000 ); - const getAppSessionsParsedResponse = rpcResponseParser.getAppSessions(getAppSessionsResponse); + const getAppSessionsParsedResponse = parseGetAppSessionsResponse(getAppSessionsResponse); expect(getAppSessionsParsedResponse).toBeDefined(); - expect(getAppSessionsParsedResponse.params).toHaveLength(1); + expect(getAppSessionsParsedResponse.params.appSessions).toHaveLength(1); - const appSession = getAppSessionsParsedResponse.params[0]; + const appSession = getAppSessionsParsedResponse.params.appSessions[0]; expect(appSession.appSessionId).toBe(appSessionId); expect(appSession.sessionData).toBeDefined(); @@ -103,17 +109,15 @@ describe('Close channel', () => { }; const submitAppStateUpdate = async ( - allocations: AppSessionAllocation[], + allocations: RPCAppSessionAllocation[], sessionData: object, expectedVersion: number ) => { - const submitAppStateMsg = await createSubmitAppStateMessage(appIdentity.messageSigner, [ - { - app_session_id: appSessionId as Hex, - allocations, - session_data: JSON.stringify(sessionData), - }, - ]); + const submitAppStateMsg = await createSubmitAppStateMessage(appIdentity.messageSigner, { + app_session_id: appSessionId as Hex, + allocations, + session_data: JSON.stringify(sessionData), + }); const submitAppStateResponse = await appWS.sendAndWaitForResponse( submitAppStateMsg, @@ -121,7 +125,7 @@ describe('Close channel', () => { 1000 ); - const submitAppStateParsedResponse = rpcResponseParser.submitAppState(submitAppStateResponse); + const submitAppStateParsedResponse = parseSubmitAppStateResponse(submitAppStateResponse); expect(submitAppStateParsedResponse).toBeDefined(); expect(submitAppStateParsedResponse.params.appSessionId).toBe(appSessionId); expect(submitAppStateParsedResponse.params.status).toBe(RPCChannelStatus.Open); @@ -131,17 +135,15 @@ describe('Close channel', () => { }; const closeAppSessionWithState = async ( - allocations: AppSessionAllocation[], + allocations: RPCAppSessionAllocation[], sessionData: object, expectedVersion: number ) => { - const closeAppSessionMsg = await createCloseAppSessionMessage(appIdentity.messageSigner, [ - { - app_session_id: appSessionId as Hex, - allocations, - session_data: JSON.stringify(sessionData), - }, - ]); + const closeAppSessionMsg = await createCloseAppSessionMessage(appIdentity.messageSigner, { + app_session_id: appSessionId as Hex, + allocations, + session_data: JSON.stringify(sessionData), + }); const closeAppSessionResponse = await appWS.sendAndWaitForResponse( closeAppSessionMsg, @@ -151,7 +153,7 @@ describe('Close channel', () => { expect(closeAppSessionResponse).toBeDefined(); - const closeAppSessionParsedResponse = rpcResponseParser.closeAppSession(closeAppSessionResponse); + const closeAppSessionParsedResponse = parseCloseAppSessionResponse(closeAppSessionResponse); expect(closeAppSessionParsedResponse).toBeDefined(); expect(closeAppSessionParsedResponse.params.appSessionId).toBe(appSessionId); expect(closeAppSessionParsedResponse.params.status).toBe(RPCChannelStatus.Closed); @@ -215,8 +217,8 @@ describe('Close channel', () => { it('should create app session with allowance for participant to deposit', async () => { await createAuthSessionWithClearnode(appWS, appIdentity, { - wallet: appIdentity.walletAddress, - participant: appIdentity.sessionAddress, + address: appIdentity.walletAddress, + session_key: appIdentity.sessionAddress, app_name: 'App Domain', expire: String(Math.floor(Date.now() / 1000) + 3600), // 1 hour expiration scope: 'console', @@ -241,16 +243,17 @@ describe('Close channel', () => { 1000 ); - const getLedgerBalancesParsedResponse = rpcResponseParser.getLedgerBalances(getLedgerBalancesResponse); + const getLedgerBalancesParsedResponse = parseGetLedgerBalancesResponse(getLedgerBalancesResponse); expect(getLedgerBalancesParsedResponse).toBeDefined(); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params[0].amount).toBe((decimalDepositAmount * BigInt(10)).toString()); - expect(getLedgerBalancesParsedResponse.params[0].asset).toBe('USDC'); + + const ledgerBalances = getLedgerBalancesParsedResponse.params.ledgerBalances; + expect(ledgerBalances).toHaveLength(1); + expect(ledgerBalances[0].amount).toBe((decimalDepositAmount * BigInt(10)).toString()); + expect(ledgerBalances[0].asset).toBe('USDC'); }); it('should create app session', async () => { - const definition: AppDefinition = { + const definition: RPCAppDefinition = { protocol: 'nitroliterpc', participants: [appIdentity.walletAddress, appCPIdentity.walletAddress], weights: [100, 0], @@ -272,20 +275,18 @@ describe('Close channel', () => { }, ]; - const createAppSessionMsg = await createAppSessionMessage(appIdentity.messageSigner, [ - { - definition, - allocations, - session_data: JSON.stringify(SESSION_DATA_WAITING), - }, - ]); + const createAppSessionMsg = await createAppSessionMessage(appIdentity.messageSigner, { + definition, + allocations, + session_data: JSON.stringify(SESSION_DATA_WAITING), + }); const createAppSessionResponse = await appWS.sendAndWaitForResponse( createAppSessionMsg, getCreateAppSessionPredicate(), 1000 ); - const createAppSessionParsedResponse = rpcResponseParser.createAppSession(createAppSessionResponse); + const createAppSessionParsedResponse = parseCreateAppSessionResponse(createAppSessionResponse); expect(createAppSessionParsedResponse).toBeDefined(); expect(createAppSessionParsedResponse.params.appSessionId).toBeDefined(); @@ -353,11 +354,13 @@ describe('Close channel', () => { 1000 ); - const getLedgerBalancesParsedResponse = rpcResponseParser.getLedgerBalances(getLedgerBalancesResponse); + const getLedgerBalancesParsedResponse = parseGetLedgerBalancesResponse(getLedgerBalancesResponse); expect(getLedgerBalancesParsedResponse).toBeDefined(); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params[0].amount).toBe((decimalDepositAmount * BigInt(9)).toString()); // 1000 - 100 - expect(getLedgerBalancesParsedResponse.params[0].asset).toBe('USDC'); + + const ledgerBalances = getLedgerBalancesParsedResponse.params.ledgerBalances; + expect(ledgerBalances).toHaveLength(1); + expect(ledgerBalances[0].amount).toBe((decimalDepositAmount * BigInt(9)).toString()); // 1000 - 100 + expect(ledgerBalances[0].asset).toBe('USDC'); }); it('should update ledger balances for receiving side', async () => { @@ -371,11 +374,13 @@ describe('Close channel', () => { 1000 ); - const getLedgerBalancesParsedResponse = rpcResponseParser.getLedgerBalances(getLedgerBalancesResponse); + const getLedgerBalancesParsedResponse = parseGetLedgerBalancesResponse(getLedgerBalancesResponse); expect(getLedgerBalancesParsedResponse).toBeDefined(); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params[0].amount).toBe((decimalDepositAmount * BigInt(11)).toString()); // 1000 + 100 - expect(getLedgerBalancesParsedResponse.params[0].asset).toBe('USDC'); + + const ledgerBalances = getLedgerBalancesParsedResponse.params.ledgerBalances; + expect(ledgerBalances).toHaveLength(1); + expect(ledgerBalances[0].amount).toBe((decimalDepositAmount * BigInt(11)).toString()); // 1000 + 100 + expect(ledgerBalances[0].asset).toBe('USDC'); }); it('should close channel and withdraw without app funds', async () => { @@ -384,7 +389,7 @@ describe('Close channel', () => { const closeResponse = await ws.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); expect(closeResponse).toBeDefined(); - const { params: closeResponseParams } = rpcResponseParser.closeChannel(closeResponse); + const { params: closeResponseParams } = parseCloseChannelResponse(closeResponse); const closeChannelTxHash = await client.closeChannel({ finalState: { intent: closeResponseParams.state.intent, @@ -406,16 +411,14 @@ describe('Close channel', () => { }); it('should resize channel by withdrawing received funds from app to channel', async () => { - const msg = await createResizeChannelMessage(cpIdentity.messageSigner, [ - { - channel_id: cpChannelId, - allocate_amount: depositAmount, - funds_destination: cpIdentity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(cpIdentity.messageSigner, { + channel_id: cpChannelId, + allocate_amount: depositAmount, + funds_destination: cpIdentity.walletAddress, + }); const resizeResponse = await cpWS.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.state.allocations).toBeDefined(); expect(resizeResponseParams.state.allocations).toHaveLength(2); @@ -469,7 +472,7 @@ describe('Close channel', () => { const closeResponse = await cpWS.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); expect(closeResponse).toBeDefined(); - const { params: closeResponseParams } = rpcResponseParser.closeChannel(closeResponse); + const { params: closeResponseParams } = parseCloseChannelResponse(closeResponse); const closeChannelTxHash = await cpClient.closeChannel({ finalState: { intent: closeResponseParams.state.intent, diff --git a/integration/tests/resize_channel.test.ts b/integration/tests/resize_channel.test.ts index 7ba5f7dea..49843a98f 100644 --- a/integration/tests/resize_channel.test.ts +++ b/integration/tests/resize_channel.test.ts @@ -5,7 +5,7 @@ import { Identity } from '@/identity'; import { TestNitroliteClient } from '@/nitroliteClient'; import { CONFIG } from '@/setup'; import { getResizeChannelPredicate, TestWebSocket } from '@/ws'; -import { createResizeChannelMessage, rpcResponseParser } from '@erc7824/nitrolite'; +import { createResizeChannelMessage, parseResizeChannelResponse } from '@erc7824/nitrolite'; import { Hex, parseUnits } from 'viem'; describe('Resize channel', () => { @@ -64,17 +64,15 @@ describe('Resize channel', () => { ); expect(preResizeChannelBalance).toBe(depositAmount * BigInt(5)); // 500 - const msg = await createResizeChannelMessage(identity.messageSigner, [ - { - channel_id: createResponseParams.channelId, - resize_amount: depositAmount, - allocate_amount: parseUnits('0', 6), - funds_destination: identity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(identity.messageSigner, { + channel_id: createResponseParams.channelId, + resize_amount: depositAmount, + allocate_amount: parseUnits('0', 6), + funds_destination: identity.walletAddress, + }); const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.channelId).toBe(createResponseParams.channelId); expect(resizeResponseParams.state.stateData).toBeDefined(); expect(resizeResponseParams.state.intent).toBe(2); // StateIntent.RESIZE // TODO: add enum to sdk @@ -155,17 +153,15 @@ describe('Resize channel', () => { ); expect(preResizeChannelBalance).toBe(depositAmount * BigInt(5)); // 500 - const msg = await createResizeChannelMessage(identity.messageSigner, [ - { - channel_id: createResponseParams.channelId, - resize_amount: -depositAmount, - allocate_amount: parseUnits('0', 6), - funds_destination: identity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(identity.messageSigner, { + channel_id: createResponseParams.channelId, + resize_amount: -depositAmount, + allocate_amount: parseUnits('0', 6), + funds_destination: identity.walletAddress, + }); const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.state.allocations).toBeDefined(); expect(resizeResponseParams.state.allocations).toHaveLength(2); expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); @@ -237,17 +233,15 @@ describe('Resize channel', () => { ); expect(preResizeChannelBalance).toBe(depositAmount * BigInt(5)); // 500 - const msg = await createResizeChannelMessage(identity.messageSigner, [ - { - channel_id: createResponseParams.channelId, - resize_amount: parseUnits('0', 6), - allocate_amount: -depositAmount, - funds_destination: identity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(identity.messageSigner, { + channel_id: createResponseParams.channelId, + resize_amount: parseUnits('0', 6), + allocate_amount: -depositAmount, + funds_destination: identity.walletAddress, + }); const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.state.allocations).toBeDefined(); expect(resizeResponseParams.state.allocations).toHaveLength(2); expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); diff --git a/sdk/src/rpc/api.ts b/sdk/src/rpc/api.ts index 285c61219..5deeac77b 100644 --- a/sdk/src/rpc/api.ts +++ b/sdk/src/rpc/api.ts @@ -12,10 +12,10 @@ import { EIP712AuthDomain, EIP712AuthMessage, AuthChallengeResponse, - RequestData, RPCMethod, + RPCData, + GetLedgerTransactionsFilters, RPCChannelStatus, - ResponsePayload, } from './types'; import { NitroliteRPC } from './nitrolite'; import { generateRequestId, getCurrentTimestamp } from './utils'; @@ -24,7 +24,6 @@ import { CreateAppSessionRequestParams, SubmitAppStateRequestParams, ResizeChannelRequestParams, - GetLedgerTransactionsFilters, GetLedgerTransactionsRequestParams, TransferRequestParams, CreateChannelRequestParams, @@ -45,17 +44,12 @@ export async function createAuthRequestMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const allowances = Object.values(params.allowances || {}).map((v) => [v.asset, v.amount]); - const paramsArray = [ - params.wallet, - params.participant, - params.app_name, - allowances, - params.expire ?? '', - params.scope ?? '', - params.application ?? '', - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthRequest, paramsArray, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthRequest, + params, + requestId, + timestamp, + }); return JSON.stringify(request); } @@ -76,9 +70,14 @@ export async function createAuthVerifyMessageFromChallenge( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ challenge: challenge }]; - - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthVerify, [params], timestamp); + const params = { challenge: challenge }; + + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthVerify, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -101,8 +100,13 @@ export async function createAuthVerifyMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ challenge: challenge.params.challengeMessage }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthVerify, params, timestamp); + const params = { challenge: challenge.params.challengeMessage }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthVerify, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); } @@ -121,8 +125,13 @@ export async function createAuthVerifyMessageWithJWT( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ jwt: jwtToken }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthVerify, params, timestamp); + const params = { jwt: jwtToken }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthVerify, + params, + requestId, + timestamp, + }); return JSON.stringify(request); } @@ -139,7 +148,12 @@ export async function createPingMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.Ping, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.Ping, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -158,7 +172,12 @@ export async function createGetConfigMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetConfig, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetConfig, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -177,7 +196,12 @@ export async function createGetUserTagMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetUserTag, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetUserTag, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -198,8 +222,13 @@ export async function createGetLedgerBalancesMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ participant: participant }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetLedgerBalances, params, timestamp); + const params = { participant: participant }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetLedgerBalances, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -222,13 +251,16 @@ export async function createGetLedgerEntriesMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - account_id: accountId, - ...(asset ? { asset } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetLedgerEntries, params, timestamp); + const params = { + account_id: accountId, + ...(asset ? { asset } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetLedgerEntries, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -261,13 +293,17 @@ export async function createGetLedgerTransactionsMessage( }); } - const paramsObj: GetLedgerTransactionsRequestParams = { + const params: GetLedgerTransactionsRequestParams = { account_id: accountId, ...filteredParams, }; - const params = [paramsObj]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetLedgerTransactions, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetLedgerTransactions, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -288,8 +324,13 @@ export async function createGetAppDefinitionMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ app_session_id: appSessionId }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetAppDefinition, params, timestamp); + const params = { app_session_id: appSessionId }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetAppDefinition, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -308,17 +349,20 @@ export async function createGetAppDefinitionMessage( export async function createGetAppSessionsMessage( signer: MessageSigner, participant: Address, - status?: string, + status?: RPCChannelStatus, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - participant, - ...(status ? { status } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetAppSessions, params, timestamp); + const params = { + participant, + ...(status ? { status } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetAppSessions, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -335,11 +379,16 @@ export async function createGetAppSessionsMessage( */ export async function createAppSessionMessage( signer: MessageSigner, - params: CreateAppSessionRequestParams[], + params: CreateAppSessionRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CreateAppSession, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CreateAppSession, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -356,11 +405,16 @@ export async function createAppSessionMessage( */ export async function createSubmitAppStateMessage( signer: MessageSigner, - params: SubmitAppStateRequestParams[], + params: SubmitAppStateRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.SubmitAppState, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.SubmitAppState, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -378,11 +432,16 @@ export async function createSubmitAppStateMessage( */ export async function createCloseAppSessionMessage( signer: MessageSigner, - params: CloseAppSessionRequestParams[], + params: CloseAppSessionRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CloseAppSession, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CloseAppSession, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -401,11 +460,19 @@ export async function createCloseAppSessionMessage( export async function createApplicationMessage( signer: MessageSigner, appSessionId: Hex, - messageParams: any[], + messageParams: any, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createAppRequest(requestId, RPCMethod.Message, messageParams, timestamp, appSessionId); + const request = NitroliteRPC.createAppRequest( + { + method: RPCMethod.Message, + params: messageParams, + requestId, + timestamp, + }, + appSessionId, + ); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -439,8 +506,13 @@ export async function createCloseChannelMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ channel_id: channelId, funds_destination: fundDestination }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CloseChannel, params, timestamp); + const params = { channel_id: channelId, funds_destination: fundDestination }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CloseChannel, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -457,11 +529,16 @@ export async function createCloseChannelMessage( */ export async function createResizeChannelMessage( signer: MessageSigner, - params: ResizeChannelRequestParams[], + params: ResizeChannelRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.ResizeChannel, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.ResizeChannel, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest, (_, value) => (typeof value === 'bigint' ? value.toString() : value)); @@ -484,13 +561,16 @@ export async function createGetChannelsMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - ...(participant ? { participant } : {}), - ...(status ? { status } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetChannels, params, timestamp); + const params = { + ...(participant ? { participant } : {}), + ...(status ? { status } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetChannels, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); } @@ -508,7 +588,12 @@ export async function createGetRPCHistoryMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetRPCHistory, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetRPCHistory, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -529,12 +614,15 @@ export async function createGetAssetsMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - ...(chainId ? { chain_id: chainId } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetAssets, params, timestamp); + const params = { + ...(chainId ? { chain_id: chainId } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetAssets, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -551,13 +639,13 @@ export async function createGetAssetsMessage( */ export async function createTransferMessage( signer: MessageSigner, - transferParams: TransferRequestParams, + params: TransferRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { // Validate that exactly one destination type is provided (XOR logic) - const hasDestination = !!transferParams.destination; - const hasDestinationTag = !!transferParams.destination_user_tag; + const hasDestination = !!params.destination; + const hasDestinationTag = !!params.destination_user_tag; if (hasDestination === hasDestinationTag) { throw new Error( @@ -567,8 +655,12 @@ export async function createTransferMessage( ); } - const params = [transferParams]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.Transfer, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.Transfer, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -587,7 +679,7 @@ export function createEIP712AuthMessageSigner( partialMessage: PartialEIP712AuthMessage, domain: EIP712AuthDomain, ): MessageSigner { - return async (payload: RequestData | ResponsePayload): Promise => { + return async (payload: RPCData): Promise => { const address = walletClient.account?.address; if (!address) { throw new Error('Wallet client is not connected or does not have an account.'); @@ -603,19 +695,12 @@ export function createEIP712AuthMessageSigner( // Safely extract the challenge from the payload for an AuthVerify request. // The expected structure is `[id, 'auth_verify', [{ challenge: '...' }], ts]` const params = payload[2]; - const firstParam = Array.isArray(params) ? params[0] : undefined; - - if ( - typeof firstParam !== 'object' || - firstParam === null || - !('challenge' in firstParam) || - typeof firstParam.challenge !== 'string' - ) { + if (!('challenge' in params) || typeof params.challenge !== 'string') { throw new Error('Invalid payload for AuthVerify: The challenge string is missing or malformed.'); } - // After the check, TypeScript knows `firstParam` is an object with a `challenge` property of type string. - const challengeUUID: string = firstParam.challenge; + // After the check, TypeScript knows `params` is an object with a `challenge` property of type string. + const challengeUUID: string = params.challenge; const message: EIP712AuthMessage = { ...partialMessage, @@ -653,7 +738,7 @@ export function createEIP712AuthMessageSigner( * @returns A MessageSigner function that signs the payload using ECDSA. */ export function createECDSAMessageSigner(privateKey: Hex): MessageSigner { - return async (payload: RequestData | ResponsePayload): Promise => { + return async (payload: RPCData): Promise => { try { const message = toHex(JSON.stringify(payload, (_, v) => (typeof v === 'bigint' ? v.toString() : v))); diff --git a/sdk/src/rpc/nitrolite.ts b/sdk/src/rpc/nitrolite.ts index e93f2fa1f..2d4c6586b 100644 --- a/sdk/src/rpc/nitrolite.ts +++ b/sdk/src/rpc/nitrolite.ts @@ -1,16 +1,12 @@ import { Address, Hex } from 'viem'; import { NitroliteRPCMessage, - RequestData, - NitroliteRPCErrorDetail, MessageSigner, SingleMessageVerifier, MultiMessageVerifier, - ParsedResponse, - ResponsePayload, ApplicationRPCMessage, - RPCResponse, - RPCMethod, + RPCRequest, + RPCData, } from './types'; import { getCurrentTimestamp, generateRequestId } from './utils'; @@ -28,14 +24,15 @@ export class NitroliteRPC { * @param timestamp - Timestamp for the request. Defaults to the current time. * @returns A formatted NitroliteRPCMessage object for the request. */ - static createRequest( - requestId: number = generateRequestId(), - method: RPCMethod, - params: any[] = [], - timestamp: number = getCurrentTimestamp(), - ): NitroliteRPCMessage { - const requestData: RequestData = [requestId, method, params, timestamp]; - const message: NitroliteRPCMessage = { req: requestData, sig: [] }; + static createRequest({ + method, + params = {}, + requestId = generateRequestId(), + timestamp = getCurrentTimestamp(), + signatures = [], + }: RPCRequest): NitroliteRPCMessage { + const requestData: RPCData = [requestId, method, params, timestamp]; + const message: NitroliteRPCMessage = { req: requestData, sig: signatures }; return message; } @@ -50,123 +47,14 @@ export class NitroliteRPC { * @returns A formatted NitroliteRPCMessage object for the request. */ static createAppRequest( - requestId: number = generateRequestId(), - method: RPCMethod, - params: any[] = [], - timestamp: number = getCurrentTimestamp(), + { requestId = generateRequestId(), method, params = {}, timestamp = getCurrentTimestamp() }: RPCRequest, sid: Hex, ): ApplicationRPCMessage { - const requestData: RequestData = [requestId, method, params, timestamp]; + const requestData: RPCData = [requestId, method, params, timestamp]; const message: ApplicationRPCMessage = { req: requestData, sid }; return message; } - /** - * Parses a raw message string or object received from the broker, - * validating its structure as a Nitrolite RPC response. Handles both - * messages with and without the top-level 'sid' field. - * Does NOT verify the signature. - * - * @param rawMessage - The raw JSON string or pre-parsed object received. - * @returns A ParsedResponse object containing the extracted data and validation status. - */ - static parseResponse(rawMessage: string | object): ParsedResponse { - // TODO: either merge or replace it with parseRPCResponse from utils.ts - let message: any; - - try { - message = typeof rawMessage === 'string' ? JSON.parse(rawMessage) : rawMessage; - } catch (e) { - console.error('Failed to parse incoming message:', e); - return { - isValid: false, - error: 'Message parsing failed', - }; - } - - if ( - !message || - typeof message !== 'object' || - !message.res || - !Array.isArray(message.res) || - message.res.length !== 4 - ) { - return { - isValid: false, - error: "Invalid message structure: Missing or invalid 'res' array.", - }; - } - - const [requestId, method, dataPayload, timestamp] = message.res; - const sid = typeof message.sid === 'string' ? message.sid : undefined; - - if ( - typeof requestId !== 'number' || - typeof method !== 'string' || - !Object.values(RPCMethod).includes(method as RPCMethod) || - !Array.isArray(dataPayload) || - typeof timestamp !== 'number' - ) { - return { - isValid: false, - requestId, - method, - sid, - timestamp, - error: "Invalid 'res' payload structure or types.", - }; - } - - let data: any[] | NitroliteRPCErrorDetail; - let isError = false; - - if (method === RPCMethod.Error) { - isError = true; - if ( - dataPayload.length === 1 && - typeof dataPayload[0] === 'object' && - dataPayload[0] !== null && - 'error' in dataPayload[0] - ) { - data = dataPayload[0] as NitroliteRPCErrorDetail; - } else { - return { - isValid: false, - requestId, - method, - sid, - timestamp, - error: 'Malformed error response payload.', - }; - } - } else { - data = dataPayload; - } - - return { - isValid: true, - isError, - requestId, - method: method as RPCMethod, - data, - sid, - timestamp, - }; - } - - /** - * Type guard to check if a response is a specific RPC response type. - * @param response - The response to check - * @param method - The method name to check against - * @returns True if the response is of the specified type - */ - static isResponseType( - response: ParsedResponse, - method: T['method'], - ): response is ParsedResponse & { data: T['params'] } { - return response.isValid && !response.isError && response.method === method; - } - /** * Extracts the payload (req or res array) from a message for signing or verification. * @@ -175,7 +63,7 @@ export class NitroliteRPC { * @throws Error if the message doesn't contain a 'req' or 'res' field. * @private */ - private static getMessagePayload(message: NitroliteRPCMessage): RequestData | ResponsePayload { + private static getMessagePayload(message: NitroliteRPCMessage): RPCData { if (message.req) return message.req; if (message.res) return message.res; throw new Error("Message must contain either 'req' or 'res' field"); diff --git a/sdk/src/rpc/parse/app.ts b/sdk/src/rpc/parse/app.ts index 7bfce8bbb..9e4a8da84 100644 --- a/sdk/src/rpc/parse/app.ts +++ b/sdk/src/rpc/parse/app.ts @@ -7,118 +7,101 @@ import { CloseAppSessionResponseParams, GetAppDefinitionResponseParams, GetAppSessionsResponseParams, - RPCChannelStatus, + RPCAppSession, } from '../types'; -import { hexSchema, addressSchema, statusEnum, ParamsParser } from './common'; +import { hexSchema, addressSchema, statusEnum, ParamsParser, dateSchema } from './common'; + +const AppSessionObjectSchema = z + .object({ + app_session_id: hexSchema, + status: statusEnum, + participants: z.array(addressSchema), + protocol: z.string(), + challenge: z.number(), + weights: z.array(z.number()), + quorum: z.number(), + version: z.number(), + nonce: z.number(), + created_at: dateSchema, + updated_at: dateSchema, + session_data: z.string().optional(), + }) + .transform( + (raw): RPCAppSession => ({ + appSessionId: raw.app_session_id, + status: raw.status, + participants: raw.participants, + protocol: raw.protocol, + challenge: raw.challenge, + weights: raw.weights, + quorum: raw.quorum, + version: raw.version, + nonce: raw.nonce, + createdAt: raw.created_at, + updatedAt: raw.updated_at, + sessionData: raw.session_data, + }), + ); const CreateAppSessionParamsSchema = z - .array( - z.object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }).transform( - (raw) => - ({ - appSessionId: raw.app_session_id as `0x${string}`, - version: raw.version, - status: raw.status as RPCChannelStatus, - }) as CreateAppSessionResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }) + .transform( + (raw): CreateAppSessionResponseParams => ({ + appSessionId: raw.app_session_id, + version: raw.version, + status: raw.status, + }), + ); const SubmitAppStateParamsSchema = z - .array( - z.object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }).transform( - (raw) => - ({ - appSessionId: raw.app_session_id as `0x${string}`, - version: raw.version, - status: raw.status as RPCChannelStatus, - }) as SubmitAppStateResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }) + .transform( + (raw): SubmitAppStateResponseParams => ({ + appSessionId: raw.app_session_id, + version: raw.version, + status: raw.status, + }), + ); const CloseAppSessionParamsSchema = z - .array( - z.object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }).transform( - (raw) => - ({ - appSessionId: raw.app_session_id as `0x${string}`, - version: raw.version, - status: raw.status as RPCChannelStatus, - }) as CloseAppSessionResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }) + .transform( + (raw): CloseAppSessionResponseParams => ({ + appSessionId: raw.app_session_id, + version: raw.version, + status: raw.status, + }), + ); const GetAppDefinitionParamsSchema = z - .array( - z - .object({ - protocol: z.string(), - participants: z.array(addressSchema), - weights: z.array(z.number()), - quorum: z.number(), - challenge: z.number(), - nonce: z.number(), - }) - .transform( - (raw) => - ({ - protocol: raw.protocol, - participants: raw.participants as Address[], - weights: raw.weights, - quorum: raw.quorum, - challenge: raw.challenge, - nonce: raw.nonce, - }) as GetAppDefinitionResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + protocol: z.string(), + participants: z.array(addressSchema), + weights: z.array(z.number()), + quorum: z.number(), + challenge: z.number(), + nonce: z.number(), + }) + .transform( + (raw): GetAppDefinitionResponseParams => ({ + protocol: raw.protocol, + participants: raw.participants as Address[], + weights: raw.weights, + quorum: raw.quorum, + challenge: raw.challenge, + nonce: raw.nonce, + }), + ); const GetAppSessionsParamsSchema = z - .array( - z.array( - z - .object({ - app_session_id: hexSchema, - status: statusEnum, - participants: z.array(addressSchema), - protocol: z.string(), - challenge: z.number(), - weights: z.array(z.number()), - quorum: z.number(), - version: z.number(), - nonce: z.number(), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - updated_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - session_data: z.string().optional(), - }) - .transform( - (s) => - ({ - appSessionId: s.app_session_id as `0x${string}`, - status: s.status as RPCChannelStatus, - participants: s.participants as Address[], - protocol: s.protocol, - challenge: s.challenge, - weights: s.weights, - quorum: s.quorum, - version: s.version, - nonce: s.nonce, - createdAt: s.created_at, - updatedAt: s.updated_at, - sessionData: s.session_data, - }) as GetAppSessionsResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetAppSessionsResponseParams[]); + .object({ + app_sessions: z.array(AppSessionObjectSchema), + }) + .transform( + (raw): GetAppSessionsResponseParams => ({ + appSessions: raw.app_sessions, + }), + ); export const appParamsParsers: Record> = { [RPCMethod.CreateAppSession]: (params) => CreateAppSessionParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/asset.ts b/sdk/src/rpc/parse/asset.ts index 0b63e9451..cae0fc50e 100644 --- a/sdk/src/rpc/parse/asset.ts +++ b/sdk/src/rpc/parse/asset.ts @@ -1,29 +1,39 @@ import { z } from 'zod'; -import { Address } from 'viem'; -import { RPCMethod, GetAssetsResponseParams } from '../types'; +import { RPCMethod, GetAssetsResponseParams, RPCAsset, AssetsResponseParams } from '../types'; import { addressSchema, ParamsParser } from './common'; +const AssetObjectSchema = z + .object({ token: addressSchema, chain_id: z.number(), symbol: z.string(), decimals: z.number() }) + .transform( + (raw): RPCAsset => ({ + token: raw.token, + chainId: raw.chain_id, + symbol: raw.symbol, + decimals: raw.decimals, + }), + ); + const GetAssetsParamsSchema = z - .array( - z.array( - z - .object({ token: addressSchema, chain_id: z.number(), symbol: z.string(), decimals: z.number() }) - .transform( - (a) => - ({ - token: a.token as Address, - chainId: a.chain_id, - symbol: a.symbol, - decimals: a.decimals, - }) as GetAssetsResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetAssetsResponseParams[]); + .object({ + assets: z.array(AssetObjectSchema), + }) + .transform( + (raw): GetAssetsResponseParams => ({ + assets: raw.assets, + }), + ); + +const AssetsParamsSchema = z + .object({ + assets: z.array(AssetObjectSchema), + }) + .transform( + (raw): AssetsResponseParams => ({ + assets: raw.assets, + }), + ); export const assetParamsParsers: Record> = { [RPCMethod.GetAssets]: (params) => GetAssetsParamsSchema.parse(params), - [RPCMethod.Assets]: (params) => GetAssetsParamsSchema.parse(params), // Alias + [RPCMethod.Assets]: (params) => AssetsParamsSchema.parse(params), }; diff --git a/sdk/src/rpc/parse/auth.ts b/sdk/src/rpc/parse/auth.ts index fd16cdcd4..a12b1f5d8 100644 --- a/sdk/src/rpc/parse/auth.ts +++ b/sdk/src/rpc/parse/auth.ts @@ -1,47 +1,30 @@ import { z } from 'zod'; -import { Address } from 'viem'; import { RPCMethod, AuthChallengeResponseParams, AuthVerifyResponseParams, AuthRequestResponseParams } from '../types'; import { addressSchema, ParamsParser } from './common'; const AuthChallengeParamsSchema = z - .array( - z - .object({ challenge_message: z.string() }) - .transform((raw) => ({ challengeMessage: raw.challenge_message }) as AuthChallengeResponseParams), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ challenge_message: z.string() }) + .transform((raw): AuthChallengeResponseParams => ({ challengeMessage: raw.challenge_message })); const AuthVerifyParamsSchema = z - .array( - z - .object({ - address: addressSchema, - session_key: addressSchema, - success: z.boolean(), - jwt_token: z.string().optional(), - }) - .transform( - (raw) => - ({ - address: raw.address as Address, - sessionKey: raw.session_key as Address, - success: raw.success, - jwtToken: raw.jwt_token, - }) as AuthVerifyResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + address: addressSchema, + session_key: addressSchema, + success: z.boolean(), + jwt_token: z.string().optional(), + }) + .transform( + (raw): AuthVerifyResponseParams => ({ + address: raw.address, + sessionKey: raw.session_key, + success: raw.success, + jwtToken: raw.jwt_token, + }), + ); const AuthRequestParamsSchema = z - .array( - z - .object({ challenge_message: z.string() }) - .transform((raw) => ({ challengeMessage: raw.challenge_message }) as AuthRequestResponseParams), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ challenge_message: z.string() }) + .transform((raw): AuthRequestResponseParams => ({ challengeMessage: raw.challenge_message })); export const authParamsParsers: Record> = { [RPCMethod.AuthChallenge]: (params) => AuthChallengeParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/channel.ts b/sdk/src/rpc/parse/channel.ts index ee5c3094f..8c03d72df 100644 --- a/sdk/src/rpc/parse/channel.ts +++ b/sdk/src/rpc/parse/channel.ts @@ -1,22 +1,22 @@ import { z } from 'zod'; -import { Address, Hex } from 'viem'; import { RPCMethod, - ChannelOperationResponseParams, - CreateChannelResponseParams, ResizeChannelResponseParams, CloseChannelResponseParams, GetChannelsResponseParams, ChannelUpdateResponseParams, - RPCChannelStatus, - ChannelUpdate, + RPCChannelUpdate, + ChannelsUpdateResponseParams, + RPCChannelUpdateWithWallet, + CreateChannelResponseParams, + RPCChannelOperation, } from '../types'; -import { hexSchema, addressSchema, statusEnum, ParamsParser } from './common'; +import { hexSchema, addressSchema, statusEnum, ParamsParser, bigIntSchema, dateSchema } from './common'; const RPCAllocationSchema = z.object({ destination: addressSchema, token: addressSchema, - amount: z.string(), + amount: bigIntSchema, }); const ChannelOperationObject = z.object({ @@ -24,121 +24,120 @@ const ChannelOperationObject = z.object({ state: z.object({ intent: z.number(), version: z.number(), - state_data: z.string(), + state_data: hexSchema, allocations: z.array(RPCAllocationSchema), }), server_signature: hexSchema, }); const ChannelOperationObjectSchema = ChannelOperationObject.transform( - (raw) => - ({ - channelId: raw.channel_id as Hex, - state: { - intent: raw.state.intent, - version: raw.state.version, - stateData: raw.state.state_data as Hex, - allocations: raw.state.allocations.map((a) => ({ - destination: a.destination as Address, - token: a.token as Address, - amount: BigInt(a.amount), - })), - }, - serverSignature: raw.server_signature, - }) as ChannelOperationResponseParams, + (raw): RPCChannelOperation => ({ + channelId: raw.channel_id, + state: { + intent: raw.state.intent, + version: raw.state.version, + stateData: raw.state.state_data, + allocations: raw.state.allocations.map((a) => ({ + destination: a.destination, + token: a.token, + amount: a.amount, + })), + }, + serverSignature: raw.server_signature, + }), ); -const ChannelOperationParamsSchema = z - .array(ChannelOperationObjectSchema) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); - const CreateChannelParamsSchema = z - .array( - z - .object({ - ...ChannelOperationObject.shape, - channel: z.object({ - participants: z.array(addressSchema), - adjudicator: addressSchema, - challenge: z.number(), - nonce: z.number(), - }), - }) - .transform( - (params) => - ({ - ...ChannelOperationObjectSchema.parse(params), - channel: { - participants: params.channel.participants, - adjudicator: params.channel.adjudicator, - challenge: params.channel.challenge, - nonce: params.channel.nonce, - }, - }) as CreateChannelResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + ...ChannelOperationObject.shape, + channel: z.object({ + participants: z.array(addressSchema), + adjudicator: addressSchema, + challenge: z.number(), + nonce: z.number(), + }), + }) + .transform( + (raw): CreateChannelResponseParams => ({ + ...ChannelOperationObjectSchema.parse(raw), + channel: { + participants: raw.channel.participants, + adjudicator: raw.channel.adjudicator, + challenge: raw.channel.challenge, + nonce: raw.channel.nonce, + }, + }), + ); -const ResizeChannelParamsSchema = ChannelOperationParamsSchema.transform( - (params) => params as ResizeChannelResponseParams, -); +const ResizeChannelParamsSchema = ChannelOperationObjectSchema + // Validate received type with linter + .transform((raw): ResizeChannelResponseParams => raw); -const CloseChannelParamsSchema = ChannelOperationParamsSchema.transform( - (params) => params as CloseChannelResponseParams, +const CloseChannelParamsSchema = ChannelOperationObjectSchema + // Validate received type with linter + .transform((raw): CloseChannelResponseParams => raw); + +const ChannelUpdateObject = z.object({ + channel_id: hexSchema, + participant: addressSchema, + status: statusEnum, + token: addressSchema, + amount: bigIntSchema, + chain_id: z.number(), + adjudicator: addressSchema, + challenge: z.number(), + nonce: z.number(), + version: z.number(), + created_at: dateSchema, + updated_at: dateSchema, +}); + +const ChannelUpdateObjectSchema = ChannelUpdateObject.transform( + (raw): RPCChannelUpdate => ({ + channelId: raw.channel_id, + participant: raw.participant, + status: raw.status, + token: raw.token, + amount: raw.amount, + chainId: raw.chain_id, + adjudicator: raw.adjudicator, + challenge: raw.challenge, + nonce: raw.nonce, + version: raw.version, + createdAt: raw.created_at, + updatedAt: raw.updated_at, + }), ); -const ChannelUpdateObjectSchema = z +const ChannelUpdateWithWalletObjectSchema = z .object({ - channel_id: hexSchema, - participant: addressSchema, - status: statusEnum, - token: addressSchema, - wallet: z.union([addressSchema, z.literal('')]), - amount: z.union([z.string(), z.number()]).transform((a) => BigInt(a)), - chain_id: z.number(), - adjudicator: addressSchema, - challenge: z.number(), - nonce: z.union([z.string(), z.number()]).transform((n) => BigInt(n)), - version: z.number(), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - updated_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), + ...ChannelUpdateObject.shape, + wallet: addressSchema, }) .transform( - (c) => - ({ - channelId: c.channel_id as Hex, - participant: c.participant as Address, - status: c.status as RPCChannelStatus, - token: c.token as Address, - wallet: c.wallet as Address, - amount: c.amount, - chainId: c.chain_id, - adjudicator: c.adjudicator as Address, - challenge: c.challenge, - nonce: c.nonce, - version: c.version, - createdAt: c.created_at, - updatedAt: c.updated_at, - }) as ChannelUpdateResponseParams, + (raw): RPCChannelUpdateWithWallet => ({ + ...ChannelUpdateObjectSchema.parse(raw), + wallet: raw.wallet, + }), ); const GetChannelsParamsSchema = z - .array(z.array(ChannelUpdateObjectSchema)) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetChannelsResponseParams); + .object({ + channels: z.array(ChannelUpdateWithWalletObjectSchema), + }) + // Validate received type with linter + .transform((raw): GetChannelsResponseParams => raw); -const ChannelUpdateParamsSchema = z - .array(ChannelUpdateObjectSchema) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); +const ChannelUpdateParamsSchema = ChannelUpdateObjectSchema + // Validate received type with linter + .transform((raw): ChannelUpdateResponseParams => raw); const ChannelsUpdateParamsSchema = z - .array(z.array(ChannelUpdateObjectSchema)) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + channels: z.array(ChannelUpdateObjectSchema), + }) + // Validate received type with linter + .transform((raw): ChannelsUpdateResponseParams => raw); export const channelParamsParsers: Record> = { [RPCMethod.CreateChannel]: (params) => CreateChannelParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/common.ts b/sdk/src/rpc/parse/common.ts index 774bc88eb..9c26812d7 100644 --- a/sdk/src/rpc/parse/common.ts +++ b/sdk/src/rpc/parse/common.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { RPCChannelStatus, RPCMethod } from '../types'; +import { Address, Hex } from 'viem'; // --- Shared Interfaces & Classes --- @@ -16,15 +17,32 @@ export class ParserParamsMissingError extends Error { // --- Shared Zod Schemas --- -export const hexSchema = z.string().refine((val) => /^0x[0-9a-fA-F]*$/.test(val), { - message: 'Must be a 0x-prefixed hex string', -}); +export const hexSchema = z + .string() + .refine((val) => /^0x[0-9a-fA-F]*$/.test(val), { + message: 'Must be a 0x-prefixed hex string', + }) + .transform((v: string) => v as Hex); -export const addressSchema = z.string().refine((val) => /^0x[0-9a-fA-F]{40}$/.test(val), { - message: 'Must be a 0x-prefixed hex string of 40 hex chars (EVM address)', -}); +export const addressSchema = z + .string() + .refine((val) => /^0x[0-9a-fA-F]{40}$/.test(val), { + message: 'Must be a 0x-prefixed hex string of 40 hex chars (EVM address)', + }) + .transform((v: string) => v as Address); -export const statusEnum = z.enum(Object.values(RPCChannelStatus) as [string, ...string[]]); +export const bigIntSchema = z.string().transform((a) => BigInt(a)); + +export const dateSchema = z.union([z.string(), z.date()]).transform((v) => new Date(v)); + +export const decimalSchema = z + .union([z.string(), z.number()]) + .transform((v) => v.toString()) + .refine((val) => /^[+-]?((\d+(\.\d*)?)|(\.\d+))$/.test(val), { + message: 'Must be a valid decimal string', + }); + +export const statusEnum = z.nativeEnum(RPCChannelStatus); // --- Shared Parser Functions --- diff --git a/sdk/src/rpc/parse/ledger.ts b/sdk/src/rpc/parse/ledger.ts index d41f47eda..5e7cc2382 100644 --- a/sdk/src/rpc/parse/ledger.ts +++ b/sdk/src/rpc/parse/ledger.ts @@ -6,60 +6,67 @@ import { GetLedgerEntriesResponseParams, BalanceUpdateResponseParams, GetLedgerTransactionsResponseParams, - TxType, - Transaction, + RPCTxType, + RPCTransaction, TransferNotificationResponseParams, TransferResponseParams, + RPCBalance, + RPCLedgerEntry, } from '../types'; -import { addressSchema, ParamsParser } from './common'; +import { addressSchema, dateSchema, decimalSchema, ParamsParser } from './common'; + +const BalanceObjectSchema = z + .object({ + asset: z.string(), + amount: decimalSchema, + }) + .transform((b): RPCBalance => b); const GetLedgerBalancesParamsSchema = z - .array( - z.array( - z.object({ - asset: z.string(), - amount: z.union([z.string(), z.number()]).transform((a) => a.toString()), - }), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetLedgerBalancesResponseParams[]); + .object({ + ledger_balances: z.array(BalanceObjectSchema), + }) + .transform( + (raw): GetLedgerBalancesResponseParams => ({ + ledgerBalances: raw.ledger_balances, + }), + ); + +const LedgerEntryObjectSchema = z + .object({ + id: z.number(), + account_id: z.string(), + account_type: z.number(), + asset: z.string(), + participant: addressSchema, + credit: decimalSchema, + debit: decimalSchema, + created_at: dateSchema, + }) + .transform( + (e): RPCLedgerEntry => ({ + id: e.id, + accountId: e.account_id, + accountType: e.account_type, + asset: e.asset, + participant: e.participant, + credit: e.credit, + debit: e.debit, + createdAt: e.created_at, + }), + ); const GetLedgerEntriesParamsSchema = z - .array( - z.array( - z - .object({ - id: z.number(), - account_id: z.string(), - account_type: z.number(), - asset: z.string(), - participant: addressSchema, - credit: z.union([z.string(), z.number()]).transform((v) => v.toString()), - debit: z.union([z.string(), z.number()]).transform((v) => v.toString()), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - }) - .transform( - (e) => - ({ - id: e.id, - accountId: e.account_id, - accountType: e.account_type, - asset: e.asset, - participant: e.participant as Address, - credit: e.credit, - debit: e.debit, - createdAt: e.created_at, - }) as GetLedgerEntriesResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetLedgerEntriesResponseParams[]); + .object({ + ledger_entries: z.array(LedgerEntryObjectSchema), + }) + .transform( + (raw): GetLedgerEntriesResponseParams => ({ + ledgerEntries: raw.ledger_entries, + }), + ); -export const txTypeEnum = z.nativeEnum(TxType); +export const txTypeEnum = z.nativeEnum(RPCTxType); export const TransactionSchema = z .object({ @@ -71,15 +78,15 @@ export const TransactionSchema = z to_account_tag: z.string().optional(), asset: z.string(), amount: z.string(), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), + created_at: dateSchema, }) .transform( - (raw): Transaction => ({ + (raw): RPCTransaction => ({ id: raw.id, txType: raw.tx_type, - fromAccount: raw.from_account as Address, + fromAccount: raw.from_account, fromAccountTag: raw.from_account_tag, - toAccount: raw.to_account as Address, + toAccount: raw.to_account, toAccountTag: raw.to_account_tag, asset: raw.asset, amount: raw.amount, @@ -88,30 +95,44 @@ export const TransactionSchema = z ); const GetLedgerTransactionsParamsSchema = z - .array(z.array(TransactionSchema)) - .refine((arr) => arr.length === 1) - .transform((arr): GetLedgerTransactionsResponseParams => arr[0]); + .object({ + ledger_transactions: z.array(TransactionSchema), + }) + .transform( + (raw): GetLedgerTransactionsResponseParams => ({ + ledgerTransactions: raw.ledger_transactions, + }), + ); const BalanceUpdateParamsSchema = z - .array( - z.array( - z - .object({ asset: z.string(), amount: z.union([z.string(), z.number()]).transform((a) => a.toString()) }) - .transform((b) => ({ asset: b.asset, amount: b.amount }) as BalanceUpdateResponseParams), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + balance_updates: z.array(BalanceObjectSchema), + }) + .transform( + (raw): BalanceUpdateResponseParams => ({ + balanceUpdates: raw.balance_updates, + }), + ); const TransferParamsSchema = z - .array(z.array(TransactionSchema)) - .refine((arr) => arr.length === 1) - .transform((arr): TransferResponseParams => arr[0]); + .object({ + transactions: z.array(TransactionSchema), + }) + .transform( + (raw): TransferResponseParams => ({ + transactions: raw.transactions, + }), + ); const TransferNotificationParamsSchema = z - .array(z.array(TransactionSchema)) - .refine((arr) => arr.length === 1) - .transform((arr): TransferNotificationResponseParams => arr[0]); + .object({ + transactions: z.array(TransactionSchema), + }) + .transform( + (raw): TransferNotificationResponseParams => ({ + transactions: raw.transactions, + }), + ); export const ledgerParamsParsers: Record> = { [RPCMethod.GetLedgerBalances]: (params) => GetLedgerBalancesParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/misc.ts b/sdk/src/rpc/parse/misc.ts index 6b4e96f8f..a3a44fbfa 100644 --- a/sdk/src/rpc/parse/misc.ts +++ b/sdk/src/rpc/parse/misc.ts @@ -1,97 +1,92 @@ import { z } from 'zod'; -import { Address } from 'viem'; import { RPCMethod, GetConfigResponseParams, ErrorResponseParams, GetRPCHistoryResponseParams, - UserTagParams, + RPCNetworkInfo, + RPCHistoryEntry, + GetUserTagResponseParams, } from '../types'; -import { hexSchema, addressSchema, ParamsParser, ParserParamsMissingError } from './common'; +import { hexSchema, addressSchema, ParamsParser } from './common'; -const NetworkInfoSchema = z.object({ - name: z.string(), - chain_id: z.number(), - custody_address: addressSchema, - adjudicator_address: addressSchema, -}); +const NetworkInfoObjectSchema = z + .object({ + name: z.string(), + chain_id: z.number(), + custody_address: addressSchema, + adjudicator_address: addressSchema, + }) + .transform( + (raw): RPCNetworkInfo => ({ + name: raw.name, + chainId: raw.chain_id, + custodyAddress: raw.custody_address, + adjudicatorAddress: raw.adjudicator_address, + }), + ); const GetConfigParamsSchema = z - .array( - z - .object({ broker_address: addressSchema, networks: z.array(NetworkInfoSchema) }) - .strict() - .transform( - (raw) => - ({ - brokerAddress: raw.broker_address as Address, - networks: raw.networks.map((n) => ({ - name: n.name, - chainId: n.chain_id, - custodyAddress: n.custody_address as Address, - adjudicatorAddress: n.adjudicator_address as Address, - })), - }) as GetConfigResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ broker_address: addressSchema, networks: z.array(NetworkInfoObjectSchema) }) + .strict() + .transform( + (raw): GetConfigResponseParams => ({ + brokerAddress: raw.broker_address, + networks: raw.networks, + }), + ); const ErrorParamsSchema = z - .array(z.string().transform((raw) => ({ error: raw }) as ErrorResponseParams)) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ error: z.string() }) + // Validate received type with linter + .transform((raw): ErrorResponseParams => raw); + +const RPCEntryObjectSchema = z + .object({ + id: z.number(), + sender: addressSchema, + req_id: z.number(), + method: z.string(), + params: z.string(), + timestamp: z.number(), + req_sig: z.array(hexSchema), + res_sig: z.array(hexSchema), + response: z.string(), + }) + .transform( + (raw): RPCHistoryEntry => ({ + id: raw.id, + sender: raw.sender, + reqId: raw.req_id, + method: raw.method, + params: raw.params, + timestamp: raw.timestamp, + reqSig: raw.req_sig, + resSig: raw.res_sig, + response: raw.response, + }), + ); const GetRPCHistoryParamsSchema = z - .array( - z.array( - z - .object({ - id: z.number(), - sender: addressSchema, - req_id: z.number(), - method: z.string(), - params: z.string(), - timestamp: z.number(), - req_sig: z.array(hexSchema), - res_sig: z.array(hexSchema), - response: z.string(), - }) - .transform( - (h) => - ({ - id: h.id, - sender: h.sender as Address, - reqId: h.req_id, - method: h.method, - params: h.params, - timestamp: h.timestamp, - reqSig: h.req_sig as any, - resSig: h.res_sig as any, - response: h.response, - }) as GetRPCHistoryResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetRPCHistoryResponseParams[]); + .object({ + rpc_entries: z.array(RPCEntryObjectSchema), + }) + .transform( + (raw): GetRPCHistoryResponseParams => ({ + rpcEntries: raw.rpc_entries, + }), + ); const GetUserTagParamsSchema = z - .array( - z - .object({ - tag: z.string(), - }) - .strict() - .transform((raw) => ({ tag: raw.tag }) as UserTagParams), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + tag: z.string(), + }) + .strict() + // Validate received type with linter + .transform((raw): GetUserTagResponseParams => raw); const parseMessageParams: ParamsParser = (params) => { - if (!Array.isArray(params) || params.length === 0) throw new ParserParamsMissingError(RPCMethod.Message); - return params[0]; + return params; }; export const miscParamsParsers: Record> = { diff --git a/sdk/src/rpc/parse/parse.ts b/sdk/src/rpc/parse/parse.ts index e88e388a9..3a4e17da2 100644 --- a/sdk/src/rpc/parse/parse.ts +++ b/sdk/src/rpc/parse/parse.ts @@ -56,45 +56,95 @@ const _parseSpecificRPCResponse = ( return result as SpecificRPCResponse; }; -/** - * The main RPC response parsing utility. - * This object provides a collection of type-safe parsers for each specific RPC method. - * It offers the best developer experience, returning a fully typed object - * without the need for manual type guards. - * - * @example - * const result = rpcResponseParser.authChallenge(rawResponse); - * // `result` is now fully typed as AuthChallengeResponse - * console.log(result.params.challengeMessage); - */ -export const rpcResponseParser = { - authChallenge: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthChallenge), - authVerify: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthVerify), - authRequest: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthRequest), - error: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Error), - getConfig: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetConfig), - getLedgerBalances: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerBalances), - getLedgerEntries: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerEntries), - getLedgerTransactions: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerTransactions), - getUserTag: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetUserTag), - createAppSession: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CreateAppSession), - submitAppState: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.SubmitAppState), - closeAppSession: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseAppSession), - getAppDefinition: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppDefinition), - getAppSessions: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppSessions), - createChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CreateChannel), - resizeChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ResizeChannel), - closeChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseChannel), - getChannels: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetChannels), - getRPCHistory: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetRPCHistory), - getAssets: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAssets), - assets: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Assets), - message: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Message), - balanceUpdate: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.BalanceUpdate), - channelsUpdate: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelsUpdate), - channelUpdate: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelUpdate), - ping: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Ping), - pong: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Pong), - transfer: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Transfer), - transferNotification: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.TransferNotification), -}; +/** Parses `auth_challenge` response */ +export const parseAuthChallengeResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthChallenge); + +/** Parses `auth_verify` response */ +export const parseAuthVerifyResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthVerify); + +/** Parses `auth_request` response */ +export const parseAuthRequestResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthRequest); + +/** Parses `error` response */ +export const parseErrorResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Error); + +/** Parses `get_config` response */ +export const parseGetConfigResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetConfig); + +/** Parses `get_ledger_balances` response */ +export const parseGetLedgerBalancesResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerBalances); + +/** Parses `get_ledger_entries` response */ +export const parseGetLedgerEntriesResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerEntries); + +/** Parses `get_ledger_transactions` response */ +export const parseGetLedgerTransactionsResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerTransactions); + +/** Parses `get_user_tag` response */ +export const parseGetUserTagResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetUserTag); + +/** Parses `create_app_session` response */ +export const parseCreateAppSessionResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.CreateAppSession); + +/** Parses `submit_app_state` response */ +export const parseSubmitAppStateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.SubmitAppState); + +/** Parses `close_app_session` response */ +export const parseCloseAppSessionResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseAppSession); + +/** Parses `get_app_definition` response */ +export const parseGetAppDefinitionResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetAppDefinition); + +/** Parses `get_app_sessions` response */ +export const parseGetAppSessionsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppSessions); + +/** Parses `create_channel` response */ +export const parseCreateChannelResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CreateChannel); + +/** Parses `resize_channel` response */ +export const parseResizeChannelResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ResizeChannel); + +/** Parses `close_channel` response */ +export const parseCloseChannelResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseChannel); + +/** Parses `get_channels` response */ +export const parseGetChannelsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetChannels); + +/** Parses `get_rpc_history` response */ +export const parseGetRPCHistoryResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetRPCHistory); + +/** Parses `get_assets` response */ +export const parseGetAssetsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAssets); + +/** Parses `assets` response */ +export const parseAssetsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Assets); + +/** Parses `message` response */ +export const parseMessageResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Message); + +/** Parses `bu` response */ +export const parseBalanceUpdateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.BalanceUpdate); + +/** Parses `channels` response */ +export const parseChannelsUpdateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelsUpdate); + +/** Parses `cu` response */ +export const parseChannelUpdateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelUpdate); + +/** Parses `ping` response */ +export const parsePingResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Ping); + +/** Parses `pong` response */ +export const parsePongResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Pong); + +/** Parses `transfer` response */ +export const parseTransferResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Transfer); + +/** Parses `tr` response */ +export const parseTransferNotificationResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.TransferNotification); diff --git a/sdk/src/rpc/types/common.ts b/sdk/src/rpc/types/common.ts new file mode 100644 index 000000000..c567c7d7a --- /dev/null +++ b/sdk/src/rpc/types/common.ts @@ -0,0 +1,295 @@ +import { Hex, Address } from 'viem'; + +/** Represents the status of a channel. */ +export enum RPCChannelStatus { + Open = 'open', + Closed = 'closed', + Challenged = 'challenged', +} + +/** + * Represents the request parameters for the 'get_transactions' RPC method. + */ +export enum RPCTxType { + Transfer = 'transfer', + Deposit = 'deposit', + Withdrawal = 'withdrawal', + AppDeposit = 'app_deposit', + AppWithdrawal = 'app_withdrawal', +} + +/** + * Defines the structure of an application definition used when creating an application. + */ +export interface RPCAppDefinition { + /** The protocol identifier or name for the application logic (e.g., "NitroRPC/0.2"). */ + protocol: string; + /** An array of participant addresses (Ethereum addresses) involved in the application. Must have at least 2 participants. */ + participants: Hex[]; + /** An array representing the relative weights or stakes of participants, often used for dispute resolution or allocation calculations. Order corresponds to the participants array. */ + weights: number[]; + /** The number of participants required to reach consensus or approve state updates. */ + quorum: number; + /** A parameter related to the challenge period or mechanism within the application's protocol, in seconds. */ + challenge: number; + /** A unique number used once, often for preventing replay attacks or ensuring uniqueness of the application instance. Must be non-zero. */ + nonce?: number; +} + +/** + * Represents a channel update message sent over the RPC protocol. + */ +export interface RPCChannelUpdate { + /** The unique identifier for the channel. */ + channelId: Hex; + /** The Ethereum address of the participant. */ + participant: Address; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + /** The token contract address. */ + token: Address; + /** The total amount in the channel. */ + amount: BigInt; + /** The chain ID where the channel exists. */ + chainId: number; + /** The adjudicator contract address. */ + adjudicator: Address; + /** The challenge period in seconds. */ + challenge: number; + /** The nonce value for the channel. */ + nonce: number; + /** The version number of the channel. */ + version: number; + /** The timestamp when the channel was created. */ + createdAt: Date; + /** The timestamp when the channel was last updated. */ + updatedAt: Date; +} + +export interface RPCChannelUpdateWithWallet extends RPCChannelUpdate { + /** The Ethereum address of the wallet associated with the channel. */ + wallet: Address; +} + +/** + * Represents the network information for the 'get_config' RPC method. + */ +export interface RPCNetworkInfo { + /** The name of the network (e.g., "Ethereum", "Polygon"). */ + name: string; + /** The chain ID of the network. */ + chainId: number; + /** The custody contract address for the network. */ + custodyAddress: Address; + /** The adjudicator contract address for the network. */ + adjudicatorAddress: Address; +} + +/** + * Represents the balance information from clearnode. + */ +export interface RPCBalance { + /** The asset symbol (e.g., "ETH", "USDC"). */ + asset: string; + /** The balance amount. */ + amount: string; +} + +/** + * Represents a single entry in the ledger. + */ +export interface RPCLedgerEntry { + /** Unique identifier for the ledger entry. */ + id: number; + /** The account identifier associated with the entry. */ + accountId: string; + /** The type of account (e.g., "wallet", "channel"). */ + accountType: number; + /** The asset symbol for the entry. */ + asset: string; + /** The Ethereum address of the participant. */ + participant: Address; + /** The credit amount. */ + credit: string; + /** The debit amount. */ + debit: string; + /** The timestamp when the entry was created. */ + createdAt: Date; +} + +/** + * Represents the app session information. + */ +export interface RPCAppSession { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + /** List of participant Ethereum addresses. */ + participants: Address[]; + /** The protocol identifier for the application. */ + protocol: string; + /** The challenge period in seconds. */ + challenge: number; + /** The signature weights for each participant. */ + weights: number[]; + /** The minimum number of signatures required for state updates. */ + quorum: number; + /** The version number of the session. */ + version: number; + /** The nonce value for the session. */ + nonce: number; + /** The timestamp when the session was created. */ + createdAt: Date; + /** The timestamp when the session was last updated. */ + updatedAt: Date; + /** Optional session data as a JSON string that stores application-specific state or metadata. */ + sessionData?: string; +} + +/** + * Represents RPC entry in the history. + */ +export interface RPCHistoryEntry { + /** Unique identifier for the RPC entry. */ + id: number; + /** The Ethereum address of the sender. */ + sender: Address; + /** The request ID for the RPC call. */ + reqId: number; + /** The RPC method name. */ + method: string; + /** The JSON string of the request parameters. */ + params: string; + /** The timestamp of the RPC call. */ + timestamp: number; + /** Array of request signatures. */ + reqSig: Hex[]; + /** Array of response signatures. */ + resSig: Hex[]; + /** The JSON string of the response. */ + response: string; +} + +/** + * Represents Asset information received from the clearnode. + */ +export interface RPCAsset { + /** The token contract address. */ + token: Address; + /** The chain ID where the asset exists. */ + chainId: number; + /** The asset symbol (e.g., "ETH", "USDC"). */ + symbol: string; + /** The number of decimal places for the asset. */ + decimals: number; +} + +/** + * Represents the parameters for the transfer transaction. + */ +export interface RPCTransaction { + /** Unique identifier for the transfer. */ + id: number; + /** The type of transaction. */ + txType: RPCTxType; + /** The source address from which assets were transferred. */ + fromAccount: Address; + /** The user tag for the source account (optional). */ + fromAccountTag?: string; + /** The destination address to which assets were transferred. */ + toAccount: Address; + /** The user tag for the destination account (optional). */ + toAccountTag?: string; + /** The asset symbol that was transferred. */ + asset: string; + /** The amount that was transferred. */ + amount: string; + /** The timestamp when the transfer was created. */ + createdAt: Date; +} + +/** + * Represents a generic RPC message structure that includes common fields. + * This interface is extended by specific RPC request and response types. + */ +export interface RPCAllowance { + /** The symbol of the asset (e.g., "USDC", "USDT"). */ + asset: string; + /** The amount of the asset that is allowed to be spent. */ + amount: string; +} + +// TODO: create single domain allocation type + +/** + * Represents the allocation of assets within an application session. + * This structure is used to define the initial allocation of assets among participants. + * It includes the participant's address, the asset (usdc, usdt, etc) being allocated, and the amount. + */ +export interface RPCAppSessionAllocation { + /** The symbol of the asset (e.g., "USDC", "USDT", "ETH"). */ + asset: string; + /** The amount of the asset. Must be a positive number. */ + amount: string; + /** The Ethereum address of the participant receiving the allocation. */ + participant: Address; +} + +/** + * Represents the allocation of assets for an RPC transfer. + * This structure is used to define the asset and amount being transferred to a specific destination address. + */ +export interface RPCChannelAllocation { + /** The destination address for the allocation. */ + destination: Address; + /** The token contract address for the asset being allocated. */ + token: Address; + /** The amount of the asset being allocated. */ + amount: bigint; +} + +/** + * Represents the allocation of assets for an RPC transfer. + * This structure is used to define the asset and amount being transferred. + */ +export interface RPCTransferAllocation { + /** The symbol of the asset (e.g., "USDC", "USDT", "ETH"). */ + asset: string; + /** The amount of the asset being transferred. */ + amount: string; +} + +/** + * Represents the state of a channel operation. + * This structure is used to define the intent, version, state data, and allocations for a channel operation. + */ +export interface RPCChannelOperationState { + /** The intent type for the state update. */ + intent: number; + /** The version number of the channel. */ + version: number; + /** The encoded state data for the channel. */ + stateData: Hex; + /** The list of allocations for the channel. */ + allocations: RPCChannelAllocation[]; +} + +export interface RPCChannelOperation { + /** The unique identifier for the channel. */ + channelId: Hex; + /** The channel state object. */ + state: RPCChannelOperationState; + /** The server's signature for the state update. */ + serverSignature: Hex; +} + +/** + * Represents the fixed part of a channel, containing essential metadata. + */ +export interface RPCChannel { + participants: Address[]; + adjudicator: Address; + challenge: number; + nonce: number; +} diff --git a/sdk/src/rpc/types/filters.ts b/sdk/src/rpc/types/filters.ts new file mode 100644 index 000000000..acdcb38e6 --- /dev/null +++ b/sdk/src/rpc/types/filters.ts @@ -0,0 +1,17 @@ +import { RPCTxType } from './common'; + +export interface PaginationFilters { + /** Pagination offset. */ + offset?: number; + /** Number of transactions to return. */ + limit?: number; + /** Sort order by created_at. */ + sort?: 'asc' | 'desc'; +} + +export interface GetLedgerTransactionsFilters extends PaginationFilters { + /** Filter by transaction type. */ + tx_type?: RPCTxType; + /** Filter by asset symbol. */ + asset?: string; +} diff --git a/sdk/src/rpc/types/index.ts b/sdk/src/rpc/types/index.ts index 543f0daec..76028ee2c 100644 --- a/sdk/src/rpc/types/index.ts +++ b/sdk/src/rpc/types/index.ts @@ -1,7 +1,10 @@ import { Address, Hex } from 'viem'; +import { RPCAllowance } from './common'; export * from './request'; export * from './response'; +export * from './filters'; +export * from './common'; /** Type alias for Request ID (uint64) */ export type RequestID = number; @@ -12,76 +15,19 @@ export type Timestamp = number; /** Type alias for Account ID (channelId or appId) */ export type AccountID = Hex; -/** Represents the data payload within a request message: [requestId, method, params, timestamp?]. */ -export type RequestData = [RequestID, RPCMethod, object[], Timestamp?]; - -/** Represents the data payload within a successful response message: [requestId, method, result, timestamp?]. */ -export type ResponseData = [RequestID, RPCMethod, object[], Timestamp?]; - -/** Represents the status of a channel. */ -export enum RPCChannelStatus { - Open = 'open', - Closed = 'closed', - Challenged = 'challenged', -} +/** Represents the data payload within a request or response message: [requestId, method, params, timestamp?]. */ +export type RPCData = [RequestID, RPCMethod, object, Timestamp?]; /** * Represents a generic RPC message structure that includes common fields. * This interface is extended by specific RPC request and response types. */ export interface GenericRPCMessage { - requestId: RequestID; + requestId?: RequestID; timestamp?: Timestamp; signatures?: Hex[]; } -/** Base type for asset allocations with common asset and amount fields. */ -export type AssetAllocation = { - /** The symbol of the asset (e.g., "USDC", "USDT", "ETH"). */ - asset: string; - /** The amount of the asset. Must be a positive number. */ - amount: string; -}; - -/** - * Represents a generic RPC message structure that includes common fields. - * This interface is extended by specific RPC request and response types. - */ -export type Allowance = { - /** The symbol of the asset (e.g., "USDC", "USDT"). */ - asset: string; - /** The amount of the asset that is allowed to be spent. */ - amount: string; -}; - -/** Represents the allocation of assets within an application session. - * This structure is used to define the initial allocation of assets among participants. - * It includes the participant's address, the asset (usdc, usdt, etc) being allocated, and the amount. - */ -export type AppSessionAllocation = AssetAllocation & { - /** The Ethereum address of the participant receiving the allocation. */ - participant: Address; -}; - -/** Represents the allocation of assets for a transfer. - * This structure is used to define the asset and amount being transferred. - */ -export type TransferAllocation = AssetAllocation; - -/** - * Represents the structure of an error object within an error response payload. - */ -export interface NitroliteRPCErrorDetail { - /** The error message describing what went wrong. */ - error: string; -} - -/** Represents the data payload for an error response: [requestId, "error", [errorDetail], timestamp?]. */ -export type ErrorResponseData = [RequestID, 'error', [NitroliteRPCErrorDetail], Timestamp?]; - -/** Union type for the 'res' payload, covering both success and error responses. */ -export type ResponsePayload = ResponseData | ErrorResponseData; - /** * Defines the wire format for Nitrolite RPC messages, based on NitroRPC principles * as adapted for the Clearnet protocol. @@ -89,9 +35,9 @@ export type ResponsePayload = ResponseData | ErrorResponseData; */ export interface NitroliteRPCMessage { /** Contains the request payload if this is a request message. */ - req?: RequestData; + req?: RPCData; /** Contains the response or error payload if this is a response message. */ - res?: ResponsePayload; + res?: RPCData; /** Optional cryptographic signature(s) for message authentication. */ sig?: Hex[]; } @@ -108,79 +54,6 @@ export interface ApplicationRPCMessage extends NitroliteRPCMessage { sid: Hex; } -/** - * Represents the result of parsing an incoming Nitrolite RPC response message. - * Contains extracted fields and validation status. - */ -export interface ParsedResponse { - /** Indicates if the message was successfully parsed and passed basic structural validation. */ - isValid: boolean; - /** If isValid is false, contains a description of the parsing or validation error. */ - error?: string; - /** Indicates if the parsed response represents an error (method === "error"). Undefined if isValid is false. */ - isError?: boolean; - /** The Request ID from the response payload. Undefined if structure is invalid. */ - requestId?: RequestID; - /** The method name from the response payload. Undefined if structure is invalid. */ - method?: RPCMethod; - /** The extracted data payload (result array for success, error detail object for error). Undefined if structure is invalid or error payload malformed. */ - data?: object[] | NitroliteRPCErrorDetail; - /** The Application Session ID from the message envelope. Undefined if structure is invalid. */ - sid?: Hex; - /** The Timestamp from the response payload. Undefined if structure is invalid. */ - timestamp?: Timestamp; -} - -/** - * Defines the structure of an application definition used when creating an application. - */ -export interface AppDefinition { - /** The protocol identifier or name for the application logic (e.g., "NitroRPC/0.2"). */ - protocol: string; - /** An array of participant addresses (Ethereum addresses) involved in the application. Must have at least 2 participants. */ - participants: Hex[]; - /** An array representing the relative weights or stakes of participants, often used for dispute resolution or allocation calculations. Order corresponds to the participants array. */ - weights: number[]; - /** The number of participants required to reach consensus or approve state updates. */ - quorum: number; - /** A parameter related to the challenge period or mechanism within the application's protocol, in seconds. */ - challenge: number; - /** A unique number used once, often for preventing replay attacks or ensuring uniqueness of the application instance. Must be non-zero. */ - nonce?: number; -} - -/** - * Represents a channel update message sent over the RPC protocol. - */ -export interface ChannelUpdate { - /** The unique identifier for the channel. */ - channelId: Hex; - /** The Ethereum address of the participant. */ - participant: Address; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; - /** The token contract address. */ - token: Address; - /** The wallet address associated with the channel. */ - wallet: Address; - /** The total amount in the channel. */ - amount: BigInt; - /** The chain ID where the channel exists. */ - chainId: number; - /** The adjudicator contract address. */ - adjudicator: Address; - /** The challenge period in seconds. */ - challenge: number; - /** The nonce value for the channel. */ - nonce: BigInt; - /** The version number of the channel. */ - version: number; - /** The timestamp when the channel was created. */ - createdAt: Date; - /** The timestamp when the channel was last updated. */ - updatedAt: Date; -} - /** * Defines standard error codes for the Nitrolite RPC protocol. * Includes standard JSON-RPC codes and custom codes for specific errors. @@ -217,7 +90,7 @@ export enum NitroliteErrorCode { * @param payload - The RequestData or ResponsePayload object (array) to sign. * @returns A Promise that resolves to the cryptographic signature as a Hex string. */ -export type MessageSigner = (payload: RequestData | ResponsePayload) => Promise; +export type MessageSigner = (payload: RPCData) => Promise; /** * Defines the function signature for signing challenge state data. @@ -236,7 +109,7 @@ export type ChallengeStateSigner = (stateHash: Hex) => Promise; * @returns A Promise that resolves to true if the signature is valid for the given payload and address, false otherwise. */ export type SingleMessageVerifier = ( - payload: RequestData | ResponsePayload, + payload: RPCData, signature: Hex, address: Address, ) => Promise; @@ -250,7 +123,7 @@ export type SingleMessageVerifier = ( * @returns A Promise that resolves to true if all required signatures from the expected signers are present and valid, false otherwise. */ export type MultiMessageVerifier = ( - payload: RequestData | ResponsePayload, + payload: RPCData, signatures: Hex[], expectedSigners: Address[], ) => Promise; @@ -265,11 +138,7 @@ export interface PartialEIP712AuthMessage { application: Address; participant: Address; expire: string; - // TODO: use Allowance type after replacing symbol with asset - allowances: { - asset: string; - amount: string; - }[]; + allowances: RPCAllowance[]; } /** diff --git a/sdk/src/rpc/types/request.ts b/sdk/src/rpc/types/request.ts index 6d1cfb0a2..33fe5d091 100644 --- a/sdk/src/rpc/types/request.ts +++ b/sdk/src/rpc/types/request.ts @@ -1,44 +1,40 @@ import { Address, Hex } from 'viem'; -import { RPCMethod, GenericRPCMessage, AppDefinition, RPCChannelStatus, TransferAllocation } from '.'; - -/** - * Represents the request parameters for the 'auth_challenge' RPC method. - */ -export interface AuthChallengeRequestParams { - /** The challenge message to be signed by the client for authentication. */ - challenge_message: string; -} -export type AuthChallengeRPCRequestParams = AuthChallengeRequestParams; // for backward compatibility +import { + RPCMethod, + GenericRPCMessage, + RPCAppDefinition, + RPCChannelStatus, + RPCTransferAllocation, + RPCAppSessionAllocation, + RPCAllowance, + GetLedgerTransactionsFilters, +} from '.'; /** * Represents the request structure for the 'auth_challenge' RPC method. */ export interface AuthChallengeRequest extends GenericRPCMessage { method: RPCMethod.AuthChallenge; - params: AuthChallengeRequestParams[]; + params: { + /** The challenge message to be signed by the client for authentication. */ + challenge_message: string; + }; } -/** - * Represents the request parameters for the 'auth_verify' RPC method. - * Either JWT or challenge must be provided. JWT takes precedence over challenge. - */ -export type AuthVerifyRequestParams = - | { - /** JSON Web Token for authentication. */ - jwt: string; - } - | { - /** The challenge token received from auth_challenge response. Used to verify the client's signature and prevent replay attacks. */ - challenge: string; - }; -export type AuthVerifyRPCRequestParams = AuthVerifyRequestParams; // for backward compatibility - /** * Represents the request structure for the 'auth_verify' RPC method. */ export interface AuthVerifyRequest extends GenericRPCMessage { method: RPCMethod.AuthVerify; - params: AuthVerifyRequestParams[]; + params: + | { + /** JSON Web Token for authentication. */ + jwt: string; + } + | { + /** The challenge token received from auth_challenge response. Used to verify the client's signature and prevent replay attacks. */ + challenge: string; + }; } /** @@ -46,254 +42,139 @@ export interface AuthVerifyRequest extends GenericRPCMessage { */ export interface GetConfigRequest extends GenericRPCMessage { method: RPCMethod.GetConfig; - params: []; + params: {}; } -/** - * Represents the request parameters for the 'get_ledger_balances' RPC method. - */ -export interface GetLedgerBalancesRequestParams { - /** The participant address to filter balances. */ - participant: Address; - /** Optional account ID to filter balances. If provided, overrides the participant address. */ - account_id?: string; -} -export type GetLedgerBalancesRPCRequestParams = GetLedgerBalancesRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_ledger_balances' RPC method. */ export interface GetLedgerBalancesRequest extends GenericRPCMessage { method: RPCMethod.GetLedgerBalances; - params: [GetLedgerBalancesRequestParams]; + params: { + /** The participant address to filter balances. */ + participant: Address; + /** Optional account ID to filter balances. If provided, overrides the participant address. */ + account_id?: string; + }; } -/** - * Represents the request parameters for the 'get_ledger_entries' RPC method. - */ -export interface GetLedgerEntriesRequestParams { - /** The account ID to filter ledger entries. */ - account_id?: string; - /** The asset symbol to filter ledger entries. */ - asset?: string; - /** Optional wallet address to filter ledger entries. If provided, overrides the authenticated wallet. */ - wallet?: Address; -} -export type GetLedgerEntriesRPCRequestParams = GetLedgerEntriesRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_ledger_entries' RPC method. */ export interface GetLedgerEntriesRequest extends GenericRPCMessage { method: RPCMethod.GetLedgerEntries; - params: [GetLedgerEntriesRequestParams]; -} - -/** - * Represents the request parameters for the 'get_transactions' RPC method. - */ -export enum TxType { - Transfer = 'transfer', - Deposit = 'deposit', - Withdrawal = 'withdrawal', - AppDeposit = 'app_deposit', - AppWithdrawal = 'app_withdrawal', + params: { + /** The account ID to filter ledger entries. */ + account_id?: string; + /** The asset symbol to filter ledger entries. */ + asset?: string; + /** Optional wallet address to filter ledger entries. If provided, overrides the authenticated wallet. */ + wallet?: Address; + }; } -/** - * Represents the request parameters for the 'get_transactions' RPC method. - */ -export interface GetLedgerTransactionsFilters { - /** The asset symbol to filter transactions. */ - asset?: string; - /** The transaction type to filter transactions. */ - tx_type?: TxType; - /** Pagination offset. */ - offset?: number; - /** Number of transactions to return. */ - limit?: number; - /** Sort order by created_at. */ - sort?: 'asc' | 'desc'; -} - -export interface GetLedgerTransactionsRequestParams extends GetLedgerTransactionsFilters { - /** The account ID to filter transactions. */ - account_id: string; -} -export type GetLedgerTransactionsRPCRequestParams = GetLedgerTransactionsRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_transactions' RPC method. */ export interface GetLedgerTransactionsRequest extends GenericRPCMessage { method: RPCMethod.GetLedgerTransactions; - params: GetLedgerTransactionsRequestParams; + params: GetLedgerTransactionsFilters & { + account_id: string; + }; } -/** - * Represents the request parameters for the 'get_user_tag' RPC method. - */ -export interface GetUserTagRequestParams { - // This method takes no parameters - empty object -} -export type GetUserTagRPCRequestParams = GetUserTagRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_user_tag' RPC method. */ export interface GetUserTagRequest extends GenericRPCMessage { method: RPCMethod.GetUserTag; - params: []; + params: {}; } -/** Represents the allocation of assets within an application session. - * This structure is used to define allocation of assets among participants. - * It includes the participant's address, the asset (usdc, usdt, etc) being allocated, and the amount. - */ -export type AppSessionAllocation = { - /** The Ethereum address of the participant receiving the allocation. */ - participant: Address; - /** The symbol of the asset being allocated (e.g., "USDC", "USDT"). */ - asset: string; - /** The amount of the asset being allocated. Must be a positive number. */ - amount: string; -}; - -/** - * Represents the request parameters for the 'create_app_session' RPC method. - */ -export interface CreateAppSessionRequestParams { - /** The detailed definition of the application being created, including protocol, participants, weights, and quorum. */ - definition: AppDefinition; - /** The initial allocation distribution among participants. Each participant must have sufficient balance for their allocation. */ - allocations: AppSessionAllocation[]; - /** Optional session data as a JSON string that can store application-specific state or metadata. */ - session_data?: string; -} -export type CreateAppSessionRPCRequestParams = CreateAppSessionRequestParams; // for backward compatibility - /** * Represents the request structure for the 'create_app_session' RPC method. */ export interface CreateAppSessionRequest extends GenericRPCMessage { method: RPCMethod.CreateAppSession; - params: [CreateAppSessionRequestParams]; -} - -/** - * Represents the request parameters for the 'submit_app_state' RPC method. - */ -export interface SubmitAppStateRequestParams { - /** The unique identifier of the application session to update. */ - app_session_id: Hex; - /** The new allocation distribution among participants. Must include all participants and maintain total balance. */ - allocations: AppSessionAllocation[]; - /** Optional session data as a JSON string that can store application-specific state or metadata. */ - session_data?: string; + params: { + /** The detailed definition of the application being created, including protocol, participants, weights, and quorum. */ + definition: RPCAppDefinition; + /** The initial allocation distribution among participants. Each participant must have sufficient balance for their allocation. */ + allocations: RPCAppSessionAllocation[]; + /** Optional session data as a JSON string that can store application-specific state or metadata. */ + session_data?: string; + }; } -export type SubmitAppStateRPCRequestParams = SubmitAppStateRequestParams; // for backward compatibility /** * Represents the request structure for the 'submit_app_state' RPC method. */ export interface SubmitAppStateRequest extends GenericRPCMessage { method: RPCMethod.SubmitAppState; - params: [SubmitAppStateRequestParams]; + params: { + /** The unique identifier of the application session to update. */ + app_session_id: Hex; + /** The new allocation distribution among participants. Must include all participants and maintain total balance. */ + allocations: RPCAppSessionAllocation[]; + /** Optional session data as a JSON string that can store application-specific state or metadata. */ + session_data?: string; + }; } -/** - * Represents the request parameters for the 'close_app_session' RPC method. - */ -export interface CloseAppSessionRequestParams { - /** The unique identifier of the application session to close. */ - app_session_id: Hex; - /** The final allocation distribution among participants upon closing. Must include all participants and maintain total balance. */ - allocations: AppSessionAllocation[]; - /** Optional session data as a JSON string that can store application-specific state or metadata. */ - session_data?: string; -} -export type CloseAppSessionRPCRequestParams = CloseAppSessionRequestParams; // for backward compatibility - /** * Represents the request structure for the 'close_app_session' RPC method. */ export interface CloseAppSessionRequest extends GenericRPCMessage { method: RPCMethod.CloseAppSession; - params: [CloseAppSessionRequestParams]; -} - -/** - * Represents the request parameters for the 'get_app_definition' RPC method. - */ -export interface GetAppDefinitionRequestParams { - /** The unique identifier of the application session to retrieve. */ - app_session_id: Hex; + params: { + /** The unique identifier of the application session to close. */ + app_session_id: Hex; + /** The final allocation distribution among participants upon closing. Must include all participants and maintain total balance. */ + allocations: RPCAppSessionAllocation[]; + /** Optional session data as a JSON string that can store application-specific state or metadata. */ + session_data?: string; + }; } -export type GetAppDefinitionRPCRequestParams = GetAppDefinitionRequestParams; // for backward compatibility /** * Represents the request structure for the 'get_app_definition' RPC method. */ export interface GetAppDefinitionRequest extends GenericRPCMessage { method: RPCMethod.GetAppDefinition; - params: [GetAppDefinitionRequestParams]; + params: { + /** The unique identifier of the application session to retrieve the definition for. */ + app_session_id: Hex; + }; } -/** - * Represents the request parameters for the 'get_app_sessions' RPC method. - */ -export interface GetAppSessionsRequestParams { - /** The participant address to filter application sessions. */ - participant: Address; - /** The status to filter application sessions (e.g., "open", "closed"). */ - status: RPCChannelStatus; -} -export type GetAppSessionsRPCRequestParams = GetAppSessionsRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_app_sessions' RPC method. */ export interface GetAppSessionsRequest extends GenericRPCMessage { method: RPCMethod.GetAppSessions; - params: [GetAppSessionsRequestParams]; + params: { + /** Optional, The participant address to filter application sessions. */ + participant?: Address; + /** Optional, The status to filter application sessions (e.g., "open", "closed"). */ + status?: RPCChannelStatus; + }; } -/** - * Represents the request parameters for the 'create_channel' RPC method. - */ -export interface CreateChannelRequestParams { - /** The blockchain network ID where the channel should be created. */ - chain_id: number; - /** The token contract address for the channel. */ - token: Address; - /** The initial amount to deposit in the channel (in raw token units). */ - amount: bigint; - /** Optional session key address for the channel. */ - session_key?: Hex; -} -export type CreateChannelRPCRequestParams = CreateChannelRequestParams; // for backward compatibility - -/** - * Represents the request parameters for the 'resize_channel' RPC method. - */ -export type ResizeChannelRequestParams = { - /** The unique identifier of the channel to resize. */ - channel_id: Hex; - /** Amount to resize the channel by (can be positive or negative). Required if allocate_amount is not provided. */ - resize_amount?: bigint; - /** Amount to allocate from the unified balance to the channel. Required if resize_amount is not provided. */ - allocate_amount?: bigint; - /** The address where the resized funds will be sent. */ - funds_destination: Address; -}; -export type ResizeChannelRPCRequestParams = ResizeChannelRequestParams; // for backward compatibility - /** * Represents the request structure for the 'create_channel' RPC method. */ export interface CreateChannelRequest extends GenericRPCMessage { method: RPCMethod.CreateChannel; - params: [CreateChannelRequestParams]; + params: { + /** The blockchain network ID where the channel should be created. */ + chain_id: number; + /** The token contract address for the channel. */ + token: Address; + /** The initial amount to deposit in the channel (in raw token units). */ + amount: bigint; + /** Optional session key address for the channel. */ + session_key?: Hex; + }; } /** @@ -301,45 +182,42 @@ export interface CreateChannelRequest extends GenericRPCMessage { */ export interface ResizeChannelRequest extends GenericRPCMessage { method: RPCMethod.ResizeChannel; - params: [ResizeChannelRequestParams]; -} - -/** - * Represents the request parameters for the 'close_channel' RPC method. - */ -export interface CloseChannelRequestParams { - /** The unique identifier of the channel to close. */ - channel_id: Hex; - /** The address where the channel funds will be sent upon closing. */ - funds_destination: Address; + params: { + /** The unique identifier of the channel to resize. */ + channel_id: Hex; + /** Amount to resize the channel by (can be positive or negative). Required if allocate_amount is not provided. */ + resize_amount?: bigint; + /** Amount to allocate from the unified balance to the channel. Required if resize_amount is not provided. */ + allocate_amount?: bigint; + /** The address where the resized funds will be sent. */ + funds_destination: Address; + }; } -export type CloseChannelRPCRequestParams = CloseChannelRequestParams; // for backward compatibility /** * Represents the request structure for the 'close_channel' RPC method. */ export interface CloseChannelRequest extends GenericRPCMessage { method: RPCMethod.CloseChannel; - params: [CloseChannelRequestParams]; -} - -/** - * Represents the request parameters for the 'get_channels' RPC method. - */ -export interface GetChannelsRequestParams { - /** The participant address to filter channels. */ - participant: Address; - /** The status to filter channels (e.g., "open", "closed"). */ - status: RPCChannelStatus; + params: { + /** The unique identifier of the channel to close. */ + channel_id: Hex; + /** The address where the channel funds will be sent upon closing. */ + funds_destination: Address; + }; } -export type GetChannelsRPCRequestParams = GetChannelsRequestParams; // for backward compatibility /** * Represents the request structure for the 'get_channels' RPC method. */ export interface GetChannelsRequest extends GenericRPCMessage { method: RPCMethod.GetChannels; - params: [GetChannelsRequestParams]; + params: { + /** Optional, The participant address to filter channels. */ + participant?: Address; + /** Optional, The status to filter channels (e.g., "open", "closed"). */ + status?: RPCChannelStatus; + }; } /** @@ -347,63 +225,41 @@ export interface GetChannelsRequest extends GenericRPCMessage { */ export interface GetRPCHistoryRequest extends GenericRPCMessage { method: RPCMethod.GetRPCHistory; - params: []; + params: {}; } -/** - * Represents the request parameters for the 'get_assets' RPC method. - */ -export interface GetAssetsRequestParams { - /** Optional chain ID to filter assets by network. If not provided, returns assets from all networks. */ - chain_id?: number; -} -export type GetAssetsRPCRequestParams = GetAssetsRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_assets' RPC method. */ export interface GetAssetsRequest extends GenericRPCMessage { method: RPCMethod.GetAssets; - params: [GetAssetsRequestParams]; + params: { + /** Optional chain ID to filter assets by network. If not provided, returns assets from all networks. */ + chain_id?: number; + }; } -/** Represents a single allowance for an asset, used in application sessions. - * This structure defines the symbol of the asset and the amount that is allowed to be spent. - */ -export type Allowance = { - /** The symbol of the asset (e.g., "USDC", "USDT"). */ - asset: string; - /** The amount of the asset that is allowed to be spent. */ - amount: string; -}; - -/** - * Represents the request parameters for the 'auth_request' RPC method. - */ -export interface AuthRequestParams { - /** The Ethereum address of the wallet being authorized. */ - wallet: Address; - /** The session key address associated with the authentication attempt. */ - participant: Address; - /** The name of the application being authorized. */ - app_name: string; - /** The allowances for the connection. */ - allowances: Allowance[]; - /** The expiration timestamp for the authorization. */ - expire: string; - /** The scope of the authorization. */ - scope: string; - /** The application address being authorized. */ - application: Address; -} -export type AuthRequestRPCRequestParams = AuthRequestParams; // for backward compatibility - /** * Represents the request structure for the 'auth_request' RPC method. */ export interface AuthRequest extends GenericRPCMessage { method: RPCMethod.AuthRequest; - params: AuthRequestParams[]; + params: { + /** The Ethereum address of the wallet being authorized. */ + address: Address; + /** The session key address associated with the authentication attempt. */ + session_key: Address; + /** The name of the application being authorized. */ + app_name: string; + /** The allowances for the connection. */ + allowances: RPCAllowance[]; + /** The expiration timestamp for the authorization. */ + expire: string; + /** The scope of the authorization. */ + scope: string; + /** The application address being authorized. */ + application: Address; + }; } /** @@ -412,7 +268,7 @@ export interface AuthRequest extends GenericRPCMessage { export interface MessageRequest extends GenericRPCMessage { method: RPCMethod.Message; /** The message parameters are handled by the virtual application */ - params: any[]; + params: any; } /** @@ -420,8 +276,7 @@ export interface MessageRequest extends GenericRPCMessage { */ export interface PingRequest extends GenericRPCMessage { method: RPCMethod.Ping; - /** No parameters needed for ping */ - params: []; + params: {}; } /** @@ -429,20 +284,7 @@ export interface PingRequest extends GenericRPCMessage { */ export interface PongRequest extends GenericRPCMessage { method: RPCMethod.Pong; - /** No parameters needed for pong */ - params: []; -} - -/** - * Represents the request parameters for the 'transfer' RPC method. - */ -export interface TransferRequestParams { - /** The destination address to transfer assets to. Required if destination_user_tag is not provided. */ - destination?: Address; - /** The destination user tag to transfer assets to. Required if destination is not provided. */ - destination_user_tag?: string; - /** The assets and amounts to transfer. */ - allocations: TransferAllocation[]; + params: {}; } /** @@ -450,9 +292,129 @@ export interface TransferRequestParams { */ export interface TransferRequest extends GenericRPCMessage { method: RPCMethod.Transfer; - params: TransferRequestParams; + params: { + /** The destination address to transfer assets to. Required if destination_user_tag is not provided. */ + destination?: Address; + /** The destination user tag to transfer assets to. Required if destination is not provided. */ + destination_user_tag?: string; + /** The assets and amounts to transfer. */ + allocations: RPCTransferAllocation[]; + }; } -export type TransferRPCRequestParams = TransferRequestParams; // for backward compatibility + +/** Represents the request parameters for the 'auth_challenge' RPC method. */ +export type AuthChallengeRequestParams = AuthChallengeRequest['params']; + +/** + * Represents the request parameters for the 'auth_verify' RPC method. + * Either JWT or challenge must be provided. JWT takes precedence over challenge. + */ +export type AuthVerifyRequestParams = AuthVerifyRequest['params']; + +/** + * Represents the request parameters for the 'get_config' RPC method. + */ +export type GetConfigRequestParams = GetConfigRequest['params']; + +/** + * Represents the request parameters for the 'get_ledger_balances' RPC method. + */ +export type GetLedgerBalancesRequestParams = GetLedgerBalancesRequest['params']; + +/** + * Represents the request parameters for the 'get_ledger_entries' RPC method. + */ +export type GetLedgerEntriesRequestParams = GetLedgerEntriesRequest['params']; + +/** + * Represents the request parameters for the 'get_ledger_transactions' RPC method. + */ +export type GetLedgerTransactionsRequestParams = GetLedgerTransactionsRequest['params']; + +/** + * Represents the request parameters for the 'get_user_tag' RPC method. + */ +export type GetUserTagRequestParams = GetUserTagRequest['params']; + +/** + * Represents the request parameters for the 'create_app_session' RPC method. + */ +export type CreateAppSessionRequestParams = CreateAppSessionRequest['params']; + +/** + * Represents the request parameters for the 'submit_app_state' RPC method. + */ +export type SubmitAppStateRequestParams = SubmitAppStateRequest['params']; + +/** + * Represents the request parameters for the 'close_app_session' RPC method. + */ +export type CloseAppSessionRequestParams = CloseAppSessionRequest['params']; + +/** + * Represents the request parameters for the 'get_app_definition' RPC method. + */ +export type GetAppDefinitionRequestParams = GetAppDefinitionRequest['params']; + +/** + * Represents the request parameters for the 'get_app_sessions' RPC method. + */ +export type GetAppSessionsRequestParams = GetAppSessionsRequest['params']; + +/** + * Represents the request parameters for the 'create_channel' RPC method. + */ +export type CreateChannelRequestParams = CreateChannelRequest['params']; + +/** + * Represents the request parameters for the 'resize_channel' RPC method. + */ +export type ResizeChannelRequestParams = ResizeChannelRequest['params']; + +/** + * Represents the request parameters for the 'close_channel' RPC method. + */ +export type CloseChannelRequestParams = CloseChannelRequest['params']; + +/** + * Represents the request parameters for the 'get_channels' RPC method. + */ +export type GetChannelsRequestParams = GetChannelsRequest['params']; + +/** + * Represents the request parameters for the 'get_rpc_history' RPC method. + */ +export type GetRPCHistoryParams = GetRPCHistoryRequest['params']; + +/** + * Represents the request parameters for the 'get_assets' RPC method. + */ +export type GetAssetsRequestParams = GetAssetsRequest['params']; + +/** + * Represents the request parameters for the 'auth_request' RPC method. + */ +export type AuthRequestParams = AuthRequest['params']; + +/** + * Represents the request parameters for the 'message' RPC method. + */ +export type MessageRequestParams = MessageRequest['params']; + +/** + * Represents the request parameters for the 'ping' RPC method. + */ +export type PingRequestParams = PingRequest['params']; + +/** + * Represents the request parameters for the 'pong' RPC method. + */ +export type PongRequestParams = PongRequest['params']; + +/** + * Represents the request parameters for the 'transfer' RPC method. + */ +export type TransferRequestParams = TransferRequest['params']; /** * Union type for all possible RPC request types. @@ -490,11 +452,11 @@ export type RPCRequestParamsByMethod = { [RPCMethod.AuthChallenge]: AuthChallengeRequestParams; [RPCMethod.AuthVerify]: AuthVerifyRequestParams; [RPCMethod.AuthRequest]: AuthRequestParams; - [RPCMethod.GetConfig]: []; + [RPCMethod.GetConfig]: GetConfigRequestParams; [RPCMethod.GetLedgerBalances]: GetLedgerBalancesRequestParams; [RPCMethod.GetLedgerEntries]: GetLedgerEntriesRequestParams; [RPCMethod.GetLedgerTransactions]: GetLedgerTransactionsRequestParams; - [RPCMethod.GetUserTag]: []; + [RPCMethod.GetUserTag]: GetUserTagRequestParams; [RPCMethod.CreateAppSession]: CreateAppSessionRequestParams; [RPCMethod.SubmitAppState]: SubmitAppStateRequestParams; [RPCMethod.CloseAppSession]: CloseAppSessionRequestParams; @@ -504,10 +466,10 @@ export type RPCRequestParamsByMethod = { [RPCMethod.ResizeChannel]: ResizeChannelRequestParams; [RPCMethod.CloseChannel]: CloseChannelRequestParams; [RPCMethod.GetChannels]: GetChannelsRequestParams; - [RPCMethod.GetRPCHistory]: []; + [RPCMethod.GetRPCHistory]: GetRPCHistoryParams; [RPCMethod.GetAssets]: GetAssetsRequestParams; - [RPCMethod.Ping]: []; - [RPCMethod.Pong]: []; - [RPCMethod.Message]: any[]; + [RPCMethod.Ping]: PingRequestParams; + [RPCMethod.Pong]: PongRequestParams; + [RPCMethod.Message]: any; [RPCMethod.Transfer]: TransferRequestParams; }; diff --git a/sdk/src/rpc/types/response.ts b/sdk/src/rpc/types/response.ts index 620f1c1d3..35b55aced 100644 --- a/sdk/src/rpc/types/response.ts +++ b/sdk/src/rpc/types/response.ts @@ -2,634 +2,481 @@ import { Address, Hex } from 'viem'; import { RPCMethod, GenericRPCMessage, - AppDefinition, + RPCAppDefinition, RPCChannelStatus, - AuthVerifyRequestParams, - TransferAllocation, - ChannelUpdate, - TxType, + RPCChannelUpdate, + RPCNetworkInfo, + RPCBalance, + RPCLedgerEntry, + RPCAppSession, + RPCHistoryEntry, + RPCAsset, + RPCTransaction, + RPCChannelUpdateWithWallet, + RPCChannelOperation, + RPCChannel, } from '.'; -/** - * Represents the parameters for the 'auth_challenge' RPC method. - */ -export interface AuthChallengeResponseParams { - /** The challenge message to be signed by the client for authentication. */ - challengeMessage: string; -} -export type AuthChallengeRPCResponseParams = AuthChallengeResponseParams; // for backward compatibility - /** * Represents the response structure for the 'auth_challenge' RPC method. */ export interface AuthChallengeResponse extends GenericRPCMessage { method: RPCMethod.AuthChallenge; - params: AuthChallengeResponseParams; + params: { + /** The challenge message to be signed by the client for authentication. */ + challengeMessage: string; + }; } /** - * Represents the parameters for the 'auth_verify' RPC method. - */ -export type AuthVerifyResponseParams = { - address: Address; - sessionKey: Address; - success: boolean; -} & { - /** Available only if challenge auth method was used in {@link AuthVerifyRequestParams} during the call to {@link RPCMethod.AuthRequest} */ - jwtToken: string; -}; -export type AuthVerifyRPCResponseParams = AuthVerifyResponseParams; // for backward compatibility - -/** - * Represents the parameters for the 'error' RPC method. + * Represents the response structure for an error response. */ -export interface ErrorResponseParams { - /** The error message describing what went wrong. */ - error: string; +export interface ErrorResponse extends GenericRPCMessage { + method: RPCMethod.Error; + params: { + /** The error message describing what went wrong. */ + error: string; + }; } -export type ErrorRPCResponseParams = ErrorResponseParams; // for backward compatibility /** - * Represents the network information for the 'get_config' RPC method. + * Represents the response structure for the 'get_config' RPC method. */ -export interface NetworkInfo { - /** The name of the network (e.g., "Ethereum", "Polygon"). */ - name: string; - /** The chain ID of the network. */ - chainId: number; - /** The custody contract address for the network. */ - custodyAddress: Address; - /** The adjudicator contract address for the network. */ - adjudicatorAddress: Address; +export interface GetConfigResponse extends GenericRPCMessage { + method: RPCMethod.GetConfig; + params: { + /** The Ethereum address of the broker. */ + brokerAddress: Address; + /** List of supported networks and their configurations. */ + networks: RPCNetworkInfo[]; + }; } /** - * Represents the parameters for the 'get_config' RPC method. + * Represents the response structure for the 'get_ledger_balances' RPC method. */ -export interface GetConfigResponseParams { - /** The Ethereum address of the broker. */ - brokerAddress: Address; - /** List of supported networks and their configurations. */ - networks: NetworkInfo[]; +export interface GetLedgerBalancesResponse extends GenericRPCMessage { + method: RPCMethod.GetLedgerBalances; + params: { + /** List of balances for each asset in the ledger. */ + ledgerBalances: RPCBalance[]; + }; } -export type GetConfigRPCResponseParams = GetConfigResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_ledger_balances' RPC method. + * Represents the response structure for the 'get_ledger_entries' RPC method. */ -export interface GetLedgerBalancesResponseParams { - /** The asset symbol (e.g., "ETH", "USDC"). */ - asset: string; - /** The balance amount. */ - amount: string; +export interface GetLedgerEntriesResponse extends GenericRPCMessage { + method: RPCMethod.GetLedgerEntries; + params: { + /** List of ledger entries containing transaction details. */ + ledgerEntries: RPCLedgerEntry[]; + }; } -export type GetLedgerBalancesRPCResponseParams = GetLedgerBalancesResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_ledger_entries' RPC method. + * Represents the response structure for the 'get_transactions' RPC method. */ -export interface GetLedgerEntriesResponseParams { - /** Unique identifier for the ledger entry. */ - id: number; - /** The account identifier associated with the entry. */ - accountId: string; - /** The type of account (e.g., "wallet", "channel"). */ - accountType: number; - /** The asset symbol for the entry. */ - asset: string; - /** The Ethereum address of the participant. */ - participant: Address; - /** The credit amount. */ - credit: string; - /** The debit amount. */ - debit: string; - /** The timestamp when the entry was created. */ - createdAt: Date; +export interface GetLedgerTransactionsResponse extends GenericRPCMessage { + method: RPCMethod.GetLedgerTransactions; + params: { + /** List of transactions in the ledger. */ + ledgerTransactions: RPCTransaction[]; + }; } -export type GetLedgerEntriesRPCResponseParams = GetLedgerEntriesResponseParams; // for backward compatibility - -export type GetLedgerTransactionsResponseParams = Transaction[]; /** - * Represents the parameters for the 'get_user_tag' RPC method. + * Represents the response structure for the 'get_user_tag' RPC method. */ -export interface UserTagParams { - /** The user's unique tag identifier. */ - tag: string; +export interface GetUserTagResponse extends GenericRPCMessage { + method: RPCMethod.GetUserTag; + params: { + /** The user's unique tag identifier. */ + tag: string; + }; } -export type GetUserTagRPCResponseParams = UserTagParams; // for backward compatibility /** - * Represents the parameters for the 'create_app_session' RPC method. + * Represents the response structure for the 'create_app_session' RPC method. */ -export interface CreateAppSessionResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The version number of the session. */ - version: number; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; +export interface CreateAppSessionResponse extends GenericRPCMessage { + method: RPCMethod.CreateAppSession; + params: { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The version number of the session. */ + version: number; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + }; } -export type CreateAppSessionRPCResponseParams = CreateAppSessionResponseParams; // for backward compatibility /** - * Represents the parameters for the 'submit_app_state' RPC method. + * Represents the response structure for the 'submit_app_state' RPC method. */ -export interface SubmitAppStateResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The version number of the session. */ - version: number; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; +export interface SubmitAppStateResponse extends GenericRPCMessage { + method: RPCMethod.SubmitAppState; + params: { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The version number of the session. */ + version: number; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + }; } -export type SubmitAppStateRPCResponseParams = SubmitAppStateResponseParams; // for backward compatibility /** - * Represents the parameters for the 'close_app_session' RPC method. + * Represents the response structure for the 'close_app_session' RPC method. */ -export interface CloseAppSessionResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The version number of the session. */ - version: number; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; +export interface CloseAppSessionResponse extends GenericRPCMessage { + method: RPCMethod.CloseAppSession; + params: { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The version number of the session. */ + version: number; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + }; } -export type CloseAppSessionRPCResponseParams = CloseAppSessionResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_app_definition' RPC method. + * Represents the response structure for the 'get_app_definition' RPC method. */ -export interface GetAppDefinitionResponseParams extends AppDefinition { - /** The protocol identifier for the application (e.g., "payment", "swap"). */ - protocol: string; - /** List of Ethereum addresses of participants in the application session. */ - participants: Address[]; - /** Array of signature weights for each participant, used for quorum calculations. */ - weights: number[]; - /** The minimum number of signatures required for state updates. */ - quorum: number; - /** The challenge period in seconds for state updates. */ - challenge: number; - /** A unique nonce value for the application session to prevent replay attacks. */ - nonce: number; +export interface GetAppDefinitionResponse extends GenericRPCMessage { + method: RPCMethod.GetAppDefinition; + params: RPCAppDefinition & { + /** A unique nonce value for the application session to prevent replay attacks. */ + nonce: number; + }; } -export type GetAppDefinitionRPCResponseParams = GetAppDefinitionResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_app_sessions' RPC method. + * Represents the response structure for the 'get_app_sessions' RPC method. */ -export interface GetAppSessionsResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; - /** List of participant Ethereum addresses. */ - participants: Address[]; - /** The protocol identifier for the application. */ - protocol: string; - /** The challenge period in seconds. */ - challenge: number; - /** The signature weights for each participant. */ - weights: number[]; - /** The minimum number of signatures required for state updates. */ - quorum: number; - /** The version number of the session. */ - version: number; - /** The nonce value for the session. */ - nonce: number; - /** The timestamp when the session was created. */ - createdAt: Date; - /** The timestamp when the session was last updated. */ - updatedAt: Date; - /** Optional session data as a JSON string that stores application-specific state or metadata. */ - sessionData?: string; -} -export type GetAppSessionsRPCResponseParams = GetAppSessionsResponseParams; // for backward compatibility - -export type ServerSignature = Hex; - -export interface RPCAllocation { - /** The destination address for the allocation. */ - destination: Address; - /** The token contract address. */ - token: Address; - /** The amount to allocate. */ - amount: bigint; +export interface GetAppSessionsResponse extends GenericRPCMessage { + method: RPCMethod.GetAppSessions; + params: { + appSessions: RPCAppSession[]; + }; } /** - * Represents the state object within channel operation responses. + * Represents the response structure for the 'create_channel' RPC method. */ -export interface ChannelOperationState { - /** The intent type for the state update. */ - intent: number; - /** The version number of the channel. */ - version: number; - /** The encoded state data for the channel. */ - stateData: Hex; - /** The list of allocations for the channel. */ - allocations: RPCAllocation[]; +export interface CreateChannelResponse extends GenericRPCMessage { + method: RPCMethod.CreateChannel; + params: RPCChannelOperation & { + channel: RPCChannel; + }; } /** - * Represents the fixed part of a channel, containing essential metadata. + * Represents the response structure for the 'resize_channel' RPC method. */ -export interface RPCChannel { - participants: Address[]; - adjudicator: Address; - challenge: number; - nonce: number; +export interface ResizeChannelResponse extends GenericRPCMessage { + method: RPCMethod.ResizeChannel; + params: RPCChannelOperation; } /** - * Represents the unified parameters for channel operations (create, resize, close). + * Represents the response structure for the 'close_channel' RPC method. */ -export interface ChannelOperationResponseParams { - /** The unique identifier for the channel. */ - channelId: Hex; - /** The channel state object. */ - state: ChannelOperationState; - /** The server's signature for the state update. */ - serverSignature: ServerSignature; +export interface CloseChannelResponse extends GenericRPCMessage { + method: RPCMethod.CloseChannel; + params: RPCChannelOperation; } /** - * Represents the parameters for the 'create_channel' RPC method. + * Represents the response structure for the 'get_channels' RPC method. */ -export interface CreateChannelResponseParams extends ChannelOperationResponseParams { - channel: RPCChannel; +export interface GetChannelsResponse extends GenericRPCMessage { + method: RPCMethod.GetChannels; + params: { + /** List of channel updates containing information about each channel. */ + channels: RPCChannelUpdateWithWallet[]; + }; } -export type CreateChannelRPCResponseParams = CreateChannelResponseParams; // for backward compatibility /** - * Represents the parameters for the 'resize_channel' RPC method. + * Represents the response structure for the 'get_rpc_history' RPC method. */ -export interface ResizeChannelResponseParams extends ChannelOperationResponseParams {} -export type ResizeChannelRPCResponseParams = ResizeChannelResponseParams; // for backward compatibility +export interface GetRPCHistoryResponse extends GenericRPCMessage { + method: RPCMethod.GetRPCHistory; + params: { + /** List of RPC entries containing historical RPC calls and their responses. */ + rpcEntries: RPCHistoryEntry[]; + }; +} /** - * Represents the parameters for the 'close_channel' RPC method. + * Represents the response structure for the 'get_assets' RPC method. */ -export interface CloseChannelResponseParams extends ChannelOperationResponseParams {} -export type CloseChannelRPCResponseParams = CloseChannelResponseParams; // for backward compatibility +export interface GetAssetsResponse extends GenericRPCMessage { + method: RPCMethod.GetAssets; + params: { + /** List of assets available in the clearnode. */ + assets: RPCAsset[]; + }; +} /** - * Represents the parameters for the 'get_channels' RPC method. + * Represents the response structure for the 'assets' RPC method. */ -export type GetChannelsResponseParams = ChannelUpdate[]; -export type GetChannelsRPCResponseParams = GetChannelsResponseParams; // for backward compatibility +export interface AssetsResponse extends GenericRPCMessage { + method: RPCMethod.Assets; + params: { + /** List of assets available in the clearnode. */ + assets: RPCAsset[]; + }; +} /** - * Represents the parameters for the 'get_rpc_history' RPC method. + * Represents the response structure for the 'auth_verify' RPC method. */ -export interface GetRPCHistoryResponseParams { - /** Unique identifier for the RPC entry. */ - id: number; - /** The Ethereum address of the sender. */ - sender: Address; - /** The request ID for the RPC call. */ - reqId: number; - /** The RPC method name. */ - method: string; - /** The JSON string of the request parameters. */ - params: string; - /** The timestamp of the RPC call. */ - timestamp: number; - /** Array of request signatures. */ - reqSig: Hex[]; - /** Array of response signatures. */ - resSig: Hex[]; - /** The JSON string of the response. */ - response: string; +export interface AuthVerifyResponse extends GenericRPCMessage { + method: RPCMethod.AuthVerify; + params: { + address: Address; + sessionKey: Address; + success: boolean; + /** Available only if challenge auth method was used in {@link AuthVerifyRequest} during the call to {@link RPCMethod.AuthRequest} */ + jwtToken?: string; + }; } -export type GetRPCHistoryRPCResponseParams = GetRPCHistoryResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_assets' RPC method. + * Represents the response structure for the 'auth_request' RPC method. */ -export interface GetAssetsResponseParams { - /** The token contract address. */ - token: Address; - /** The chain ID where the asset exists. */ - chainId: number; - /** The asset symbol (e.g., "ETH", "USDC"). */ - symbol: string; - /** The number of decimal places for the asset. */ - decimals: number; +export interface AuthRequestResponse extends GenericRPCMessage { + method: RPCMethod.AuthRequest; + params: { + /** The challenge message to be signed by the client for authentication. */ + challengeMessage: string; + }; } -export type GetAssetsRPCResponseParams = GetAssetsResponseParams; // for backward compatibility /** - * Represents the response structure for an error response. + * Represents the response structure for the 'message' RPC method. */ -export interface ErrorResponse extends GenericRPCMessage { - method: RPCMethod.Error; - params: ErrorResponseParams; +export interface MessageResponse extends GenericRPCMessage { + method: RPCMethod.Message; + params: {}; } /** - * Represents the response structure for the 'get_config' RPC method. + * Represents the response structure for the 'bu' RPC method. */ -export interface GetConfigResponse extends GenericRPCMessage { - method: RPCMethod.GetConfig; - params: GetConfigResponseParams; +export interface BalanceUpdateResponse extends GenericRPCMessage { + method: RPCMethod.BalanceUpdate; + params: { + /** List of balance updates. */ + balanceUpdates: RPCBalance[]; + }; } /** - * Represents the response structure for the 'get_ledger_balances' RPC method. + * Represents the response structure for the 'channels' RPC method. */ -export interface GetLedgerBalancesResponse extends GenericRPCMessage { - method: RPCMethod.GetLedgerBalances; - params: GetLedgerBalancesResponseParams[]; +export interface ChannelsUpdateResponse extends GenericRPCMessage { + method: RPCMethod.ChannelsUpdate; + params: { + /** List of channel updates. */ + channels: RPCChannelUpdate[]; + }; } /** - * Represents the response structure for the 'get_ledger_entries' RPC method. + * Represents the response structure for the 'cu' RPC method. */ -export interface GetLedgerEntriesResponse extends GenericRPCMessage { - method: RPCMethod.GetLedgerEntries; - params: GetLedgerEntriesResponseParams[]; +export interface ChannelUpdateResponse extends GenericRPCMessage { + method: RPCMethod.ChannelUpdate; + params: RPCChannelUpdate; } /** - * Represents the response structure for the 'get_transactions' RPC method. + * Represents the response structure for the 'ping' RPC method. */ -export interface GetLedgerTransactionsResponse extends GenericRPCMessage { - method: RPCMethod.GetLedgerTransactions; - params: GetLedgerTransactionsResponseParams; +export interface PingResponse extends GenericRPCMessage { + method: RPCMethod.Ping; + params: {}; } /** - * Represents the response structure for the 'get_user_tag' RPC method. + * Represents the response structure for the 'pong' RPC method. */ -export interface GetUserTagResponse extends GenericRPCMessage { - method: RPCMethod.GetUserTag; - params: UserTagParams; +export interface PongResponse extends GenericRPCMessage { + method: RPCMethod.Pong; + params: {}; } /** - * Represents the response structure for the 'create_app_session' RPC method. + * Represents the response structure for the 'transfer' RPC method. */ -export interface CreateAppSessionResponse extends GenericRPCMessage { - method: RPCMethod.CreateAppSession; - params: CreateAppSessionResponseParams; +export interface TransferResponse extends GenericRPCMessage { + method: RPCMethod.Transfer; + params: { + /** List of transactions representing transfers. */ + transactions: RPCTransaction[]; + }; } /** - * Represents the response structure for the 'submit_app_state' RPC method. + * Represents the response structure for the 'transfer_notification' RPC method. */ -export interface SubmitAppStateResponse extends GenericRPCMessage { - method: RPCMethod.SubmitAppState; - params: SubmitAppStateResponseParams; +export interface TransferNotificationResponse extends GenericRPCMessage { + method: RPCMethod.TransferNotification; + params: { + /** List of transactions representing transfers. */ + transactions: RPCTransaction[]; + }; } /** - * Represents the response structure for the 'close_app_session' RPC method. + * Represents the parameters for the 'auth_challenge' RPC method. */ -export interface CloseAppSessionResponse extends GenericRPCMessage { - method: RPCMethod.CloseAppSession; - params: CloseAppSessionResponseParams; -} +export type AuthChallengeResponseParams = AuthChallengeResponse['params']; /** - * Represents the response structure for the 'get_app_definition' RPC method. + * Represents the parameters for the 'auth_verify' RPC method. */ -export interface GetAppDefinitionResponse extends GenericRPCMessage { - method: RPCMethod.GetAppDefinition; - params: GetAppDefinitionResponseParams; -} +export type AuthVerifyResponseParams = AuthVerifyResponse['params']; /** - * Represents the response structure for the 'get_app_sessions' RPC method. + * Represents the parameters for the 'error' RPC method. */ -export interface GetAppSessionsResponse extends GenericRPCMessage { - method: RPCMethod.GetAppSessions; - params: GetAppSessionsResponseParams[]; -} +export type ErrorResponseParams = ErrorResponse['params']; /** - * Represents the response structure for the 'create_channel' RPC method. + * Represents the parameters for the 'get_config' RPC method. */ -export interface CreateChannelResponse extends GenericRPCMessage { - method: RPCMethod.CreateChannel; - params: CreateChannelResponseParams; -} +export type GetConfigResponseParams = GetConfigResponse['params']; /** - * Represents the response structure for the 'resize_channel' RPC method. + * Represents the parameters for the 'get_ledger_balances' RPC method. */ -export interface ResizeChannelResponse extends GenericRPCMessage { - method: RPCMethod.ResizeChannel; - params: ResizeChannelResponseParams; -} +export type GetLedgerBalancesResponseParams = GetLedgerBalancesResponse['params']; /** - * Represents the response structure for the 'close_channel' RPC method. + * Represents the parameters for the 'get_ledger_entries' RPC method. */ -export interface CloseChannelResponse extends GenericRPCMessage { - method: RPCMethod.CloseChannel; - params: CloseChannelResponseParams; -} +export type GetLedgerEntriesResponseParams = GetLedgerEntriesResponse['params']; /** - * Represents the response structure for the 'get_channels' RPC method. + * Represents the parameters for the 'get_ledger_transactions' RPC method. */ -export interface GetChannelsResponse extends GenericRPCMessage { - method: RPCMethod.GetChannels; - params: GetChannelsResponseParams; -} +export type GetLedgerTransactionsResponseParams = GetLedgerTransactionsResponse['params']; /** - * Represents the response structure for the 'get_rpc_history' RPC method. + * Represents the parameters for the 'get_user_tag' RPC method. */ -export interface GetRPCHistoryResponse extends GenericRPCMessage { - method: RPCMethod.GetRPCHistory; - params: GetRPCHistoryResponseParams[]; -} +export type GetUserTagResponseParams = GetUserTagResponse['params']; /** - * Represents the response structure for the 'get_assets' RPC method. + * Represents the parameters for the 'create_app_session' RPC method. */ -export interface GetAssetsResponse extends GenericRPCMessage { - method: RPCMethod.GetAssets; - params: GetAssetsResponseParams[]; -} - -export interface AssetsResponse extends GenericRPCMessage { - method: RPCMethod.Assets; - params: GetAssetsResponseParams[]; -} +export type CreateAppSessionResponseParams = CreateAppSessionResponse['params']; /** - * Represents the response structure for the 'auth_verify' RPC method. + * Represents the parameters for the 'submit_app_state' RPC method. */ -export interface AuthVerifyResponse extends GenericRPCMessage { - method: RPCMethod.AuthVerify; - params: AuthVerifyResponseParams; -} +export type SubmitAppStateResponseParams = SubmitAppStateResponse['params']; /** - * Represents the parameters for the 'auth_request' RPC method. + * Represents the parameters for the 'close_app_session' RPC method. */ -export interface AuthRequestResponseParams { - /** The challenge message to be signed by the client for authentication. */ - challengeMessage: string; -} -export type AuthRequestRPCResponseParams = AuthRequestResponseParams; // for backward compatibility +export type CloseAppSessionResponseParams = CloseAppSessionResponse['params']; /** - * Represents the response structure for the 'auth_request' RPC method. + * Represents the parameters for the 'get_app_definition' RPC method. */ -export interface AuthRequestResponse extends GenericRPCMessage { - method: RPCMethod.AuthRequest; - params: AuthRequestResponseParams; -} +export type GetAppDefinitionResponseParams = GetAppDefinitionResponse['params']; /** - * Represents the response parameters for the 'message' RPC method. + * Represents the parameters for the 'get_app_sessions' RPC method. */ -export interface MessageResponseParams { - // Message response parameters are handled by the application -} -export type MessageRPCResponseParams = MessageResponseParams; // for backward compatibility +export type GetAppSessionsResponseParams = GetAppSessionsResponse['params']; /** - * Represents the response structure for the 'message' RPC method. + * Represents the parameters for the 'create_channel' RPC method. */ -export interface MessageResponse extends GenericRPCMessage { - method: RPCMethod.Message; - params: MessageResponseParams; -} +export type CreateChannelResponseParams = CreateChannelResponse['params']; /** - * Represents the parameters for the 'bu' RPC method. + * Represents the parameters for the 'resize_channel' RPC method. */ -export interface BalanceUpdateResponseParams { - /** The asset symbol (e.g., "ETH", "USDC"). */ - asset: string; - /** The balance amount. */ - amount: string; -} -export type BalanceUpdateRPCResponseParams = BalanceUpdateResponseParams; // for backward compatibility +export type ResizeChannelResponseParams = ResizeChannelResponse['params']; /** - * Represents the response structure for the 'bu' RPC method. + * Represents the parameters for the 'close_channel' RPC method. */ -export interface BalanceUpdateResponse extends GenericRPCMessage { - method: RPCMethod.BalanceUpdate; - params: BalanceUpdateResponseParams[]; -} +export type CloseChannelResponseParams = CloseChannelResponse['params']; /** - * Represents the parameters for the 'channels' RPC method. + * Represents the parameters for the 'get_channels' RPC method. */ -export type ChannelsUpdateResponseParams = ChannelUpdate; +export type GetChannelsResponseParams = GetChannelsResponse['params']; /** - * Represents the response structure for the 'channels_update' RPC method. + * Represents the parameters for the 'get_rpc_history' RPC method. */ -export interface ChannelsUpdateResponse extends GenericRPCMessage { - method: RPCMethod.ChannelsUpdate; - params: ChannelsUpdateResponseParams; -} +export type GetRPCHistoryResponseParams = GetRPCHistoryResponse['params']; /** - * Represents the parameters for the 'cu' RPC method. + * Represents the parameters for the 'get_assets' RPC method. */ -export type ChannelUpdateResponseParams = ChannelUpdate; -export type ChannelUpdateRPCResponseParams = ChannelUpdateResponseParams; // for backward compatibility +export type GetAssetsResponseParams = GetAssetsResponse['params']; /** - * Represents the response structure for the 'cu' RPC method. + * Represents the parameters for the 'assets' RPC method. */ -export interface ChannelUpdateResponse extends GenericRPCMessage { - method: RPCMethod.ChannelUpdate; - params: ChannelUpdateResponseParams; -} +export type AssetsResponseParams = AssetsResponse['params']; /** - * Represents the parameters for the 'ping' RPC method. + * Represents the parameters for the 'auth_request' RPC method. */ -export interface PingResponseParams { - // No parameters needed for ping -} -export type PingRPCResponseParams = PingResponseParams; // for backward compatibility +export type AuthRequestResponseParams = AuthRequestResponse['params']; /** - * Represents the response structure for the 'ping' RPC method. + * Represents the parameters for the 'message' RPC method. */ -export interface PingResponse extends GenericRPCMessage { - method: RPCMethod.Ping; - params: PingResponseParams; -} +export type MessageResponseParams = MessageResponse['params']; /** - * Represents the parameters for the 'pong' RPC method. + * Represents the parameters for the 'bu' RPC method. */ -export interface PongResponseParams { - // No parameters needed for pong -} -export type PongRPCResponseParams = PongResponseParams; // for backward compatibility +export type BalanceUpdateResponseParams = BalanceUpdateResponse['params']; /** - * Represents the response structure for the 'pong' RPC method. + * Represents the parameters for the 'channels' RPC method. */ -export interface PongResponse extends GenericRPCMessage { - method: RPCMethod.Pong; - params: PongResponseParams; -} +export type ChannelsUpdateResponseParams = ChannelsUpdateResponse['params']; /** - * Represents the parameters for the transfer transaction. - */ -export interface Transaction { - /** Unique identifier for the transfer. */ - id: number; - /** The type of transaction. */ - txType: TxType; - /** The source address from which assets were transferred. */ - fromAccount: Address; - /** The user tag for the source account (optional). */ - fromAccountTag?: string; - /** The destination address to which assets were transferred. */ - toAccount: Address; - /** The user tag for the destination account (optional). */ - toAccountTag?: string; - /** The asset symbol that was transferred. */ - asset: string; - /** The amount that was transferred. */ - amount: string; - /** The timestamp when the transfer was created. */ - createdAt: Date; -} + * Represents the parameters for the 'cu' RPC method. + */ +export type ChannelUpdateResponseParams = ChannelUpdateResponse['params']; /** - * Represents the parameters for the 'transfer' RPC method. + * Represents the parameters for the 'ping' RPC method. */ -export type TransferResponseParams = Transaction[]; +export type PingResponseParams = PingResponse['params']; /** - * Represents the response structure for the 'transfer' RPC method. + * Represents the parameters for the 'pong' RPC method. */ -export interface TransferResponse extends GenericRPCMessage { - method: RPCMethod.Transfer; - params: TransferResponseParams; -} +export type PongResponseParams = PongResponse['params']; /** - * Represents the parameters for the 'transfer_notification' RPC method. + * Represents the parameters for the 'transfer' RPC method. */ -export type TransferNotificationResponseParams = Transaction[]; +export type TransferResponseParams = TransferResponse['params']; /** - * Represents the response structure for the 'transfer_notification' RPC method. + * Represents the parameters for the 'tr' RPC method. */ -export interface TransferNotificationResponse extends GenericRPCMessage { - method: RPCMethod.TransferNotification; - params: TransferNotificationResponseParams; -} +export type TransferNotificationResponseParams = TransferNotificationResponse['params']; /** * Union type for all possible RPC response types. diff --git a/sdk/src/utils/channel.ts b/sdk/src/utils/channel.ts index 0ff7f162d..3c9a2905c 100644 --- a/sdk/src/utils/channel.ts +++ b/sdk/src/utils/channel.ts @@ -1,6 +1,6 @@ -import { keccak256, encodeAbiParameters, Address } from 'viem'; +import { keccak256, encodeAbiParameters, Address, Hex } from 'viem'; import { Channel, ChannelId, State } from '../client/types'; // Updated import path -import { ChannelOperationState, RPCChannel, ServerSignature } from '../rpc'; +import { RPCChannel, RPCChannelOperationState } from '../rpc'; /** * Compute the unique identifier for a channel based on its configuration. @@ -66,7 +66,7 @@ export function convertRPCToClientChannel(ch: RPCChannel): Channel { }; } -export function convertRPCToClientState(s: ChannelOperationState, sig: ServerSignature): State { +export function convertRPCToClientState(s: RPCChannelOperationState, sig: Hex): State { return { intent: s.intent, version: BigInt(s.version), diff --git a/sdk/test/unit/rpc/api.test.ts b/sdk/test/unit/rpc/api.test.ts index 650e1f712..572305757 100644 --- a/sdk/test/unit/rpc/api.test.ts +++ b/sdk/test/unit/rpc/api.test.ts @@ -21,17 +21,16 @@ import { createECDSAMessageSigner, } from '../../../src/rpc/api'; import { - CreateAppSessionRequest, MessageSigner, AuthChallengeResponse, RPCMethod, - RPCChannelStatus, - RequestData, - TransferAllocation, ResizeChannelRequestParams, AuthRequestParams, CloseAppSessionRequestParams, - TxType, + RPCChannelStatus, + RPCTransferAllocation, + RPCTxType, + RPCData, } from '../../../src/rpc/types'; describe('API message creators', () => { @@ -49,8 +48,8 @@ describe('API message creators', () => { test('createAuthRequestMessage', async () => { const authRequest: AuthRequestParams = { - wallet: clientAddress, - participant: clientAddress, + address: clientAddress, + session_key: clientAddress, app_name: 'test-app', allowances: [], expire: '', @@ -64,7 +63,15 @@ describe('API message creators', () => { req: [ requestId, RPCMethod.AuthRequest, - [clientAddress, clientAddress, 'test-app', [], '', '', clientAddress], + { + address: clientAddress, + session_key: clientAddress, + app_name: 'test-app', + allowances: [], + expire: '', + scope: '', + application: clientAddress, + }, timestamp, ], sig: [], @@ -74,10 +81,10 @@ describe('API message creators', () => { test('createAuthVerifyMessageFromChallenge', async () => { const challenge = 'challenge123'; const msgStr = await createAuthVerifyMessageFromChallenge(signer, challenge, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, [[{ challenge }]], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, { challenge }, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.AuthVerify, [[{ challenge }]], timestamp], + req: [requestId, RPCMethod.AuthVerify, { challenge }, timestamp], sig: ['0xsig'], }); }); @@ -98,10 +105,10 @@ describe('API message creators', () => { test('successful challenge flow', async () => { const msgStr = await createAuthVerifyMessage(signer, rawResponse, requestId, timestamp); const challenge = 'c8261773-2619-4fbe-9514-96392f87e7b2'; - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, [{ challenge }], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, { challenge }, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.AuthVerify, [{ challenge }], timestamp], + req: [requestId, RPCMethod.AuthVerify, { challenge }, timestamp], sig: ['0xsig'], }); }); @@ -109,37 +116,37 @@ describe('API message creators', () => { test('createPingMessage', async () => { const msgStr = await createPingMessage(signer, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Ping, [], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Ping, {}, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.Ping, [], timestamp], + req: [requestId, RPCMethod.Ping, {}, timestamp], sig: ['0xsig'], }); }); test('createGetConfigMessage', async () => { const msgStr = await createGetConfigMessage(signer, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetConfig, [], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetConfig, {}, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.GetConfig, [], timestamp], + req: [requestId, RPCMethod.GetConfig, {}, timestamp], sig: ['0xsig'], }); }); test('createGetUserTagMessage', async () => { const msgStr = await createGetUserTagMessage(signer, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetUserTag, [], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetUserTag, {}, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.GetUserTag, [], timestamp], + req: [requestId, RPCMethod.GetUserTag, {}, timestamp], sig: ['0xsig'], }); }); test('createGetLedgerBalancesMessage', async () => { const participant = '0x0123124124124100000000000000000000000000' as Address; - const ledgerParams = [{ participant }]; + const ledgerParams = { participant }; const msgStr = await createGetLedgerBalancesMessage(signer, participant, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerBalances, ledgerParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -150,7 +157,7 @@ describe('API message creators', () => { }); test('createGetAppDefinitionMessage', async () => { - const appParams = [{ app_session_id: appId }]; + const appParams = { app_session_id: appId }; const msgStr = await createGetAppDefinitionMessage(signer, appId, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetAppDefinition, appParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -161,30 +168,28 @@ describe('API message creators', () => { }); test('createAppSessionMessage', async () => { - const params = [ - { - definition: { - protocol: 'p', - participants: [], - weights: [], - quorum: 0, - challenge: 0, - nonce: 0, - }, - allocations: [ - { - participant: '0xAaBbCcDdEeFf0011223344556677889900aAbBcC' as Address, - asset: 'usdc', - amount: '0.0', - }, - { - participant: '0x00112233445566778899AaBbCcDdEeFf00112233' as Address, - asset: 'usdc', - amount: '200.0', - }, - ], + const params = { + definition: { + protocol: 'p', + participants: [], + weights: [], + quorum: 0, + challenge: 0, + nonce: 0, }, - ]; + allocations: [ + { + participant: '0xAaBbCcDdEeFf0011223344556677889900aAbBcC' as Address, + asset: 'usdc', + amount: '0.0', + }, + { + participant: '0x00112233445566778899AaBbCcDdEeFf00112233' as Address, + asset: 'usdc', + amount: '200.0', + }, + ], + }; const msgStr = await createAppSessionMessage(signer, params, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.CreateAppSession, params, timestamp]); const parsed = JSON.parse(msgStr); @@ -195,7 +200,7 @@ describe('API message creators', () => { }); test('createCloseAppSessionMessage', async () => { - const closeParams: CloseAppSessionRequestParams[] = [{ app_session_id: appId, allocations: [] }]; + const closeParams: CloseAppSessionRequestParams = { app_session_id: appId, allocations: [] }; const msgStr = await createCloseAppSessionMessage(signer, closeParams, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.CloseAppSession, closeParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -206,7 +211,7 @@ describe('API message creators', () => { }); test('createApplicationMessage', async () => { - const messageParams = ['hello']; + const messageParams = 'hello'; const msgStr = await createApplicationMessage(signer, appId, messageParams, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Message, messageParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -222,7 +227,7 @@ describe('API message creators', () => { expect(signer).toHaveBeenCalledWith([ requestId, RPCMethod.CloseChannel, - [{ channel_id: channelId, funds_destination: fundDestination }], + { channel_id: channelId, funds_destination: fundDestination }, timestamp, ]); const parsed = JSON.parse(msgStr); @@ -230,7 +235,7 @@ describe('API message creators', () => { req: [ requestId, RPCMethod.CloseChannel, - [{ channel_id: channelId, funds_destination: fundDestination }], + { channel_id: channelId, funds_destination: fundDestination }, timestamp, ], sig: ['0xsig'], @@ -243,28 +248,26 @@ describe('API message creators', () => { expect(signer).not.toHaveBeenCalled(); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.AuthVerify, [{ jwt: jwtToken }], timestamp], + req: [requestId, RPCMethod.AuthVerify, { jwt: jwtToken }, timestamp], sig: [], }); }); test('createResizeChannelMessage', async () => { - const resizeParams: ResizeChannelRequestParams[] = [ - { - channel_id: channelId, - funds_destination: fundDestination, - resize_amount: 1000n, - }, - ]; + const resizeParams: ResizeChannelRequestParams = { + channel_id: channelId, + funds_destination: fundDestination, + resize_amount: 1000n, + }; const msgStr = await createResizeChannelMessage(signer, resizeParams, requestId, timestamp); // The signer should be called with the original bigint value expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.ResizeChannel, resizeParams, timestamp]); const parsed = JSON.parse(msgStr); // The parsed message should have the stringified bigint - const resizeParamsExpected = resizeParams.map((param) => ({ - ...param, - resize_amount: param.resize_amount?.toString(), - })); + const resizeParamsExpected = { + ...resizeParams, + resize_amount: resizeParams.resize_amount?.toString(), + }; expect(parsed).toEqual({ req: [requestId, RPCMethod.ResizeChannel, resizeParamsExpected, timestamp], sig: ['0xsig'], @@ -277,19 +280,19 @@ describe('API message creators', () => { expect(signer).toHaveBeenCalledWith([ requestId, RPCMethod.GetChannels, - [{ participant, status: RPCChannelStatus.Open }], + { participant, status: RPCChannelStatus.Open }, timestamp, ]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.GetChannels, [{ participant, status: RPCChannelStatus.Open }], timestamp], + req: [requestId, RPCMethod.GetChannels, { participant, status: RPCChannelStatus.Open }, timestamp], sig: ['0xsig'], }); }); test('createTransferMessage with destination address', async () => { const destination = '0x1234567890123456789012345678901234567890' as Address; - const allocations: TransferAllocation[] = [ + const allocations: RPCTransferAllocation[] = [ { asset: 'usdc', amount: '100.5', @@ -301,17 +304,17 @@ describe('API message creators', () => { ]; const transferParams = { destination, allocations }; const msgStr = await createTransferMessage(signer, transferParams, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, [transferParams], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, transferParams, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.Transfer, [transferParams], timestamp], + req: [requestId, RPCMethod.Transfer, transferParams, timestamp], sig: ['0xsig'], }); }); test('createTransferMessage with destination_user_tag', async () => { const destination_user_tag = 'UX123D8C'; - const allocations: TransferAllocation[] = [ + const allocations: RPCTransferAllocation[] = [ { asset: 'usdc', amount: '100.5', @@ -319,16 +322,16 @@ describe('API message creators', () => { ]; const transferParams = { destination_user_tag, allocations }; const msgStr = await createTransferMessage(signer, transferParams, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, [transferParams], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, transferParams, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.Transfer, [transferParams], timestamp], + req: [requestId, RPCMethod.Transfer, transferParams, timestamp], sig: ['0xsig'], }); }); test('createTransferMessage validates destination parameters', async () => { - const allocations: TransferAllocation[] = [{ asset: 'usdc', amount: '100.5' }]; + const allocations: RPCTransferAllocation[] = [{ asset: 'usdc', amount: '100.5' }]; // Test missing both parameters await expect(createTransferMessage(signer, { allocations }, requestId, timestamp)).rejects.toThrow( @@ -345,7 +348,7 @@ describe('API message creators', () => { test('createGetLedgerTransactionsMessage with no filters', async () => { const accountId = 'test-account'; - const expectedParams = [{ account_id: accountId }]; + const expectedParams = { account_id: accountId }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, undefined, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -359,21 +362,19 @@ describe('API message creators', () => { const accountId = 'test-account'; const filters = { asset: 'usdc', - tx_type: TxType.Transfer, + tx_type: RPCTxType.Transfer, offset: 10, limit: 20, sort: 'desc' as const, }; - const expectedParams = [ - { - account_id: accountId, - asset: 'usdc', - tx_type: TxType.Transfer, - offset: 10, - limit: 20, - sort: 'desc', - }, - ]; + const expectedParams = { + account_id: accountId, + asset: 'usdc', + tx_type: RPCTxType.Transfer, + offset: 10, + limit: 20, + sort: 'desc', + }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, filters, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -389,13 +390,11 @@ describe('API message creators', () => { asset: 'eth', limit: 5, }; - const expectedParams = [ - { - account_id: accountId, - asset: 'eth', - limit: 5, - }, - ]; + const expectedParams = { + account_id: accountId, + asset: 'eth', + limit: 5, + }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, filters, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -409,18 +408,16 @@ describe('API message creators', () => { const accountId = 'test-account'; const filters = { asset: '', - tx_type: TxType.Transfer, + tx_type: RPCTxType.Transfer, offset: 0, limit: undefined, sort: null as any, }; - const expectedParams = [ - { - account_id: accountId, - tx_type: TxType.Transfer, - offset: 0, - }, - ]; + const expectedParams = { + account_id: accountId, + tx_type: RPCTxType.Transfer, + offset: 0, + }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, filters, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -432,12 +429,12 @@ describe('API message creators', () => { test('createECDSAMessageSigner', async () => { const privateKey = '0xb482c8fa261c29eaaa646703948e2cc2a2ff54411cc42d8fce9a161035dfb3dc'; - const payload = [42, 'ping', [{ p1: 4337, p2: 7702 }], 20] as unknown as RequestData; + const payload: RPCData = [42, RPCMethod.Ping, { p1: 4337, p2: 7702 }, 20]; const signer = createECDSAMessageSigner(privateKey); const signature = await signer(payload); expect(signature).toBeDefined(); expect(signature).toEqual( - '0xebf96c7d3d64ab9195a341d3c922e2cb88ea592d2e229aa64d27e024f895e5720e68786c8b34a61d34a0b6f5e0f65dbe95f0a46dee9b7055df3e33f3209ea0d21b', + '0x7263178cbb9b9820491b3add77f83ebbab7df700fc30734a659b69bf0268073a2494ccbcf0ee3e98a9321f88385013a88aabe6a640d6411cda19fbbc197d38ac1c', ); }); }); diff --git a/sdk/test/unit/rpc/nitrolite.test.ts b/sdk/test/unit/rpc/nitrolite.test.ts index 3fc49b943..4c49551fc 100644 --- a/sdk/test/unit/rpc/nitrolite.test.ts +++ b/sdk/test/unit/rpc/nitrolite.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, jest } from '@jest/globals'; +import { describe, test, expect, jest, beforeAll, afterAll } from '@jest/globals'; import { Address, Hex } from 'viem'; import { NitroliteRPC } from '../../../src/rpc/nitrolite'; import { @@ -6,9 +6,8 @@ import { MessageSigner, SingleMessageVerifier, MultiMessageVerifier, - RequestData, - ResponsePayload, RPCMethod, + RPCData, } from '../../../src/rpc/types'; describe('NitroliteRPC', () => { @@ -22,10 +21,18 @@ describe('NitroliteRPC', () => { test('should create a valid request message', () => { const requestId = 12345; const method = RPCMethod.Ping; - const params = ['param1', 'param2']; + const params = { + param1: 'value1', + param2: 'value2', + }; const timestamp = 1619876543210; - const result = NitroliteRPC.createRequest(requestId, method, params, timestamp); + const result = NitroliteRPC.createRequest({ + requestId, + method, + params, + timestamp, + }); expect(result).toEqual({ req: [requestId, method, params, timestamp], @@ -35,12 +42,15 @@ describe('NitroliteRPC', () => { test('should use default values when not provided', () => { jest.spyOn(global.Date, 'now').mockReturnValue(1619876543210); - const result = NitroliteRPC.createRequest(undefined, RPCMethod.Ping); + const result = NitroliteRPC.createRequest({ + method: RPCMethod.Ping, + params: {}, + }); expect(result.req).toBeDefined(); expect(result.req![0]).toBeGreaterThan(0); expect(result.req![1]).toBe(RPCMethod.Ping); - expect(result.req![2]).toEqual([]); + expect(result.req![2]).toEqual({}); expect(result.req![3]).toBe(1619876543210); }); }); @@ -49,11 +59,22 @@ describe('NitroliteRPC', () => { test('should create a valid application request message', () => { const requestId = 12345; const method = RPCMethod.Ping; - const params = ['param1', 'param2']; + const params = { + param1: 'value1', + param2: 'value2', + }; const timestamp = 1619876543210; const accountId = '0xaccountId' as Hex; - const result = NitroliteRPC.createAppRequest(requestId, method, params, timestamp, accountId); + const result = NitroliteRPC.createAppRequest( + { + requestId, + method, + params, + timestamp, + }, + accountId, + ); expect(result).toEqual({ req: [requestId, method, params, timestamp], @@ -62,121 +83,10 @@ describe('NitroliteRPC', () => { }); }); - describe('parseResponse', () => { - test('should parse a valid response message string', () => { - const responseStr = JSON.stringify({ - res: [12345, RPCMethod.Ping, ['result1', 'result2'], 1619876543210], - }); - - const result = NitroliteRPC.parseResponse(responseStr); - - expect(result).toEqual({ - isValid: true, - isError: false, - requestId: 12345, - method: RPCMethod.Ping, - data: ['result1', 'result2'], - timestamp: 1619876543210, - }); - }); - - test('should parse a valid response message object', () => { - const responseObj = { - res: [12345, RPCMethod.Ping, ['result1', 'result2'], 1619876543210], - }; - - const result = NitroliteRPC.parseResponse(responseObj); - - expect(result).toEqual({ - isValid: true, - isError: false, - requestId: 12345, - method: RPCMethod.Ping, - data: ['result1', 'result2'], - timestamp: 1619876543210, - }); - }); - - test('should parse a valid response message with sid field', () => { - const responseObj = { - res: [12345, RPCMethod.Ping, ['result1', 'result2'], 1619876543210], - sid: '0xaccountId' as Hex, - }; - - const result = NitroliteRPC.parseResponse(responseObj); - - expect(result).toEqual({ - isValid: true, - isError: false, - requestId: 12345, - method: RPCMethod.Ping, - data: ['result1', 'result2'], - sid: '0xaccountId', - timestamp: 1619876543210, - }); - }); - - test('should handle error responses correctly', () => { - const errorResponse = { - res: [12345, 'error', [{ error: 'Something went wrong' }], 1619876543210], - }; - - const result = NitroliteRPC.parseResponse(errorResponse); - - expect(result).toEqual({ - isValid: true, - isError: true, - requestId: 12345, - method: 'error', - data: { error: 'Something went wrong' }, - timestamp: 1619876543210, - }); - }); - - test('should return invalid for malformed JSON', () => { - const result = NitroliteRPC.parseResponse('invalid json'); - - expect(result.isValid).toBe(false); - expect(result.error).toBe('Message parsing failed'); - }); - - test('should return invalid for missing res field', () => { - const result = NitroliteRPC.parseResponse({ something: 'else' }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("Invalid message structure: Missing or invalid 'res' array."); - }); - - test('should return invalid for incorrectly sized res array', () => { - const result = NitroliteRPC.parseResponse({ res: [1, 2, 3] }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("Invalid message structure: Missing or invalid 'res' array."); - }); - - test('should return invalid for incorrect types in res array', () => { - const result = NitroliteRPC.parseResponse({ - res: ['not-a-number', 123, 'not-an-array', 'not-a-number'], - }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("Invalid 'res' payload structure or types."); - }); - - test('should return invalid for malformed error response', () => { - const result = NitroliteRPC.parseResponse({ - res: [12345, 'error', ['not an error object'], 1619876543210], - }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe('Malformed error response payload.'); - }); - }); - describe('signRequestMessage', () => { test('should sign a request message and add signature to the message', async () => { const mockSigner = jest - .fn<(data: RequestData | ResponsePayload) => Promise>() + .fn<(data: RPCData) => Promise>() .mockResolvedValue('0xsignature' as Hex); const request: NitroliteRPCMessage = { req: [12345, RPCMethod.Ping, ['param1', 'param2'], 1619876543210], diff --git a/sdk/test/unit/rpc/utils.test.ts b/sdk/test/unit/rpc/utils.test.ts index 88b39ba1f..a3d91d82d 100644 --- a/sdk/test/unit/rpc/utils.test.ts +++ b/sdk/test/unit/rpc/utils.test.ts @@ -12,7 +12,7 @@ import { isValidResponseTimestamp, isValidResponseRequestId, } from '../../../src/rpc/utils'; -import { rpcResponseParser } from '../../../src/rpc/parse/parse'; +import { parseAuthChallengeResponse, parseCreateAppSessionResponse, parseGetConfigResponse, parseGetLedgerBalancesResponse, parsePingResponse } from '../../../src/rpc/parse/parse'; import { NitroliteRPCMessage, RPCMethod, RPCChannelStatus } from '../../../src/rpc/types'; describe('RPC Utils', () => { @@ -201,14 +201,14 @@ describe('RPC Utils', () => { }); }); -describe('rpcResponseParser', () => { +describe('rpc response parsers', () => { test('should parse auth_challenge response correctly', () => { const rawResponse = JSON.stringify({ - res: [123, RPCMethod.AuthChallenge, [{ challenge_message: 'test-challenge' }], 456], + res: [123, RPCMethod.AuthChallenge, { challenge_message: 'test-challenge' }, 456], sig: ['0x123'], }); - const result = rpcResponseParser.authChallenge(rawResponse); + const result = parseAuthChallengeResponse(rawResponse); expect(result.method).toBe(RPCMethod.AuthChallenge); expect(result.requestId).toBe(123); expect(result.timestamp).toBe(456); @@ -217,24 +217,26 @@ describe('rpcResponseParser', () => { }); test('should parse get_ledger_balances response correctly', () => { - const balances = [ - [ + const balances = { + ledger_balances: [ { asset: 'eth', amount: 1.5 }, { asset: 'usdc', amount: 1000 }, ], - ]; + }; const rawResponse = JSON.stringify({ res: [123, RPCMethod.GetLedgerBalances, balances, 456], sig: ['0x123'], }); - const result = rpcResponseParser.getLedgerBalances(rawResponse); + const result = parseGetLedgerBalancesResponse(rawResponse); expect(result.method).toBe(RPCMethod.GetLedgerBalances); - expect(result.params).toEqual([ - { asset: 'eth', amount: '1.5' }, - { asset: 'usdc', amount: '1000' }, - ]); + expect(result.params).toEqual({ + ledgerBalances: [ + { asset: 'eth', amount: '1.5' }, + { asset: 'usdc', amount: '1000' }, + ], + }); }); test('should parse get_config response correctly', () => { @@ -251,11 +253,11 @@ describe('rpcResponseParser', () => { }; const rawResponse = JSON.stringify({ - res: [123, RPCMethod.GetConfig, [config], 456], + res: [123, RPCMethod.GetConfig, config, 456], sig: ['0x123'], }); - const result = rpcResponseParser.getConfig(rawResponse); + const result = parseGetConfigResponse(rawResponse); expect(result.method).toBe(RPCMethod.GetConfig); expect(result.params).toEqual({ brokerAddress: '0x1234567890123456789012345678901234567890', @@ -272,11 +274,11 @@ describe('rpcResponseParser', () => { test('should parse ping response correctly', () => { const rawResponse = JSON.stringify({ - res: [123, RPCMethod.Ping, [], 456], + res: [123, RPCMethod.Ping, {}, 456], sig: ['0x123'], }); - const result = rpcResponseParser.ping(rawResponse); + const result = parsePingResponse(rawResponse); expect(result.method).toBe(RPCMethod.Ping); expect(result.requestId).toBe(123); expect(result.params).toEqual({}); @@ -290,11 +292,11 @@ describe('rpcResponseParser', () => { }; const rawResponse = JSON.stringify({ - res: [123, RPCMethod.CreateAppSession, [params], 456], + res: [123, RPCMethod.CreateAppSession, params, 456], sig: ['0x123'], }); - const result = rpcResponseParser.createAppSession(rawResponse); + const result = parseCreateAppSessionResponse(rawResponse); expect(result.method).toBe(RPCMethod.CreateAppSession); expect(result.params).toEqual({ @@ -309,22 +311,22 @@ describe('rpcResponseParser', () => { res: [123, RPCMethod.Ping, 456], // Missing one element }); - expect(() => rpcResponseParser.ping(invalidResponse)).toThrow('Invalid RPC response format'); + expect(() => parsePingResponse(invalidResponse)).toThrow('Invalid RPC response format'); }); test('should throw error for invalid JSON', () => { const invalidJSON = 'this is not json'; - expect(() => rpcResponseParser.ping(invalidJSON)).toThrow(/Failed to parse RPC response/); + expect(() => parsePingResponse(invalidJSON)).toThrow(/Failed to parse RPC response/); }); test('should throw error when parsing with the wrong method', () => { const rawResponse = JSON.stringify({ - res: [123, RPCMethod.AuthChallenge, [{ challenge_message: 'test-challenge' }], 456], + res: [123, RPCMethod.AuthChallenge, { challenge_message: 'test-challenge' }, 456], sig: ['0x123'], }); // Try to parse an auth_challenge response with the ping parser - expect(() => rpcResponseParser.ping(rawResponse)).toThrow( + expect(() => parsePingResponse(rawResponse)).toThrow( "Expected RPC method to be 'ping', but received 'auth_challenge'", ); }); From 2290bb25b8e41bd80604d700ecd15b54be3fd027 Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Wed, 6 Aug 2025 15:51:05 +0300 Subject: [PATCH 2/2] fix: resolve issues after rebase --- clearnode/rpc_router_private_test.go | 4 +- integration/common/nitroliteClient.ts | 3 +- integration/tests/challenge_channel.test.ts | 155 -------------------- sdk/src/rpc/api.ts | 7 +- sdk/src/rpc/parse/channel.ts | 4 +- sdk/src/rpc/parse/common.ts | 3 +- 6 files changed, 14 insertions(+), 162 deletions(-) delete mode 100644 integration/tests/challenge_channel.test.ts diff --git a/clearnode/rpc_router_private_test.go b/clearnode/rpc_router_private_test.go index d2b94efc9..40fa836c8 100644 --- a/clearnode/rpc_router_private_test.go +++ b/clearnode/rpc_router_private_test.go @@ -1708,7 +1708,7 @@ func TestRPCRouterHandleCreateChannel(t *testing.T) { router.HandleCreateChannel(ctx) res := assertResponse(t, ctx, "create_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be CreateChannelResponse") // Verify response structure @@ -1861,7 +1861,7 @@ func TestRPCRouterHandleCreateChannel(t *testing.T) { // This should work as zero amount channels are allowed res := assertResponse(t, ctx, "create_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be CreateChannelResponse") require.True(t, resObj.State.Allocations[0].RawAmount.IsZero(), "User allocation should be zero") }) diff --git a/integration/common/nitroliteClient.ts b/integration/common/nitroliteClient.ts index b54d16f51..4295b11f4 100644 --- a/integration/common/nitroliteClient.ts +++ b/integration/common/nitroliteClient.ts @@ -7,6 +7,7 @@ import { NitroliteClient, parseChannelUpdateResponse, parseCloseChannelResponse, + parseCreateChannelResponse, RPCChannelStatus, } from '@erc7824/nitrolite'; import { Identity } from './identity'; @@ -55,7 +56,7 @@ export class TestNitroliteClient extends NitroliteClient { const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); expect(createResponse).toBeDefined(); - const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const { params: createParsedResponseParams } = parseCreateChannelResponse(createResponse); const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), diff --git a/integration/tests/challenge_channel.test.ts b/integration/tests/challenge_channel.test.ts deleted file mode 100644 index bdc98085b..000000000 --- a/integration/tests/challenge_channel.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { createAuthSessionWithClearnode } from '@/auth'; -import { BlockchainUtils } from '@/blockchainUtils'; -import { DatabaseUtils } from '@/databaseUtils'; -import { Identity } from '@/identity'; -import { TestNitroliteClient } from '@/nitroliteClient'; -import { CONFIG } from '@/setup'; -import { getChannelUpdatePredicateWithStatus, TestWebSocket, getGetLedgerEntriesPredicate } from '@/ws'; -import { createGetLedgerEntriesMessage, parseGetLedgerEntriesResponse, RPCChannelStatus } from '@erc7824/nitrolite'; -import { parseUnits, GetTxpoolContentReturnType, Hash } from 'viem'; - -// TODO: this test could stuck anvil if is not gracefully closed -describe.skip('Close channel', () => { - const depositAmount = parseUnits('100', 6); // 100 USDC (decimals = 6) - const decimalDepositAmount = 100; - - let ws: TestWebSocket; - let identity: Identity; - let client: TestNitroliteClient; - let blockUtils: BlockchainUtils; - let databaseUtils: DatabaseUtils; - - beforeAll(async () => { - blockUtils = new BlockchainUtils(); - databaseUtils = new DatabaseUtils(); - identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); - ws = new TestWebSocket(CONFIG.CLEARNODE_URL, CONFIG.DEBUG_MODE); - - await blockUtils.resumeMining(); - }); - - beforeEach(async () => { - await ws.connect(); - await createAuthSessionWithClearnode(ws, identity); - await blockUtils.makeSnapshot(); - }); - - afterEach(async () => { - ws.close(); - await databaseUtils.cleanupDatabaseData(); - await blockUtils.resetSnapshot(); - }); - - afterAll(async () => { - databaseUtils.close(); - - await blockUtils.resumeMining(); - }); - - it('should create nitrolite client to challenge channels', async () => { - client = new TestNitroliteClient(identity); - - expect(client).toBeDefined(); - expect(client).toHaveProperty('depositAndCreateChannel'); - expect(client).toHaveProperty('challengeChannel'); - }); - - it('should challenge channel in joining state', async () => { - const joiningChannelPromise = ws.waitForMessage( - getChannelUpdatePredicateWithStatus(RPCChannelStatus.Joining), - undefined, - 5000 - ); - - const hash = await client.approveTokens(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, depositAmount); - await blockUtils.waitForTransaction(hash); - - await blockUtils.pauseMining(); - - const { channelId, txHash: createTxHash } = await client.depositAndCreateChannel( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - depositAmount, - { - initialAllocationAmounts: [depositAmount, BigInt(0)], - stateData: '0x', - } - ); - - // Mine exactly one block to ensure the transaction is processed and join is not mined - const depositTxPromise = blockUtils.waitForTransaction(createTxHash); - await blockUtils.mineBlock(); - await depositTxPromise; - - const { lastValidState } = await client.getChannelData(channelId); - const poolWithJoin: GetTxpoolContentReturnType = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - clearInterval(interval); - reject(new Error('Timed out waiting for pending transaction in txpool')); - }, 5000); - - const interval = setInterval(async () => { - const pool = await blockUtils.readTxPool(); - if (Object.keys(pool.pending).length > 0) { - clearInterval(interval); - clearTimeout(timeout); - resolve(pool); - } - }, 200); - }); - - // TODO: this approach is very brittle, and could fail if there are multiple pending transactions - // which usually doesn't happen in tests, but still - const txKey = Object.keys(poolWithJoin.pending)[0]; - const txIndex = Object.keys(poolWithJoin.pending[txKey])[0]; - const joinTx = poolWithJoin.pending[txKey][txIndex]; - - await blockUtils.dropTxFromPool(joinTx.hash as Hash); - - const challengeTxHash = await client.challengeChannel({ - channelId, - candidateState: lastValidState, - }); - - const challengeTxPromise = blockUtils.waitForTransaction(challengeTxHash); - await blockUtils.mineBlock(); - await challengeTxPromise; - - const joinTxHash = await blockUtils.sendRawTransactionAs( - CONFIG.IDENTITIES[0].WALLET_PK, - { - chainId: Number(BigInt(joinTx.chainId)), - nonce: Number(BigInt(joinTx.nonce)), - gasPrice: BigInt(joinTx.gasPrice), - gas: BigInt(joinTx.gas), - to: joinTx.to, - value: BigInt(joinTx.value), - data: joinTx.input, - }, - { - v: BigInt(joinTx.v), - r: joinTx.r, - s: joinTx.s, - } - ); - - const joinTxPromise = blockUtils.waitForTransaction(joinTxHash); - await blockUtils.mineBlock(); - await joinTxPromise; - - const channelData = await client.getChannelData(channelId); - expect(channelData).toBeDefined(); - - const joiningResponse = await joiningChannelPromise; - expect(joiningResponse).toBeDefined(); - - const msg = await createGetLedgerEntriesMessage(identity.messageSigner, channelId); - const response = await ws.sendAndWaitForResponse(msg, getGetLedgerEntriesPredicate(), 5000); - - const { params: parsedResponseParams } = parseGetLedgerEntriesResponse(response); - expect(parsedResponseParams).toBeDefined(); - - expect(parsedResponseParams).toHaveLength(2); - expect(+parsedResponseParams[0].debit + +parsedResponseParams[1].debit).toEqual(decimalDepositAmount); - expect(+parsedResponseParams[0].credit + +parsedResponseParams[1].credit).toEqual(decimalDepositAmount); - }); -}); diff --git a/sdk/src/rpc/api.ts b/sdk/src/rpc/api.ts index 5deeac77b..99f564fa5 100644 --- a/sdk/src/rpc/api.ts +++ b/sdk/src/rpc/api.ts @@ -484,7 +484,12 @@ export async function createCreateChannelMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CreateChannel, [params], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CreateChannel, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest, (_, value) => (typeof value === 'bigint' ? value.toString() : value)); } diff --git a/sdk/src/rpc/parse/channel.ts b/sdk/src/rpc/parse/channel.ts index 8c03d72df..33d3601e0 100644 --- a/sdk/src/rpc/parse/channel.ts +++ b/sdk/src/rpc/parse/channel.ts @@ -40,7 +40,7 @@ const ChannelOperationObjectSchema = ChannelOperationObject.transform( allocations: raw.state.allocations.map((a) => ({ destination: a.destination, token: a.token, - amount: a.amount, + amount: BigInt(a.amount), })), }, serverSignature: raw.server_signature, @@ -98,7 +98,7 @@ const ChannelUpdateObjectSchema = ChannelUpdateObject.transform( participant: raw.participant, status: raw.status, token: raw.token, - amount: raw.amount, + amount: BigInt(raw.amount), chainId: raw.chain_id, adjudicator: raw.adjudicator, challenge: raw.challenge, diff --git a/sdk/src/rpc/parse/common.ts b/sdk/src/rpc/parse/common.ts index 9c26812d7..c5a38940b 100644 --- a/sdk/src/rpc/parse/common.ts +++ b/sdk/src/rpc/parse/common.ts @@ -31,7 +31,8 @@ export const addressSchema = z }) .transform((v: string) => v as Address); -export const bigIntSchema = z.string().transform((a) => BigInt(a)); +// TODO: add more validation for bigints if needed +export const bigIntSchema = z.string(); export const dateSchema = z.union([z.string(), z.date()]).transform((v) => new Date(v));