Documentation
¶
Overview ¶
Package rpc provides the RPC API types for the ClearNode broker service.
The ClearNode RPC API enables interaction with the broker for payment channel management, virtual application sessions, and ledger operations. All monetary amounts use decimal.Decimal for arbitrary precision arithmetic.
Package rpc provides a high-level client for interacting with the ClearNode RPC server.
The Client type wraps a Dialer to provide convenient methods for all RPC operations, including authentication, channel management, application sessions, and event handling.
Package rpc provides the core data structures and utilities for the Clearnode RPC protocol.
This package implements a secure, signature-based RPC communication protocol designed for blockchain and distributed systems. It provides strong typing, efficient encoding, and clear separation between client-facing and internal errors.
Protocol Overview ¶
The protocol uses a request-response pattern with cryptographic signatures:
- Requests contain a payload and one or more signatures
- Responses mirror the request structure with their own signatures
- Payloads use a compact array-based JSON encoding for efficiency
- All messages include timestamps for replay protection
Core Types ¶
Request and Response types wrap payloads with signatures:
type Request struct {
Req Payload // The request payload
Sig []sign.Signature // One or more signatures
}
type Response struct {
Res Payload // The response payload
Sig []sign.Signature // One or more signatures
}
Payloads contain the actual RPC data:
type Payload struct {
RequestID uint64 // Unique request identifier
Method string // RPC method name
Params Params // Method parameters
Timestamp uint64 // Unix milliseconds timestamp
}
JSON Encoding ¶
Payloads use a compact array encoding for efficiency. A payload like:
Payload{
RequestID: 12345,
Method: "wallet_transfer",
Params: {"to": "0xabc", "amount": "100"},
Timestamp: 1634567890123,
}
Encodes to:
[12345, "wallet_transfer", {"to": "0xabc", "amount": "100"}, 1634567890123]
This format reduces message size while maintaining readability and compatibility.
Error Handling ¶
The package provides explicit error types for client communication:
// Client-facing error - will be sent in response
if amount < 0 {
return rpc.Errorf("invalid amount: cannot be negative")
}
// Internal error - generic message sent to client
if err := db.Save(); err != nil {
return fmt.Errorf("database error: %w", err)
}
Parameter Handling ¶
The Params type provides flexible parameter handling with type safety:
// Creating parameters from a struct
params, err := rpc.NewParams(struct{
Address string `json:"address"`
Amount string `json:"amount"`
}{
Address: "0x123...",
Amount: "1000000000000000000",
})
// Extracting parameters into a struct
var req TransferRequest
err := params.Translate(&req)
Security Considerations ¶
When using this protocol:
- Always verify signatures before processing requests
- Validate timestamps to prevent replay attacks
- Use rpc.Errorf() for safe client-facing errors
- Thoroughly validate all parameters
- Use unique request IDs to prevent duplicate processing
Client Communication ¶
The package provides two levels of client APIs:
1. Low-level Dialer interface for direct RPC communication 2. High-level Client type with methods for all ClearNode operations
## High-Level Client (Recommended)
The Client type provides convenient methods for all RPC operations:
// Create client with WebSocket dialer
dialer := rpc.NewWebsocketDialer(rpc.DefaultWebsocketDialerConfig)
client := rpc.NewClient(dialer)
// Set up event handlers
client.HandleBalanceUpdateEvent(func(ctx context.Context, notif rpc.BalanceUpdateNotification, sigs []sign.Signature) {
log.Info("Balance updated", "balances", notif.BalanceUpdates)
})
// Connect to server and start event handling
err := client.Start(ctx, "wss://server.example.com/ws", func(err error) {
if err != nil {
log.Error("Connection closed", "error", err)
}
})
if err != nil {
log.Fatal("Failed to start client", "error", err)
}
// Make RPC calls
config, _, err := client.GetConfig(ctx)
if err != nil {
log.Fatal(err)
}
// Authenticate
authReq := rpc.AuthRequestRequest{
Address: walletAddress,
SessionKey: sessionKeyAddress,
Application: "MyApp",
}
authResp, _, err := client.AuthWithSig(ctx, authReq, walletSigner)
if err != nil {
log.Fatal(err)
}
jwtToken := authResp.JwtToken
// Make authenticated calls
balances, _, err := client.GetLedgerBalances(ctx, rpc.GetLedgerBalancesRequest{})
## Low-Level Dialer
For direct RPC communication without the convenience methods:
params, _ := rpc.NewParams(map[string]string{"key": "value"})
payload := rpc.NewPayload(1, "method_name", params)
request := rpc.NewRequest(payload)
response, err := dialer.Call(ctx, &request)
if err != nil {
log.Error("RPC call failed", "error", err)
}
// Handle events manually
go func() {
for event := range dialer.EventCh() {
if event == nil {
break
}
log.Info("Received event", "method", event.Res.Method)
}
}()
API Types ¶
The package includes comprehensive type definitions for the ClearNode RPC API:
- Request/Response types for all RPC methods - Asset and network configuration types - Payment channel state and operations - Application session management - Ledger and transaction types - Event notification types
All monetary values use decimal.Decimal for arbitrary precision arithmetic.
Server Implementation ¶
The package provides a complete RPC server implementation through the Node interface:
// Create and configure the server
config := rpc.WebsocketNodeConfig{
Signer: signer,
Logger: logger,
OnConnectHandler: func(send rpc.SendResponseFunc) {
// Handle new connections
},
OnAuthenticatedHandler: func(userID string, send rpc.SendResponseFunc) {
// Handle authentication
},
}
node, err := rpc.NewWebsocketNode(config)
if err != nil {
log.Fatal(err)
}
// Register handlers
node.Handle("get_balance", handleGetBalance)
node.Handle("transfer", handleTransfer)
// Add middleware
node.Use(loggingMiddleware)
node.Use(authMiddleware)
// Create handler groups
privateGroup := node.NewGroup("private")
privateGroup.Use(requireAuthMiddleware)
privateGroup.Handle("create_channel", handleCreateChannel)
// Start the server
http.Handle("/ws", node)
http.ListenAndServe(":8080", nil)
Writing handlers:
func handleGetBalance(c *rpc.Context) {
// Extract parameters
var req GetBalanceRequest
if err := c.Request.Req.Params.Translate(&req); err != nil {
c.Fail(nil, "invalid parameters")
return
}
// Process request
balance := getBalanceForUser(c.UserID, req.Asset)
// Send response
c.Succeed("get_balance", rpc.Params{"balance": balance})
}
Writing middleware:
func authMiddleware(c *rpc.Context) {
// Check if connection is authenticated
if c.UserID == "" {
// Try to authenticate from request
token := extractToken(c.Request)
userID, err := validateToken(token)
if err != nil {
c.Fail(nil, "authentication required")
return
}
c.UserID = userID
}
// Continue to next handler
c.Next()
}
Example Usage ¶
Creating and sending a request:
// Create request
params, _ := rpc.NewParams(map[string]string{"key": "value"})
payload := rpc.NewPayload(12345, "method_name", params)
request := rpc.NewRequest(payload, signature)
// Marshal and send
data, _ := json.Marshal(request)
// ... send data over transport ...
Processing a request:
// Unmarshal request
var request rpc.Request
err := json.Unmarshal(data, &request)
// Verify signatures using GetSigners
signers, err := request.GetSigners()
if err != nil {
return rpc.Errorf("invalid signatures: %v", err)
}
// Check if request is from a known address
authorized := false
for _, signer := range signers {
if signer == trustedAddress {
authorized = true
break
}
}
if !authorized {
return rpc.Errorf("unauthorized request")
}
// Process based on method
switch request.Req.Method {
case "transfer":
var params TransferParams
if err := request.Req.Params.Translate(¶ms); err != nil {
return rpc.Errorf("invalid parameters: %v", err)
}
// ... handle transfer ...
}
Testing ¶
The package includes a comprehensive test suite with mock implementations:
- client_test.go: Unit tests for all client methods - client_internal_test.go: Tests for internal authentication methods - client_manual_test.go: Integration tests against live server (requires credentials)
The manual test demonstrates real-world usage patterns and can be run with:
TEST_WALLET_PK=<wallet_private_key> TEST_SESSION_PK=<session_private_key> go test -run TestManualClient
Index ¶
- Variables
- func IsSupportedVersion(version Version) bool
- type AccountType
- type Allowance
- type AllowanceUsage
- type AppAllocation
- type AppDefinition
- type AppSession
- type AppSessionIntent
- type AppSessionUpdateEventHandler
- type AppSessionUpdateNotification
- type Asset
- type AuthJWTVerifyRequest
- type AuthJWTVerifyResponse
- type AuthRequestRequest
- type AuthRequestResponse
- type AuthSigVerifyRequest
- type AuthSigVerifyResponse
- type BalanceUpdateEventHandler
- type BalanceUpdateNotification
- type BlockchainInfo
- type BrokerConfig
- type Channel
- type ChannelOperationResponse
- type ChannelStatus
- type ChannelUpdateEventHandler
- type ChannelUpdateNotification
- type Client
- func (c *Client) AuthJWTVerify(ctx context.Context, reqParams AuthJWTVerifyRequest) (AuthJWTVerifyResponse, []sign.Signature, error)
- func (c *Client) AuthWithSig(ctx context.Context, reqParams AuthRequestRequest, signer sign.Signer) (AuthSigVerifyResponse, []sign.Signature, error)
- func (c *Client) CleanupSessionKeyCache(ctx context.Context) ([]sign.Signature, error)
- func (c *Client) CloseAppSession(ctx context.Context, req *Request) (CloseAppSessionResponse, []sign.Signature, error)
- func (c *Client) CloseChannel(ctx context.Context, req *Request) (CloseChannelResponse, []sign.Signature, error)
- func (c *Client) CreateAppSession(ctx context.Context, req *Request) (CreateAppSessionResponse, []sign.Signature, error)
- func (c *Client) CreateChannel(ctx context.Context, req *Request) (CreateChannelResponse, []sign.Signature, error)
- func (c *Client) GetAppDefinition(ctx context.Context, reqParams GetAppDefinitionRequest) (GetAppDefinitionResponse, []sign.Signature, error)
- func (c *Client) GetAppSessions(ctx context.Context, reqParams GetAppSessionsRequest) (GetAppSessionsResponse, []sign.Signature, error)
- func (c *Client) GetAssets(ctx context.Context, reqParams GetAssetsRequest) (GetAssetsResponse, []sign.Signature, error)
- func (c *Client) GetChannels(ctx context.Context, reqParams GetChannelsRequest) (GetChannelsResponse, []sign.Signature, error)
- func (c *Client) GetConfig(ctx context.Context) (GetConfigResponse, []sign.Signature, error)
- func (c *Client) GetLedgerBalances(ctx context.Context, reqParams GetLedgerBalancesRequest) (GetLedgerBalancesResponse, []sign.Signature, error)
- func (c *Client) GetLedgerEntries(ctx context.Context, reqParams GetLedgerEntriesRequest) (GetLedgerEntriesResponse, []sign.Signature, error)
- func (c *Client) GetLedgerTransactions(ctx context.Context, reqParams GetLedgerTransactionsRequest) (GetLedgerTransactionsResponse, []sign.Signature, error)
- func (c *Client) GetRPCHistory(ctx context.Context, reqParams GetRPCHistoryRequest) (GetRPCHistoryResponse, []sign.Signature, error)
- func (c *Client) GetSessionKeys(ctx context.Context, reqParams GetSessionKeysRequest) (GetSessionKeysResponse, []sign.Signature, error)
- func (c *Client) GetUserTag(ctx context.Context) (GetUserTagResponse, []sign.Signature, error)
- func (c *Client) HandleAppSessionUpdateEvent(handler AppSessionUpdateEventHandler)
- func (c *Client) HandleBalanceUpdateEvent(handler BalanceUpdateEventHandler)
- func (c *Client) HandleChannelUpdateEvent(handler ChannelUpdateEventHandler)
- func (c *Client) HandleTransferEvent(handler TransferEventHandler)
- func (c *Client) Ping(ctx context.Context) ([]sign.Signature, error)
- func (c *Client) PreparePayload(method Method, reqParams any) (Payload, error)
- func (c *Client) ResizeChannel(ctx context.Context, req *Request) (ResizeChannelResponse, []sign.Signature, error)
- func (c *Client) Start(ctx context.Context, url string, handleClosure func(err error)) error
- func (c *Client) SubmitAppState(ctx context.Context, req *Request) (SubmitAppStateResponse, []sign.Signature, error)
- func (c *Client) Transfer(ctx context.Context, req *Request) (TransferResponse, []sign.Signature, error)
- type CloseAppSessionRequest
- type CloseAppSessionResponse
- type CloseChannelRequest
- type CloseChannelResponse
- type Connection
- type ConnectionHub
- type Context
- type CreateAppSessionRequest
- type CreateAppSessionResponse
- type CreateChannelRequest
- type CreateChannelResponse
- type Dialer
- type Error
- type Event
- type GetAppDefinitionRequest
- type GetAppDefinitionResponse
- type GetAppSessionsRequest
- type GetAppSessionsResponse
- type GetAssetsRequest
- type GetAssetsResponse
- type GetChannelsRequest
- type GetChannelsResponse
- type GetConfigResponse
- type GetLedgerBalancesRequest
- type GetLedgerBalancesResponse
- type GetLedgerEntriesRequest
- type GetLedgerEntriesResponse
- type GetLedgerTransactionsRequest
- type GetLedgerTransactionsResponse
- type GetRPCHistoryRequest
- type GetRPCHistoryResponse
- type GetSessionKeysRequest
- type GetSessionKeysResponse
- type GetUserTagResponse
- type GorillaWsConnectionAdapter
- type Handler
- type HandlerGroup
- type HistoryEntry
- type LedgerBalance
- type LedgerEntry
- type LedgerTransaction
- type ListOptions
- type Method
- type Node
- type Params
- type Payload
- type Request
- type ResizeChannelRequest
- type ResizeChannelResponse
- type Response
- type RevokeSessionKeyRequest
- type RevokeSessionKeyResponse
- type SafeStorage
- type SendResponseFunc
- type SessionKeyResponse
- type SortType
- type StateAllocation
- type StateIntent
- type SubmitAppStateRequest
- type SubmitAppStateResponse
- type TransferAllocation
- type TransferEventHandler
- type TransferNotification
- type TransferRequest
- type TransferResponse
- type UnsignedState
- type Version
- type WebsocketConnection
- func (conn *WebsocketConnection) ConnectionID() string
- func (conn *WebsocketConnection) RawRequests() <-chan []byte
- func (conn *WebsocketConnection) Serve(parentCtx context.Context, handleClosure func(error))
- func (conn *WebsocketConnection) SetUserID(userID string)
- func (conn *WebsocketConnection) UserID() string
- func (conn *WebsocketConnection) WriteRawResponse(message []byte) bool
- type WebsocketConnectionConfig
- type WebsocketDialer
- type WebsocketDialerConfig
- type WebsocketHandlerGroup
- type WebsocketNode
- func (wn *WebsocketNode) Handle(method string, handler Handler)
- func (wn *WebsocketNode) NewGroup(name string) HandlerGroup
- func (wn *WebsocketNode) Notify(userID, method string, params Params)
- func (wn *WebsocketNode) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (wn *WebsocketNode) Use(middleware Handler)
- type WebsocketNodeConfig
Constants ¶
This section is empty.
Variables ¶
var ( // Connection errors ErrAlreadyConnected = fmt.Errorf("already connected") ErrNotConnected = fmt.Errorf("not connected to server") ErrConnectionTimeout = fmt.Errorf("websocket connection timeout") ErrReadingMessage = fmt.Errorf("error reading message") // Request/Response errors ErrNilRequest = fmt.Errorf("nil request") ErrInvalidRequestMethod = fmt.Errorf("invalid request method") ErrMarshalingRequest = fmt.Errorf("error marshaling request") ErrSendingRequest = fmt.Errorf("error sending request") ErrNoResponse = fmt.Errorf("no response received") ErrSendingPing = fmt.Errorf("error sending ping") // WebSocket-specific errors ErrDialingWebsocket = fmt.Errorf("error dialing websocket server") )
Dialer error messages
var DefaultWebsocketDialerConfig = WebsocketDialerConfig{ HandshakeTimeout: 5 * time.Second, PingInterval: 5 * time.Second, PingRequestID: 100, EventChanSize: 100, }
DefaultWebsocketDialerConfig provides sensible defaults for WebSocket connections
Functions ¶
func IsSupportedVersion ¶ added in v0.4.0
IsSupportedVersion checks if the given version is supported by the broker.
Types ¶
type AccountType ¶
type AccountType uint16
AccountType categorizes ledger accounts following standard accounting principles.
const ( // AssetDefault represents asset accounts (1000-1999) AssetDefault AccountType = 1000 // LiabilityDefault represents liability accounts (2000-2999) LiabilityDefault AccountType = 2000 // EquityDefault represents equity/capital accounts (3000-3999) EquityDefault AccountType = 3000 // RevenueDefault represents revenue accounts (4000-4999) RevenueDefault AccountType = 4000 // ExpenseDefault represents expense accounts (5000-5999) ExpenseDefault AccountType = 5000 )
type Allowance ¶
type Allowance struct {
// Asset is the token symbol
Asset string `json:"asset"`
// Amount is the spending limit
Amount string `json:"amount"`
}
Allowance defines spending limits for authenticated sessions.
type AllowanceUsage ¶ added in v0.5.0
type AllowanceUsage struct {
// Asset is the token/asset symbol
Asset string `json:"asset"`
// Allowance is the total spending limit for this asset
Allowance decimal.Decimal `json:"allowance"`
// Used is how much of the allowance has been spent
Used decimal.Decimal `json:"used"`
}
AllowanceUsage represents an asset allowance with usage tracking.
type AppAllocation ¶
type AppAllocation struct {
// Participant is the recipient's address
Participant string `json:"participant"`
// AssetSymbol identifies the asset
AssetSymbol string `json:"asset"`
// Amount allocated to the participant
Amount decimal.Decimal `json:"amount"`
}
AppAllocation defines asset distribution for a participant in an app session.
type AppDefinition ¶
type AppDefinition struct {
// Application is the identifier of the application
Application string `json:"application"`
// Protocol identifies the version of the application protocol
Protocol Version `json:"protocol"`
// ParticipantWallets lists the wallet addresses of all participants
ParticipantWallets []string `json:"participants"`
// Weights defines the signature weight for each participant
Weights []int64 `json:"weights"`
// Quorum is the minimum weight required for consensus
Quorum uint64 `json:"quorum"`
// Challenge is the timeout period for disputes (in seconds)
Challenge uint64 `json:"challenge"`
// Nonce ensures uniqueness of the application instance
Nonce uint64 `json:"nonce"`
}
AppDefinition defines the protocol for a multi-party application.
type AppSession ¶
type AppSession struct {
// AppSessionID is the unique session identifier
AppSessionID string `json:"app_session_id"`
// Application is the name of the application
Application string `json:"application"`
// Status indicates the session state (open/closed)
Status string `json:"status"`
// ParticipantWallets lists all participants
ParticipantWallets []string `json:"participants"`
// SessionData contains application-specific state
SessionData string `json:"session_data,omitempty"`
// Protocol identifies the version of the application protocol
Protocol Version `json:"protocol"`
// Challenge is the dispute timeout period
Challenge uint64 `json:"challenge"`
// Weights defines participant signature weights
Weights []int64 `json:"weights"`
// Quorum is the consensus threshold
Quorum uint64 `json:"quorum"`
// Version tracks state updates
Version uint64 `json:"version"`
// Nonce ensures uniqueness
Nonce uint64 `json:"nonce"`
// CreatedAt is when the session was created (RFC3339)
CreatedAt string `json:"created_at"`
// UpdatedAt is when the session was last modified (RFC3339)
UpdatedAt string `json:"updated_at"`
}
AppSession represents an active virtual application session.
type AppSessionIntent ¶ added in v0.4.0
type AppSessionIntent string
AppSessionIntent indicates the purpose of an application state update.
const ( // AppSessionIntentOperate is for normal application operation AppSessionIntentOperate AppSessionIntent = "operate" // AppSessionIntentDeposit is for adding funds to the session AppSessionIntentDeposit AppSessionIntent = "deposit" // AppSessionIntentWithdraw is for removing funds from the session AppSessionIntentWithdraw AppSessionIntent = "withdraw" )
type AppSessionUpdateEventHandler ¶ added in v0.5.0
type AppSessionUpdateEventHandler func(ctx context.Context, notif AppSessionUpdateNotification, resSig []sign.Signature)
AppSessionUpdateEventHandler processes application session update notifications from the server. These notifications are sent when an application session's state changes.
type AppSessionUpdateNotification ¶ added in v0.4.0
type AppSessionUpdateNotification struct {
AppSession AppSession `json:"app_session"`
// ParticipantAllocations contains each participant's asset allocations
ParticipantAllocations []AppAllocation `json:"participant_allocations"`
}
AppSessionUpdateNotification is sent when an application session's state changes. This includes session creation, state updates, and session closure.
type Asset ¶
type Asset struct {
// Token is the token contract address
Token string `json:"token"`
// ChainID identifies the blockchain network
ChainID uint32 `json:"chain_id"`
// Symbol is the token symbol (e.g., "USDC")
Symbol string `json:"symbol"`
// Decimals is the number of decimal places for the token
Decimals uint8 `json:"decimals"`
}
Asset represents a supported token/asset on a specific chain.
type AuthJWTVerifyRequest ¶
type AuthJWTVerifyRequest struct {
// JWT is the token to verify
JWT string `json:"jwt"`
}
AuthJWTVerifyRequest verifies an existing JWT token.
type AuthJWTVerifyResponse ¶
type AuthJWTVerifyResponse struct {
// Address is the wallet address from the JWT
Address string `json:"address"`
// SessionKey is the session identifier from the JWT
SessionKey string `json:"session_key"`
// Success indicates if the JWT is valid
Success bool `json:"success"`
}
AuthJWTVerifyResponse contains the JWT verification result.
type AuthRequestRequest ¶
type AuthRequestRequest struct {
// Address is the wallet address requesting authentication
Address string `json:"address"`
// SessionKey is a unique key for this authentication session
SessionKey string `json:"session_key"`
// Application identifies the application requesting authentication
Application string `json:"application"`
// Allowances define spending limits for the authenticated session
Allowances []Allowance `json:"allowances"`
// ExpiresAt defines when the authentication expires (Unix timestamp)
ExpiresAt uint64 `json:"expires_at"`
// Scope defines the permission scope for the session
Scope string `json:"scope"`
}
AuthRequestRequest initiates wallet-based authentication flow.
type AuthRequestResponse ¶
type AuthRequestResponse struct {
// ChallengeMessage is the UUID that must be signed by the wallet
ChallengeMessage uuid.UUID `json:"challenge_message"`
}
AuthRequestResponse contains the challenge for wallet signature.
type AuthSigVerifyRequest ¶
type AuthSigVerifyRequest struct {
// Challenge is the UUID that was signed
Challenge uuid.UUID `json:"challenge"`
}
AuthSigVerifyRequest verifies a signed authentication challenge.
type AuthSigVerifyResponse ¶
type AuthSigVerifyResponse struct {
// Address is the authenticated wallet address
Address string `json:"address"`
// SessionKey echoes the session key from the request
SessionKey string `json:"session_key"`
// JwtToken is the JWT for authenticated API calls
JwtToken string `json:"jwt_token"`
// Success indicates if authentication succeeded
Success bool `json:"success"`
}
AuthSigVerifyResponse contains the authentication result and session token.
type BalanceUpdateEventHandler ¶
type BalanceUpdateEventHandler func(ctx context.Context, notif BalanceUpdateNotification, resSig []sign.Signature)
BalanceUpdateEventHandler processes balance update notifications from the server. These notifications are sent when account balances change due to transfers, channel operations, or application session updates.
type BalanceUpdateNotification ¶
type BalanceUpdateNotification struct {
// BalanceUpdates contains the new balances for affected accounts
BalanceUpdates []LedgerBalance `json:"balance_updates"`
}
BalanceUpdateNotification is sent when account balances change. This notification is triggered by transfers, channel operations, or app session updates.
type BlockchainInfo ¶ added in v0.5.0
type BlockchainInfo struct {
// ID is the network's chain identifier
ID uint32 `json:"chain_id"`
// Name is the human-readable name of the blockchain
Name string `json:"name"` // TODO: add to SDK
// CustodyAddress is the custody contract address
CustodyAddress string `json:"custody_address"`
// AdjudicatorAddress is the adjudicator contract address
AdjudicatorAddress string `json:"adjudicator_address"`
}
BlockchainInfo describes a supported blockchain network.
type BrokerConfig ¶
type BrokerConfig struct {
// BrokerAddress is the wallet address of the broker
BrokerAddress string `json:"broker_address"`
// Networks lists all supported blockchain networks
Blockchains []BlockchainInfo `json:"networks"` // TODO: rename to "blockchains"
}
BrokerConfig contains the broker's configuration and supported networks.
type Channel ¶
type Channel struct {
// ChannelID is the unique channel identifier
ChannelID string `json:"channel_id"`
// Participant is the user's wallet address
Participant string `json:"participant"`
// Status indicates the channel state
Status ChannelStatus `json:"status"`
// Token is the asset contract address
Token string `json:"token"`
// Wallet is the participant's wallet address
Wallet string `json:"wallet"`
// RawAmount is the total channel capacity (user + broker funds)
RawAmount decimal.Decimal `json:"amount"`
// ChainID identifies the blockchain network
ChainID uint32 `json:"chain_id"`
// Adjudicator is the dispute resolution contract
Adjudicator string `json:"adjudicator"`
// Challenge is the dispute timeout period
Challenge uint64 `json:"challenge"`
// Nonce ensures channel uniqueness
Nonce uint64 `json:"nonce"`
// Version tracks state updates
Version uint64 `json:"version"`
// CreatedAt is when the channel was opened (RFC3339)
CreatedAt string `json:"created_at"`
// UpdatedAt is when the channel was last modified (RFC3339)
UpdatedAt string `json:"updated_at"`
}
Channel represents a payment channel between a user and the broker.
type ChannelOperationResponse ¶
type ChannelOperationResponse struct {
// ChannelID is the channel identifier
ChannelID string `json:"channel_id"`
// Channel contains the on-chain channel parameters
Channel *struct {
Participants [2]string `json:"participants"`
Adjudicator string `json:"adjudicator"`
Challenge uint64 `json:"challenge"`
Nonce uint64 `json:"nonce"`
} `json:"channel,omitempty"`
// State is the new channel state
State UnsignedState `json:"state"`
// StateSignature is the broker's signature on the state
StateSignature sign.Signature `json:"server_signature"`
}
ChannelOperationResponse is returned by channel create/resize/close operations.
type ChannelStatus ¶
type ChannelStatus string
ChannelStatus represents the current state of a payment channel.
var ( // ChannelStatusOpen indicates an active channel ChannelStatusOpen ChannelStatus = "open" // ChannelStatusResizing indicates a channel being resized ChannelStatusResizing ChannelStatus = "resizing" // ChannelStatusClosed indicates a finalized channel ChannelStatusClosed ChannelStatus = "closed" // ChannelStatusChallenged indicates a channel in dispute ChannelStatusChallenged ChannelStatus = "challenged" )
type ChannelUpdateEventHandler ¶
type ChannelUpdateEventHandler func(ctx context.Context, notif ChannelUpdateNotification, resSig []sign.Signature)
ChannelUpdateEventHandler processes channel update notifications from the server. These notifications are sent when a channel's state changes, including creation, resizing, closure, or challenge events.
type ChannelUpdateNotification ¶
type ChannelUpdateNotification Channel
ChannelUpdateNotification is sent when a channel's state changes. This includes channel creation, resizing, closure, or challenge events. The notification contains the full updated channel information.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client provides a high-level interface for interacting with the ClearNode RPC server. It wraps a Dialer to provide convenient methods for all RPC operations and manages event handlers for asynchronous notifications from the server.
The Client is safe for concurrent use and supports:
- All public RPC methods (no authentication required)
- Authentication via wallet signature or JWT
- Authenticated RPC methods (require prior authentication)
- Event handling for balance updates, channel updates, and transfers
Example usage:
dialer := rpc.NewWebsocketDialer(rpc.DefaultWebsocketDialerConfig)
client := rpc.NewClient(dialer)
// Connect to the server
go dialer.Dial(ctx, "wss://server.example.com/ws", handleError)
// Set up event handlers
client.HandleBalanceUpdateEvent(func(ctx context.Context, notif BalanceUpdateNotification, sigs []sign.Signature) {
fmt.Printf("Balance updated: %+v\n", notif.BalanceUpdates)
})
// Start listening for events
go client.ListenEvents(ctx, handleError)
// Make RPC calls
config, _, err := client.GetConfig(ctx)
if err != nil {
log.Fatal(err)
}
func NewClient ¶
NewClient creates a new RPC client using the provided dialer. The dialer must be connected before making RPC calls.
Example:
dialer := rpc.NewWebsocketDialer(rpc.DefaultWebsocketDialerConfig) client := rpc.NewClient(dialer)
func (*Client) AuthJWTVerify ¶
func (c *Client) AuthJWTVerify(ctx context.Context, reqParams AuthJWTVerifyRequest) (AuthJWTVerifyResponse, []sign.Signature, error)
AuthJWTVerify verifies an existing JWT token and returns the associated session info. This is useful for validating a stored JWT token before making authenticated calls.
Parameters:
- reqParams: Contains the JWT token to verify
Returns:
- AuthJWTVerifyResponse with address, session key, and success status
- Response signatures for verification
- Error if the JWT is invalid or expired
Example:
verifyReq := AuthJWTVerifyRequest{JWT: storedJwtToken}
verifyRes, _, err := client.AuthJWTVerify(ctx, verifyReq)
if err != nil || !verifyRes.Success {
// Token is invalid or expired, need to re-authenticate
authRes, _, err = client.AuthWithSig(ctx, authReq, signer)
}
func (*Client) AuthWithSig ¶
func (c *Client) AuthWithSig(ctx context.Context, reqParams AuthRequestRequest, signer sign.Signer) (AuthSigVerifyResponse, []sign.Signature, error)
AuthWithSig performs wallet-based authentication using a signature. This method handles the complete authentication flow:
- Sends an auth request to get a challenge
- Signs the challenge using the provided signer
- Verifies the signature and receives a JWT token
The JWT token returned should be stored and used for subsequent authenticated calls.
Parameters:
- reqParams: Authentication request containing:
- Address: Main wallet address requesting authentication (cold wallet)
- SessionKey: Address of a different key that will be used for signing during this session (hot wallet)
- Application: Name of the application
- Allowances: Spending limits for the session
- ExpiresAt: When the authentication expires (Unix timestamp)
- Scope: Permission scope (e.g., "trade", "view", or empty)
- signer: Signer interface to sign the challenge (should correspond to Address, not SessionKey)
Returns:
- AuthSigVerifyResponse containing JWT token and success status
- Response signatures for verification
- Error if authentication fails
Example:
walletSigner, _ := sign.NewEthereumSigner(walletPrivateKey) // Main wallet
sessionSigner, _ := sign.NewEthereumSigner(sessionPrivateKey) // Session key
authReq := AuthRequestRequest{
Address: walletSigner.PublicKey().Address().String(), // Main wallet
SessionKey: sessionSigner.PublicKey().Address().String(), // Different key for session
Application: "MyDApp",
Allowances: []Allowance{{Asset: "usdc", Amount: "1000"}},
}
// Sign with main wallet, but SessionKey will be used for subsequent operations
authRes, _, err := client.AuthWithSig(ctx, authReq, walletSigner)
if err != nil {
log.Fatal("Authentication failed", "error", err)
}
jwtToken := authRes.JwtToken // Store this for authenticated calls
func (*Client) CleanupSessionKeyCache ¶ added in v0.5.0
CleanupSessionKeyCache clears cached session key data on the server. This is applicable only when Clearnode is running in test mode.
Example:
sigs, err := client.CleanupSessionKeyCache(ctx)
if err != nil {
log.Error("failed to cleanup session key cache", "error", err)
}
func (*Client) CloseAppSession ¶
func (c *Client) CloseAppSession(ctx context.Context, req *Request) (CloseAppSessionResponse, []sign.Signature, error)
CloseAppSession closes an application session and distributes final assets. Requires signatures from enough participants to meet the quorum requirement.
Requires authentication and sufficient signatures to satisfy quorum.
Parameters:
- req: Prepared Request with CloseAppSessionMethod containing:
- AppSessionID: ID of the session to close
- SessionData: Final application state (optional)
- Allocations: Final asset distribution
The request must include signatures totaling at least the quorum weight. Typically, all participants should sign to agree on the final distribution.
Returns:
- CloseAppSessionResponse with closed session details
- Response signatures for verification
- Error if not authenticated, insufficient signatures, or closure fails
Example:
closeReq := CloseAppSessionRequest{
AppSessionID: "app123",
Allocations: []AppAllocation{
{ParticipantWallet: player1, AssetSymbol: "USDC", Amount: decimal.NewFromInt(150)},
{ParticipantWallet: player2, AssetSymbol: "USDC", Amount: decimal.NewFromInt(50)},
},
}
payload, _ := client.PreparePayload(CloseAppSessionMethod, closeReq)
hash, _ := payload.Hash()
// Get signatures to meet quorum (typically all participants)
sig1, err := player1Signer.Sign(hash)
if err != nil {
log.Fatal("Failed to sign", "error", err)
}
sig2, err := player2Signer.Sign(hash)
if err != nil {
log.Fatal("Failed to sign", "error", err)
}
fullReq := rpc.NewRequest(payload, sig1, sig2)
response, _, err := client.CloseAppSession(ctx, &fullReq)
func (*Client) CloseChannel ¶
func (c *Client) CloseChannel(ctx context.Context, req *Request) (CloseChannelResponse, []sign.Signature, error)
CloseChannel requests the server to close a payment channel. The server returns a final signed state that you must sign and submit to the blockchain to close the channel on-chain and recover your funds.
Requires authentication.
Parameters:
- req: Prepared Request with CloseChannelMethod containing:
- ChannelID: ID of the channel to close
- FundsDestination: Where to send the channel funds after closure
Returns:
- CloseChannelResponse with final channel state and server signature
- Response signatures for verification
- Error if not authenticated, invalid request, or server rejects
Example:
closeReq := CloseChannelRequest{
ChannelID: "ch123",
FundsDestination: walletAddress,
}
payload, _ := client.PreparePayload(CloseChannelMethod, closeReq)
hash, _ := payload.Hash()
sig, err := sessionSigner.Sign(hash)
if err != nil {
log.Fatal("Failed to sign request", "error", err)
}
fullReq := rpc.NewRequest(payload, sig)
response, _, err := client.CloseChannel(ctx, &fullReq)
if err != nil {
log.Error("Failed to close channel", "error", err)
}
// Sign the final state and submit to blockchain to close channel
func (*Client) CreateAppSession ¶
func (c *Client) CreateAppSession(ctx context.Context, req *Request) (CreateAppSessionResponse, []sign.Signature, error)
CreateAppSession starts a new virtual application session. Application sessions enable multi-party state channel applications.
Requires authentication and signatures from all participants.
Parameters:
- req: Prepared Request with CreateAppSessionMethod containing:
- Definition: Application protocol and participants
- Allocations: Initial asset distribution
- SessionData: Application-specific initial state (optional)
The request must be signed by all participants listed in the definition.
Returns:
- CreateAppSessionResponse with session ID and details
- Response signatures for verification
- Error if not authenticated, missing signatures, or creation fails
Example:
createSessReq := CreateAppSessionRequest{
Definition: AppDefinition{
Protocol: "game/v1",
ParticipantWallets: []string{player1, player2},
Weights: []int64{1, 1},
Quorum: 2,
Challenge: 3600,
Nonce: uint64(uuid.New().ID()),
},
Allocations: []AppAllocation{
{ParticipantWallet: player1, AssetSymbol: "USDC", Amount: decimal.NewFromInt(100)},
{ParticipantWallet: player2, AssetSymbol: "USDC", Amount: decimal.NewFromInt(100)},
},
}
payload, _ := client.PreparePayload(CreateAppSessionMethod, createSessReq)
hash, _ := payload.Hash()
// Both participants must sign
sig1, err := player1Signer.Sign(hash)
if err != nil {
log.Fatal("Player 1 sign failed", "error", err)
}
sig2, err := player2Signer.Sign(hash)
if err != nil {
log.Fatal("Player 2 sign failed", "error", err)
}
fullReq := rpc.NewRequest(payload, sig1, sig2)
response, _, err := client.CreateAppSession(ctx, &fullReq)
func (*Client) CreateChannel ¶
func (c *Client) CreateChannel(ctx context.Context, req *Request) (CreateChannelResponse, []sign.Signature, error)
CreateChannel requests the server to create a new payment channel. The server validates the request and returns a signed channel state that you must then sign and submit to the blockchain yourself to open the channel on-chain.
Requires authentication.
Parameters:
- req: Prepared Request with CreateChannelMethod containing:
- ChainID: Blockchain network identifier
- Token: Asset/token address for the channel
- Amount: Initial funding amount
- SessionKey: Key that will control the channel (required for security)
Returns:
- CreateChannelResponse with initial channel state and server signature
- Response signatures for verification
- Error if not authenticated, invalid request, or server rejects
After receiving the response, you must:
- Sign the state yourself
- Submit both signatures to the blockchain to open the channel
Example:
amount := decimal.NewFromInt(1000000)
createReq := CreateChannelRequest{
ChainID: 1,
Token: "0xUSDC",
Amount: &amount,
SessionKey: &sessionKeyAddress, // Required
}
payload, _ := client.PreparePayload(CreateChannelMethod, createReq)
hash, _ := payload.Hash()
sig, err := sessionSigner.Sign(hash)
if err != nil {
log.Fatal("Failed to sign request", "error", err)
}
fullReq := rpc.NewRequest(payload, sig)
response, _, err := client.CreateChannel(ctx, &fullReq)
if err != nil {
log.Fatal("Failed to create channel", "error", err)
}
// Now sign the state and submit to blockchain
stateHash := computeStateHash(response.State)
mySignature, _ := sessionSigner.Sign(stateHash)
// Submit response.StateSignature and mySignature to blockchain
func (*Client) GetAppDefinition ¶
func (c *Client) GetAppDefinition(ctx context.Context, reqParams GetAppDefinitionRequest) (GetAppDefinitionResponse, []sign.Signature, error)
GetAppDefinition retrieves the protocol definition for a specific application session. This includes the participants, consensus rules, and protocol parameters.
No authentication required.
Parameters:
- reqParams: Contains the AppSessionID to query
Returns:
- GetAppDefinitionResponse with the application protocol details
- Response signatures for verification
- Error if the request fails or session not found
Example:
def, _, err := client.GetAppDefinition(ctx, GetAppDefinitionRequest{
AppSessionID: "app123",
})
if err != nil {
log.Error("Failed to get app definition", "error", err)
}
fmt.Printf("Protocol: %s, Participants: %v\n", def.Protocol, def.ParticipantWallets)
func (*Client) GetAppSessions ¶
func (c *Client) GetAppSessions(ctx context.Context, reqParams GetAppSessionsRequest) (GetAppSessionsResponse, []sign.Signature, error)
GetAppSessions retrieves a list of application sessions with optional filters. Sessions can be filtered by participant wallet address and status.
No authentication required.
Parameters:
- reqParams: Filter and pagination options:
- Participant: Filter by wallet address (optional)
- Status: Filter by session state (e.g., "open", "closed") (optional)
- ListOptions: Pagination (offset, limit) and sorting
Returns:
- GetAppSessionsResponse containing the filtered list of sessions
- Response signatures for verification
- Error if the request fails
Example:
// Get all open sessions for a specific participant
sessions, _, err := client.GetAppSessions(ctx, GetAppSessionsRequest{
Participant: "0x1234...",
Status: "open",
ListOptions: ListOptions{Limit: 10},
})
func (*Client) GetAssets ¶
func (c *Client) GetAssets(ctx context.Context, reqParams GetAssetsRequest) (GetAssetsResponse, []sign.Signature, error)
GetAssets retrieves the list of supported assets/tokens from the server. Assets can be filtered by chain ID to get tokens for a specific network.
No authentication required.
Parameters:
- reqParams: Filter options (optional ChainID filter)
Returns:
- GetAssetsResponse containing the list of supported assets
- Response signatures for verification
- Error if the request fails
Example:
// Get all assets
assets, _, err := client.GetAssets(ctx, GetAssetsRequest{})
// Get assets for a specific chain
ethChainID := uint32(1)
ethAssets, _, err := client.GetAssets(ctx, GetAssetsRequest{
ChainID: ðChainID,
})
func (*Client) GetChannels ¶
func (c *Client) GetChannels(ctx context.Context, reqParams GetChannelsRequest) (GetChannelsResponse, []sign.Signature, error)
GetChannels retrieves a list of payment channels with optional filters. Channels can be filtered by participant wallet address and status.
No authentication required.
Parameters:
- reqParams: Filter and pagination options:
- Participant: Filter by wallet address (optional)
- Status: Filter by channel state (e.g., "open", "closed", "challenged") (optional)
- ListOptions: Pagination (offset, limit) and sorting
Returns:
- GetChannelsResponse containing the filtered list of channels
- Response signatures for verification
- Error if the request fails
Example:
// Get all open channels for a user
channels, _, err := client.GetChannels(ctx, GetChannelsRequest{
Participant: userWallet,
Status: "open",
})
for _, ch := range channels.Channels {
fmt.Printf("Channel %s: %s on chain %d\n", ch.ChannelID, ch.Status, ch.ChainID)
}
func (*Client) GetConfig ¶
GetConfig retrieves the server's configuration including supported networks. This is typically the first call made by clients to discover available chains and the server's wallet address.
No authentication required.
Returns:
- GetConfigResponse containing server address and network configurations
- Response signatures for verification
- Error if the request fails
Example:
config, sigs, err := client.GetConfig(ctx)
if err != nil {
log.Fatal("Failed to get config", "error", err)
}
for _, network := range config.Networks {
fmt.Printf("Chain %d: Custody=%s\n", network.ChainID, network.CustodyAddress)
}
func (*Client) GetLedgerBalances ¶
func (c *Client) GetLedgerBalances(ctx context.Context, reqParams GetLedgerBalancesRequest) (GetLedgerBalancesResponse, []sign.Signature, error)
GetLedgerBalances retrieves account balances for the authenticated user. Balances show the current amount of each asset held in the user's accounts.
Requires authentication.
Parameters:
- reqParams: Filter options:
- AccountID: Filter balances by specific account (optional)
Returns:
- GetLedgerBalancesResponse containing balance information by asset
- Response signatures for verification
- Error if not authenticated or request fails
Example:
balances, _, err := client.GetLedgerBalances(ctx, GetLedgerBalancesRequest{})
if err != nil {
log.Fatal("Failed to get balances", "error", err)
}
for _, balance := range balances.LedgerBalances {
fmt.Printf("%s: %s\n", balance.Asset, balance.Amount)
}
func (*Client) GetLedgerEntries ¶
func (c *Client) GetLedgerEntries(ctx context.Context, reqParams GetLedgerEntriesRequest) (GetLedgerEntriesResponse, []sign.Signature, error)
GetLedgerEntries retrieves double-entry bookkeeping entries from the ledger. Entries can be filtered by account ID, asset, and wallet address.
No authentication required.
Parameters:
- reqParams: Filter and pagination options:
- AccountID: Filter by account identifier (optional)
- Asset: Filter by token/asset symbol (optional)
- Wallet: Filter by participant wallet address (optional)
- ListOptions: Pagination (offset, limit) and sorting
Returns:
- GetLedgerEntriesResponse containing the filtered ledger entries
- Response signatures for verification
- Error if the request fails
Example:
entries, _, err := client.GetLedgerEntries(ctx, GetLedgerEntriesRequest{
Asset: "usdc",
Wallet: userWallet,
ListOptions: ListOptions{Limit: 50},
})
func (*Client) GetLedgerTransactions ¶
func (c *Client) GetLedgerTransactions(ctx context.Context, reqParams GetLedgerTransactionsRequest) (GetLedgerTransactionsResponse, []sign.Signature, error)
GetLedgerTransactions retrieves ledger transactions (transfers between accounts). Transactions can be filtered by account ID, asset, and transaction type.
No authentication required.
Parameters:
- reqParams: Filter and pagination options:
- AccountID: Filter by account (optional)
- Asset: Filter by token/asset symbol (optional)
- TxType: Filter by transaction type (optional)
- ListOptions: Pagination (offset, limit) and sorting
Returns:
- GetLedgerTransactionsResponse containing the filtered transactions
- Response signatures for verification
- Error if the request fails
Example:
// Get recent USDC transactions
txns, _, err := client.GetLedgerTransactions(ctx, GetLedgerTransactionsRequest{
Asset: "usdc",
ListOptions: ListOptions{
Limit: 20,
Sort: &SortTypeDescending,
},
})
for _, tx := range txns.LedgerTransactions {
fmt.Printf("%s: %s from %s to %s\n", tx.TxType, tx.Amount, tx.FromAccount, tx.ToAccount)
}
func (*Client) GetRPCHistory ¶
func (c *Client) GetRPCHistory(ctx context.Context, reqParams GetRPCHistoryRequest) (GetRPCHistoryResponse, []sign.Signature, error)
GetRPCHistory retrieves the RPC call history for the authenticated user. History entries include method calls with their parameters, results, and signatures.
Requires authentication.
Parameters:
- reqParams: Pagination options (offset, limit, sort)
Returns:
- GetRPCHistoryResponse containing historical RPC entries
- Response signatures for verification
- Error if not authenticated or request fails
Example:
history, _, err := client.GetRPCHistory(ctx, GetRPCHistoryRequest{
ListOptions: ListOptions{Limit: 100},
})
if err != nil {
log.Error("Failed to get RPC history", "error", err)
}
for _, entry := range history.RPCEntries {
fmt.Printf("[%d] %s by %s\n", entry.Timestamp, entry.Method, entry.Sender)
}
func (*Client) GetSessionKeys ¶ added in v0.5.0
func (c *Client) GetSessionKeys(ctx context.Context, reqParams GetSessionKeysRequest) (GetSessionKeysResponse, []sign.Signature, error)
GetSessionKeys retrieves session keys with allowances for the authenticated user. Returns all active session keys with their spending limits and usage tracking.
Requires authentication.
Parameters:
- reqParams: Optional filter options (e.g., pagination)
Returns:
- GetSessionKeysResponse containing session keys with allowances
- Response signatures for verification
- Error if not authenticated or request fails
Example:
sessionKeys, _, err := client.GetSessionKeys(ctx, GetSessionKeysRequest{})
if err != nil {
log.Error("Failed to get session keys", "error", err)
}
for _, sk := range sessionKeys.SessionKeys {
fmt.Printf("Session key: %s, Application: %s\n", sk.SessionKey, sk.Application)
for _, allowance := range sk.Allowances {
fmt.Printf(" %s: %s / %s used\n", allowance.Asset, allowance.Used, allowance.Allowance)
}
}
func (*Client) GetUserTag ¶
GetUserTag returns the human-readable tag for the authenticated wallet. User tags provide a friendly identifier for wallet addresses.
Requires authentication.
Returns:
- GetUserTagResponse containing the user's tag
- Response signatures for verification
- Error if not authenticated or request fails
Example:
tag, _, err := client.GetUserTag(ctx)
if err != nil {
log.Error("Failed to get user tag", "error", err)
} else {
fmt.Printf("User tag: %s\n", tag.Tag)
}
func (*Client) HandleAppSessionUpdateEvent ¶ added in v0.5.0
func (c *Client) HandleAppSessionUpdateEvent(handler AppSessionUpdateEventHandler)
HandleAppSessionUpdateEvent registers a handler for application session update notifications. The handler will be called whenever an application session's state changes. Only one handler can be registered at a time; subsequent calls override the previous handler.
Example:
client.HandleAppSessionUpdateEvent(func(ctx context.Context, notif AppSessionUpdateNotification, sigs []sign.Signature) {
fmt.Printf("App Session %s updated: status=%s\n", notif.AppSession.AppSessionID, notif.AppSession.Status)
})
func (*Client) HandleBalanceUpdateEvent ¶
func (c *Client) HandleBalanceUpdateEvent(handler BalanceUpdateEventHandler)
HandleBalanceUpdateEvent registers a handler for balance update notifications. The handler will be called whenever the server sends a balance update event. Only one handler can be registered at a time; subsequent calls override the previous handler.
Example:
client.HandleBalanceUpdateEvent(func(ctx context.Context, notif BalanceUpdateNotification, sigs []sign.Signature) {
for _, balance := range notif.BalanceUpdates {
fmt.Printf("Balance changed: %s = %s\n", balance.Asset, balance.Amount)
}
})
func (*Client) HandleChannelUpdateEvent ¶
func (c *Client) HandleChannelUpdateEvent(handler ChannelUpdateEventHandler)
HandleChannelUpdateEvent registers a handler for channel update notifications. The handler will be called whenever a channel's state changes. Only one handler can be registered at a time; subsequent calls override the previous handler.
Example:
client.HandleChannelUpdateEvent(func(ctx context.Context, notif ChannelUpdateNotification, sigs []sign.Signature) {
fmt.Printf("Channel %s updated: status=%s\n", notif.ChannelID, notif.Status)
})
func (*Client) HandleTransferEvent ¶
func (c *Client) HandleTransferEvent(handler TransferEventHandler)
HandleTransferEvent registers a handler for transfer notifications. The handler will be called for both incoming and outgoing transfers. Only one handler can be registered at a time; subsequent calls override the previous handler.
Example:
client.HandleTransferEvent(func(ctx context.Context, notif TransferNotification, sigs []sign.Signature) {
for _, tx := range notif.Transactions {
if tx.ToAccount == myAccount {
fmt.Printf("Received %s %s from %s\n", tx.Amount, tx.Asset, tx.FromAccount)
}
}
})
func (*Client) Ping ¶
Ping sends a ping request to the server to check connectivity and liveness. Returns the response signatures if successful.
This method is useful for:
- Testing the connection is alive
- Measuring round-trip latency
- Keeping the connection active
Example:
sigs, err := client.Ping(ctx)
if err != nil {
log.Error("Ping failed", "error", err)
}
func (*Client) PreparePayload ¶
PreparePayload creates a Payload for an RPC method call. This helper method generates a unique request ID and packages the parameters.
Parameters:
- method: The RPC method to call
- reqParams: The request parameters (can be nil for methods without parameters)
Returns:
- Payload ready to be wrapped in a Request
- Error if parameter marshaling fails
Example:
req := GetAssetsRequest{ChainID: &chainID}
payload, err := client.PreparePayload(GetAssetsMethod, req)
if err != nil {
log.Fatal("Failed to prepare payload", "error", err)
}
// Now create a Request with the payload and any required signatures
fullReq := rpc.NewRequest(payload)
func (*Client) ResizeChannel ¶
func (c *Client) ResizeChannel(ctx context.Context, req *Request) (ResizeChannelResponse, []sign.Signature, error)
ResizeChannel requests the server to modify channel funding. The server returns a signed state update that you must sign and submit to the blockchain.
Requires authentication.
Parameters:
- req: Prepared Request with ResizeChannelMethod containing:
- ChannelID: ID of the channel to resize
- AllocateAmount: Amount to move from your unified balance on ClearNode to the channel (optional)
- ResizeAmount: Amount to move from custody ledger on Custody Smart Contract to the channel (optional)
- FundsDestination: Where to send funds if reducing channel size
AllocateAmount and ResizeAmount are mutually exclusive - provide only one.
Returns:
- ResizeChannelResponse with updated channel state and server signature
- Response signatures for verification
- Error if not authenticated, invalid request, or server rejects
Example:
// Move 500 tokens from your ClearNode balance to the channel
allocateAmount := decimal.NewFromInt(500)
resizeReq := ResizeChannelRequest{
ChannelID: "ch123",
AllocateAmount: &allocateAmount,
FundsDestination: walletAddress,
}
payload, _ := client.PreparePayload(ResizeChannelMethod, resizeReq)
hash, _ := payload.Hash()
sig, err := sessionSigner.Sign(hash)
if err != nil {
log.Fatal("Failed to sign request", "error", err)
}
fullReq := rpc.NewRequest(payload, sig)
response, _, err := client.ResizeChannel(ctx, &fullReq)
if err != nil {
log.Fatal("Failed to resize channel", "error", err)
}
// Sign and submit the new state to blockchain
func (*Client) Start ¶
Start establishes a connection to the RPC server and begins listening for events. This method combines connection establishment and event handling in a single call, simplifying the client initialization process.
The method will: 1. Establish a WebSocket connection to the specified URL 2. Start listening for server events in the background 3. Return immediately after successful connection (non-blocking)
Parameters:
- ctx: Context for the connection lifetime (canceling stops the connection)
- url: WebSocket URL to connect to (e.g., "wss://server.example.com/ws")
- handleClosure: Callback invoked when the connection closes (with error if any)
Returns an error if the initial connection fails (e.g., invalid URL, network error). After a successful return, the connection runs in the background until the context is canceled or a connection error occurs.
Example:
client := rpc.NewClient(dialer)
// Set up event handlers before starting
client.HandleBalanceUpdateEvent(handleBalanceUpdate)
// Start the client
err := client.Start(ctx, "wss://server.example.com/ws", func(err error) {
if err != nil {
log.Error("Connection closed", "error", err)
}
})
if err != nil {
log.Fatal("Failed to start client", "error", err)
}
// Now you can make RPC calls
config, _, err := client.GetConfig(ctx)
func (*Client) SubmitAppState ¶
func (c *Client) SubmitAppState(ctx context.Context, req *Request) (SubmitAppStateResponse, []sign.Signature, error)
SubmitAppState updates an application session's state. Requires signatures from enough participants to meet the session's quorum requirement.
Requires authentication and sufficient signatures to satisfy quorum.
Parameters:
- req: Prepared Request with SubmitAppStateMethod containing:
- AppSessionID: ID of the session to update
- Allocations: New asset distribution
- SessionData: New application state (optional)
The request must include signatures totaling at least the quorum weight. For example, if quorum is 2 and all participants have weight 1, you need signatures from at least 2 participants.
Returns:
- SubmitAppStateResponse with updated session details and new version
- Response signatures for verification
- Error if not authenticated, insufficient signatures, or invalid state
Example:
updateReq := SubmitAppStateRequest{
AppSessionID: "app123",
Allocations: []AppAllocation{
{ParticipantWallet: winner, AssetSymbol: "USDC", Amount: decimal.NewFromInt(190)},
{ParticipantWallet: loser, AssetSymbol: "USDC", Amount: decimal.NewFromInt(10)},
},
SessionData: &gameResultData,
}
payload, _ := client.PreparePayload(SubmitAppStateMethod, updateReq)
hash, _ := payload.Hash()
// Get signatures to meet quorum (e.g., both players if quorum=2)
sig1, err := player1Signer.Sign(hash)
if err != nil {
log.Fatal("Failed to sign", "error", err)
}
sig2, err := player2Signer.Sign(hash)
if err != nil {
log.Fatal("Failed to sign", "error", err)
}
fullReq := rpc.NewRequest(payload, sig1, sig2)
response, _, err := client.SubmitAppState(ctx, &fullReq)
func (*Client) Transfer ¶
func (c *Client) Transfer(ctx context.Context, req *Request) (TransferResponse, []sign.Signature, error)
Transfer moves funds between accounts within the ClearNode system. This is an off-chain transfer that updates balances on ClearNode without touching the blockchain. Transfers are instant and gas-free.
Requires authentication.
Parameters:
- reqParams: Transfer request containing:
- Destination: Recipient's ClearNode account (wallet address)
- DestinationUserTag: Recipient's human-readable tag (optional)
- Allocations: List of assets and amounts to transfer
Returns:
- TransferResponse with created ledger transactions
- Response signatures for verification
- Error if not authenticated, insufficient balance, or transfer fails
Example:
transferReq := TransferRequest{
Destination: "0xRecipient...",
Allocations: []TransferAllocation{
{AssetSymbol: "usdc", Amount: decimal.NewFromInt(100)},
{AssetSymbol: "eth", Amount: decimal.NewFromFloat(0.1)},
},
}
response, _, err := client.Transfer(ctx, transferReq)
if err != nil {
log.Fatal("Transfer failed", "error", err)
}
for _, tx := range response.Transactions {
fmt.Printf("Transferred %s %s to %s\n", tx.Amount, tx.Asset, tx.ToAccount)
}
type CloseAppSessionRequest ¶
type CloseAppSessionRequest struct {
// AppSessionID identifies the session to close
AppSessionID string `json:"app_session_id"`
// SessionData contains the final application state
SessionData *string `json:"session_data"`
// Allocations defines the final asset distribution
Allocations []AppAllocation `json:"allocations"`
}
CloseAppSessionRequest specifies application session closure parameters.
type CloseAppSessionResponse ¶
type CloseAppSessionResponse AppSession
CloseAppSessionResponse contains the closed application session.
type CloseChannelRequest ¶
type CloseChannelRequest struct {
// ChannelID identifies the channel to close
ChannelID string `json:"channel_id"`
// FundsDestination is where to send the channel funds
FundsDestination string `json:"funds_destination"`
}
CloseChannelRequest specifies channel closure parameters.
type CloseChannelResponse ¶
type CloseChannelResponse ChannelOperationResponse
CloseChannelResponse contains the final channel state after closure.
type Connection ¶ added in v0.4.0
type Connection interface {
// ConnectionID returns the unique identifier for this connection.
// This ID is generated when the connection is established and remains
// constant throughout the connection's lifetime.
ConnectionID() string
// UserID returns the authenticated user's identifier for this connection.
// Returns an empty string if the connection has not been authenticated.
UserID() string
// SetUserID sets the UserID for this connection.
// This is typically called during authentication when the connection
// becomes associated with a specific user account.
SetUserID(userID string)
// RawRequests returns a read-only channel for receiving incoming raw request messages.
// Messages received on this channel are raw bytes that need to be unmarshaled
// into Request objects for processing. The channel is closed when the
// connection is terminated.
RawRequests() <-chan []byte
// WriteRawResponse attempts to send a raw response message to the client.
// The method returns true if the message was successfully queued for sending,
// or false if the operation timed out (indicating a potentially unresponsive client).
// Messages that fail to send may trigger connection closure.
WriteRawResponse(message []byte) bool
// Serve starts the connection's lifecycle by spawning goroutines for reading and writing.
// This method returns immediately after starting the goroutines. The handleClosure
// callback will be invoked asynchronously when the connection terminates (with an
// error if abnormal termination occurred). The parentCtx parameter enables
// graceful shutdown of the connection.
Serve(parentCtx context.Context, handleClosure func(error))
}
Connection represents an active RPC connection that handles bidirectional communication. Implementations of this interface manage the connection lifecycle, message routing, and authentication state. The interface is designed to be transport-agnostic, though the primary implementation uses WebSocket.
type ConnectionHub ¶ added in v0.4.0
type ConnectionHub struct {
// contains filtered or unexported fields
}
ConnectionHub provides centralized management of all active RPC connections. It maintains thread-safe mappings between connection IDs and Connection instances, as well as user IDs and their associated connections. This enables efficient message routing and connection lifecycle management.
Key features:
- Thread-safe connection storage and retrieval
- User-to-connection mapping for authenticated sessions
- Automatic cleanup of auth mappings when connections close
- Support for re-authentication (updating user associations)
- Broadcast capabilities to all connections for a specific user
func NewConnectionHub ¶ added in v0.4.0
func NewConnectionHub() *ConnectionHub
NewConnectionHub creates a new ConnectionHub instance with initialized maps. The hub is typically used internally by Node implementations to manage the lifecycle of all active connections.
func (*ConnectionHub) Add ¶ added in v0.4.0
func (hub *ConnectionHub) Add(conn Connection) error
Add registers a new connection with the hub. The connection is indexed by its ConnectionID for fast retrieval. If the connection has an associated UserID (is authenticated), it also updates the user-to-connection mapping.
Returns an error if:
- The connection is nil
- A connection with the same ID already exists
func (*ConnectionHub) Get ¶ added in v0.4.0
func (hub *ConnectionHub) Get(connID string) Connection
Get retrieves a connection by its unique connection ID. Returns the Connection instance if found, or nil if no connection with the specified ID exists in the hub.
This method is safe for concurrent access.
func (*ConnectionHub) Publish ¶ added in v0.4.0
func (hub *ConnectionHub) Publish(userID string, response []byte)
Publish broadcasts a message to all active connections for a specific user. This enables server-initiated notifications to be sent to all of a user's connected clients (e.g., multiple browser tabs or devices).
The method:
- Looks up all connections associated with the user
- Attempts to send the message to each connection
- Silently skips any connections that fail to accept the message
If the user has no active connections, the message is silently dropped. This method is safe for concurrent access.
func (*ConnectionHub) Reauthenticate ¶ added in v0.4.0
func (hub *ConnectionHub) Reauthenticate(connID, userID string) error
Reauthenticate updates the UserID association for an existing connection. This method handles the complete re-authentication process:
- Removes the connection from the old user's mapping (if any)
- Updates the connection's UserID
- Adds the connection to the new user's mapping
This is typically called when a user logs in or switches accounts on an existing connection.
Returns an error if the specified connection doesn't exist.
func (*ConnectionHub) Remove ¶ added in v0.4.0
func (hub *ConnectionHub) Remove(connID string)
Remove unregisters a connection from the hub. This method:
- Removes the connection from the main connection map
- Cleans up any user-to-connection mappings
- Removes empty user entries to prevent memory leaks
If the connection doesn't exist, this method does nothing (no-op). This method is safe for concurrent access.
type Context ¶ added in v0.4.0
type Context struct {
// Context is the standard Go context for the request
Context context.Context
// UserID is the authenticated user's identifier (empty if not authenticated)
UserID string
// Signer is used to sign the response message
Signer sign.Signer
// Request is the original RPC request message
Request Request
// Response is the response message to be sent back to the client
Response Response
// Storage provides per-connection storage for session data
Storage *SafeStorage
// contains filtered or unexported fields
}
Context encapsulates all information related to an RPC request and provides methods for handlers to process and respond. It implements a middleware pattern where handlers can be chained together, each having the ability to process the request, modify the context, or delegate to the next handler.
The Context serves multiple purposes:
- Request/response container: Holds the incoming request and outgoing response
- Middleware chain management: Tracks and executes the handler chain
- Session state: Provides per-connection storage for maintaining state
- Authentication context: Carries the authenticated user ID
- Response helpers: Convenient methods for success and error responses
func (*Context) Fail ¶ added in v0.4.0
Fail sets an error response for the RPC request. This method should be called by handlers when an error occurs during request processing.
Error handling behavior:
- If err is an RPCError: The exact error message is sent to the client
- If err is any other error type: The fallbackMessage is sent to the client
- If both err is nil/non-RPCError AND fallbackMessage is empty: A generic error message is sent
This design allows handlers to control what error information is exposed to clients:
- Use RPCError for client-safe, descriptive error messages
- Use regular errors with a fallbackMessage to hide internal error details
Usage examples:
// Hide internal error details from client
balance, err := ledger.GetBalance(account)
if err != nil {
c.Fail(err, "failed to retrieve balance")
return
}
// Validation error with no internal error
if len(params) < 3 {
c.Fail(nil, "invalid parameters: expected at least 3")
return
}
The response will have Method="error" and Params containing the error message.
func (*Context) GetRawResponse ¶ added in v0.4.0
GetRawResponse returns the signed response message as raw bytes. This is called internally after handler processing to prepare the response.
func (*Context) Next ¶ added in v0.4.0
func (c *Context) Next()
Next advances the middleware chain by executing the next handler. This enables handlers to perform pre-processing, call Next() to delegate to subsequent handlers, then perform post-processing. If there are no more handlers in the chain, Next() returns immediately without error.
Example middleware pattern:
func authMiddleware(c *Context) {
// Pre-processing: check authentication
if c.UserID == "" {
c.Fail(nil, "authentication required")
return
}
c.Next() // Continue to next handler
// Post-processing: log the response
log.Info("Request processed", "user", c.UserID)
}
func (*Context) Succeed ¶ added in v0.4.0
Succeed sets a successful response for the RPC request. This method should be called by handlers when the request has been processed successfully. The method parameter typically matches the request method, and params contains the result data.
Example:
func handleGetBalance(c *Context) {
balance := getBalanceForUser(c.UserID)
c.Succeed("get_balance", Params{"balance": balance})
}
type CreateAppSessionRequest ¶
type CreateAppSessionRequest struct {
// Definition specifies the application protocol and participants
Definition AppDefinition `json:"definition"`
// Allocations defines the initial asset distribution
Allocations []AppAllocation `json:"allocations"`
// SessionData contains application-specific state data
SessionData *string `json:"session_data"`
}
CreateAppSessionRequest starts a new virtual application session.
type CreateAppSessionResponse ¶
type CreateAppSessionResponse AppSession
CreateAppSessionResponse contains the created application session.
type CreateChannelRequest ¶
type CreateChannelRequest struct {
// ChainID identifies the blockchain network
ChainID uint32 `json:"chain_id" validate:"required"`
// Token is the asset/token address for the channel
Token string `json:"token" validate:"required"`
// SessionKey optionally specifies a custom session identifier
SessionKey *string `json:"session_key,omitempty" validate:"omitempty"`
}
CreateChannelRequest opens a new payment channel with the broker.
type CreateChannelResponse ¶
type CreateChannelResponse ChannelOperationResponse
CreateChannelResponse contains the created channel details and initial state.
type Dialer ¶
type Dialer interface {
// Dial establishes a connection to the specified URL.
// This method is designed to be called in a goroutine as it blocks until the connection is closed.
// The handleClosure callback is invoked when the connection is closed, with an error if any.
Dial(ctx context.Context, url string, handleClosure func(err error)) error
// IsConnected returns true if the dialer has an active connection.
IsConnected() bool
// Call sends an RPC request and waits for a response.
// It returns an error if the request cannot be sent or no response is received.
// The context can be used to cancel the request.
Call(ctx context.Context, req *Request) (*Response, error)
// EventCh returns a read-only channel for receiving unsolicited events from the server.
// Events are responses that don't match any pending request ID.
EventCh() <-chan *Response
}
Dialer is the interface for RPC client connections. It provides methods to establish connections, send requests, and receive responses.
type Error ¶
type Error struct {
// contains filtered or unexported fields
}
Error represents an error in the RPC protocol that should be sent back to the client in the RPC response. Unlike generic errors, Error messages are guaranteed to be included in the error response sent to the client.
Use Error when you want to provide specific, user-facing error messages in RPC responses. For internal errors that should not be exposed to clients, use regular errors instead.
Example:
// Client will receive this exact error message
return rpc.Errorf("invalid wallet address: %s", addr)
// Client will receive a generic error message
return fmt.Errorf("database connection failed")
func Errorf ¶
Errorf creates a new Error with a formatted error message that will be sent to the client in the RPC response. This is the preferred way to create client-facing errors in RPC handlers.
The error message should be clear, actionable, and safe to expose to external clients. Avoid including sensitive information like internal system details, file paths, or database specifics.
Usage in RPC handlers:
// In a handler function that returns an error
if amount.IsNegative() {
return Errorf("invalid amount: cannot be negative")
}
// With formatting for specific details
if balance.LessThan(amount) {
return Errorf("insufficient balance: need %s but have %s", amount, balance)
}
func (Error) Error ¶
Error implements the error interface for Error. It returns the underlying error message that will be sent to clients.
This method allows Error to be used anywhere a standard Go error is expected, while maintaining the distinction that this error's message is safe for external client consumption.
type Event ¶
type Event string
Event represents a notification event type sent by the broker. Events are unsolicited notifications sent to connected clients.
const ( // BalanceUpdateEvent notifies clients of balance changes. BalanceUpdateEvent Event = "bu" // ChannelUpdateEvent notifies clients of channel state changes. ChannelUpdateEvent Event = "cu" // TransferEvent notifies clients of incoming transfers. TransferEvent Event = "tr" // AppSessionUpdateEvent notifies clients of app session state changes. AppSessionUpdateEvent Event = "asu" )
type GetAppDefinitionRequest ¶
type GetAppDefinitionRequest struct {
// AppSessionID is the unique identifier for the application session
AppSessionID string `json:"app_session_id"`
}
GetAppDefinitionRequest queries for a specific application session's definition.
type GetAppDefinitionResponse ¶
type GetAppDefinitionResponse AppDefinition
GetAppDefinitionResponse returns the application's protocol definition.
type GetAppSessionsRequest ¶
type GetAppSessionsRequest struct {
ListOptions
// Participant filters sessions by wallet address
Participant string `json:"participant,omitempty"`
// Status filters by session state (e.g., "open", "closed")
Status string `json:"status,omitempty"`
}
GetAppSessionsRequest queries for application sessions with optional filters.
type GetAppSessionsResponse ¶
type GetAppSessionsResponse struct {
AppSessions []AppSession `json:"app_sessions"`
}
GetAppSessionsResponse contains the filtered list of application sessions.
type GetAssetsRequest ¶
type GetAssetsRequest struct {
// ChainID optionally filters assets by blockchain network
ChainID *uint32 `json:"chain_id,omitempty"`
}
GetAssetsRequest filters the list of supported assets.
type GetAssetsResponse ¶
type GetAssetsResponse struct {
Assets []Asset `json:"assets"`
}
GetAssetsResponse contains the list of supported assets/tokens.
type GetChannelsRequest ¶
type GetChannelsRequest struct {
ListOptions
// Participant filters channels by wallet address
Participant string `json:"participant,omitempty"`
// Status filters by channel state (e.g., "open", "closed", "challenged")
Status string `json:"status,omitempty"`
}
GetChannelsRequest queries for payment channels with optional filters.
type GetChannelsResponse ¶
type GetChannelsResponse struct {
Channels []Channel `json:"channels"`
}
GetChannelsResponse contains the filtered list of payment channels.
type GetConfigResponse ¶
type GetConfigResponse BrokerConfig
GetConfigResponse returns the broker's configuration including supported networks. This is typically the first call made by clients to discover available chains.
type GetLedgerBalancesRequest ¶
type GetLedgerBalancesRequest struct {
// AccountID optionally filters balances by account
AccountID string `json:"account_id,omitempty"`
}
GetLedgerBalancesRequest queries account balances.
type GetLedgerBalancesResponse ¶
type GetLedgerBalancesResponse struct {
LedgerBalances []LedgerBalance `json:"ledger_balances"`
}
GetLedgerBalancesResponse contains the account balances by asset.
type GetLedgerEntriesRequest ¶
type GetLedgerEntriesRequest struct {
ListOptions
// AccountID filters entries by the account identifier
AccountID string `json:"account_id,omitempty"`
// Asset filters by token/asset symbol
Asset string `json:"asset,omitempty"`
// Wallet filters by participant wallet address
Wallet string `json:"wallet,omitempty"`
}
GetLedgerEntriesRequest queries double-entry bookkeeping entries.
type GetLedgerEntriesResponse ¶
type GetLedgerEntriesResponse struct {
LedgerEntries []LedgerEntry `json:"ledger_entries"`
}
GetLedgerEntriesResponse contains the filtered ledger entries.
type GetLedgerTransactionsRequest ¶
type GetLedgerTransactionsRequest struct {
ListOptions
// AccountID filters transactions by account
AccountID string `json:"account_id,omitempty"`
// Asset filters by token/asset symbol
Asset string `json:"asset,omitempty"`
// TxType filters by transaction type
TxType string `json:"tx_type,omitempty"`
}
GetLedgerTransactionsRequest queries ledger transactions with optional filters.
type GetLedgerTransactionsResponse ¶
type GetLedgerTransactionsResponse struct {
LedgerTransactions []LedgerTransaction `json:"ledger_transactions"`
}
GetLedgerTransactionsResponse contains the filtered ledger transactions.
type GetRPCHistoryRequest ¶
type GetRPCHistoryRequest struct {
ListOptions
}
GetRPCHistoryRequest queries the RPC call history for the authenticated user.
type GetRPCHistoryResponse ¶
type GetRPCHistoryResponse struct {
RPCEntries []HistoryEntry `json:"rpc_entries"`
}
GetRPCHistoryResponse contains the RPC call history entries.
type GetSessionKeysRequest ¶ added in v0.5.0
type GetSessionKeysRequest struct {
}
GetSessionKeysRequest queries for session keys associated with the authenticated wallet.
type GetSessionKeysResponse ¶ added in v0.5.0
type GetSessionKeysResponse struct {
SessionKeys []SessionKeyResponse `json:"session_keys"`
}
GetSessionKeysResponse contains the list of active session keys.
type GetUserTagResponse ¶
type GetUserTagResponse struct {
Tag string `json:"tag"`
}
GetUserTagResponse returns the human-readable tag for a wallet address.
type GorillaWsConnectionAdapter ¶ added in v0.4.0
type GorillaWsConnectionAdapter interface {
// ReadMessage reads a message from the WebSocket connection.
ReadMessage() (messageType int, p []byte, err error)
// NextWriter returns a writer for the next message to be sent on the WebSocket connection.
NextWriter(messageType int) (io.WriteCloser, error)
// Close closes the WebSocket connection.
Close() error
}
GorillaWsConnectionAdapter abstracts the methods of a WebSocket connection needed by WebsocketConnection.
type Handler ¶ added in v0.4.0
type Handler func(c *Context)
Handler defines the function signature for RPC request processors. Handlers receive a Context containing the request and all necessary information to process it. They can call c.Next() to delegate to the next handler in the middleware chain, enabling composable request processing pipelines.
type HandlerGroup ¶ added in v0.4.0
type HandlerGroup interface {
Handle(method string, handler Handler)
Use(middleware Handler)
NewGroup(name string) HandlerGroup
}
type HistoryEntry ¶
type HistoryEntry struct {
// ID is the unique entry identifier
ID uint `json:"id"`
// Sender is the wallet address that made the call
Sender string `json:"sender"`
// ReqID is the request identifier
ReqID uint64 `json:"req_id"`
// Method is the RPC method name
Method string `json:"method"`
// Params contains the request parameters (JSON)
Params string `json:"params"`
// Timestamp is when the call was made (Unix timestamp)
Timestamp uint64 `json:"timestamp"`
// ReqSig contains request signatures
ReqSig []sign.Signature `json:"req_sig"`
// Result contains the response data (JSON)
Result string `json:"response"`
// ResSig contains response signatures
ResSig []sign.Signature `json:"res_sig"`
}
HistoryEntry records an RPC method call and response with signatures.
type LedgerBalance ¶
type LedgerBalance struct {
// Asset is the token/asset symbol
Asset string `json:"asset"`
// Amount is the current balance
Amount decimal.Decimal `json:"amount"`
}
LedgerBalance represents an account balance for a specific asset.
type LedgerEntry ¶
type LedgerEntry struct {
// ID is the unique entry identifier
ID uint `json:"id"`
// AccountID identifies the ledger account
AccountID string `json:"account_id"`
// AccountType categorizes the account
AccountType AccountType `json:"account_type"`
// Asset is the token/asset symbol
Asset string `json:"asset"`
// Participant is the wallet address involved
Participant string `json:"participant"`
// Credit amount (increases liability/income/capital)
Credit decimal.Decimal `json:"credit"`
// Debit amount (increases expense/asset/drawing)
Debit decimal.Decimal `json:"debit"`
// CreatedAt is when the entry was recorded
CreatedAt time.Time `json:"created_at"`
}
LedgerEntry represents a double-entry bookkeeping entry. The system follows the DEADCLIC mnemonic for debit/credit rules: - DEAD: Debit to increase Expense, Asset, Drawing accounts - CLIC: Credit to increase Liability, Income, Capital accounts
type LedgerTransaction ¶
type LedgerTransaction struct {
// Id is the unique transaction identifier
Id uint `json:"id"`
// TxType categorizes the transaction
TxType string `json:"tx_type"`
// FromAccount is the source account
FromAccount string `json:"from_account"`
// FromAccountTag is the human-readable source tag
FromAccountTag string `json:"from_account_tag,omitempty"`
// ToAccount is the destination account
ToAccount string `json:"to_account"`
// ToAccountTag is the human-readable destination tag
ToAccountTag string `json:"to_account_tag,omitempty"`
// Asset is the token/asset symbol
Asset string `json:"asset"`
// Amount transferred
Amount decimal.Decimal `json:"amount"`
// CreatedAt is when the transaction occurred
CreatedAt time.Time `json:"created_at"`
}
LedgerTransaction represents a transfer between accounts.
type ListOptions ¶
type ListOptions struct {
// Offset is the number of items to skip (for pagination)
Offset uint32 `json:"offset,omitempty"`
// Limit is the maximum number of items to return
Limit uint32 `json:"limit,omitempty"`
// Sort specifies the sort order (asc/desc)
Sort *SortType `json:"sort,omitempty"`
}
ListOptions provides pagination and sorting for list endpoints.
type Method ¶
type Method string
Method represents an RPC method name that can be called on the ClearNode broker. These constants define all available methods in the RPC API.
const ( // PingMethod is a simple method to check connectivity and liveness. PingMethod Method = "ping" // PongMethod is the response to a ping request. PongMethod Method = "pong" // ErrorMethod is the identifier for error responses. ErrorMethod Method = "error" // GetConfigMethod returns the broker configuration and supported networks. GetConfigMethod Method = "get_config" // GetAssetsMethod returns the list of supported assets/tokens. GetAssetsMethod Method = "get_assets" // GetAppDefinitionMethod returns the definition of an application session. GetAppDefinitionMethod Method = "get_app_definition" // GetAppSessionsMethod returns a list of application sessions. GetAppSessionsMethod Method = "get_app_sessions" // GetChannelsMethod returns a list of payment channels. GetChannelsMethod Method = "get_channels" // GetLedgerEntriesMethod returns ledger entries (double-entry bookkeeping). GetLedgerEntriesMethod Method = "get_ledger_entries" // GetLedgerTransactionsMethod returns ledger transactions. GetLedgerTransactionsMethod Method = "get_ledger_transactions" // AuthRequestMethod initiates authentication with challenge generation. AuthRequestMethod Method = "auth_request" // AuthChallengeMethod is the response to an auth request with the challenge. AuthChallengeMethod Method = "auth_challenge" // AuthVerifyMethod verifies authentication via signature or JWT. AuthVerifyMethod Method = "auth_verify" // GetUserTagMethod returns the human-readable tag for a wallet (auth required). GetUserTagMethod Method = "get_user_tag" // GetLedgerBalancesMethod returns account balances (auth required). GetLedgerBalancesMethod Method = "get_ledger_balances" // GetRPCHistoryMethod returns RPC call history (auth required). GetRPCHistoryMethod Method = "get_rpc_history" // GetSessionKeysMethod returns session keys with allowances (auth required). GetSessionKeysMethod Method = "get_session_keys" // CreateChannelMethod creates a new payment channel (auth required). CreateChannelMethod Method = "create_channel" // ResizeChannelMethod resizes an existing channel (auth required). ResizeChannelMethod Method = "resize_channel" // CloseChannelMethod closes a payment channel (auth required). CloseChannelMethod Method = "close_channel" // TransferMethod transfers funds between accounts (auth required). TransferMethod Method = "transfer" // CreateAppSessionMethod creates a virtual application session (auth required). CreateAppSessionMethod Method = "create_app_session" // SubmitAppStateMethod updates application state (auth required). SubmitAppStateMethod Method = "submit_app_state" // CloseAppSessionMethod closes an application session (auth required). CloseAppSessionMethod Method = "close_app_session" // CleanupSessionKeyCacheMethod clears the session key cache (test mode only). CleanupSessionKeyCacheMethod Method = "cleanup_session_key_cache" )
type Node ¶ added in v0.4.0
type Node interface {
// Handle registers a handler function for a specific RPC method.
// When a request with the matching method name is received,
// the handler will be invoked with the request context.
Handle(method string, handler Handler)
// Notify sends a server-initiated notification to a specific user.
// All active connections for the user will receive the notification.
// If the user has no active connections, the notification is dropped.
Notify(userID string, method string, params Params)
// Use adds global middleware that will be executed for all requests.
// Middleware is executed in the order it was added, before any
// method-specific handlers.
Use(middleware Handler)
// NewGroup creates a new handler group for organizing related endpoints.
// Groups can have their own middleware and can be nested to create
// hierarchical handler structures.
NewGroup(name string) HandlerGroup
}
Node represents an RPC server that manages client connections and routes messages to appropriate handlers. It provides a foundation for building RPC-based services with support for middleware, authentication, and server-initiated notifications. The interface is transport-agnostic, allowing for different implementations (WebSocket, HTTP/2, etc.).
type Params ¶
type Params map[string]json.RawMessage
Params represents method-specific parameters as a map of JSON raw messages. This design allows maximum flexibility while maintaining type safety: - Parameters are stored as raw JSON until needed - The Translate method provides type-safe extraction into Go structs - Supports optional parameters and forward compatibility
Example usage:
// Creating params from a struct
params, _ := NewParams(TransferRequest{To: "0x123", Amount: "100"})
// Accessing individual parameters
var amount string
json.Unmarshal(params["amount"], &amount)
// Translating to a struct
var req TransferRequest
params.Translate(&req)
func NewErrorParams ¶
NewErrorParams creates a Params map containing an error message. This is a convenience function for creating standardized error parameters that follow the protocol's error response convention.
The error message is stored under the "error" key and properly JSON-encoded.
Example usage:
// Creating error params for a validation failure
errParams := NewErrorParams("invalid address format")
payload := NewPayload(requestID, method, errParams)
// Creating error params from an existing error
if err := validateAmount(amount); err != nil {
errParams := NewErrorParams(err.Error())
response := NewResponse(NewPayload(id, method, errParams))
}
The resulting Params will contain: {"error": "invalid address format"}
func NewParams ¶
NewParams creates a Params map from any JSON-serializable value. This is typically used with structs or maps to create method parameters.
The function works by: 1. Marshaling the input value to JSON 2. Unmarshaling it into a Params map 3. Each field becomes a key with its JSON representation as the value
Example:
type TransferRequest struct {
From string `json:"from"`
To string `json:"to"`
Amount string `json:"amount"`
}
req := TransferRequest{
From: "0x111...",
To: "0x222...",
Amount: "1000000000000000000",
}
params, err := NewParams(req)
// params now contains: {"from": "0x111...", "to": "0x222...", "amount": "1000000000000000000"}
Returns an error if the value cannot be marshaled to JSON or is not a valid object.
func (Params) Error ¶
Error extracts and returns an error from the Params if one exists. This method checks for the standard "error" key in the params and attempts to unmarshal its value as a string error message.
Returns:
- An error with the message if the "error" key exists and contains a valid string
- nil if no error key exists or if the value cannot be unmarshaled
This is typically used when processing response payloads to check for errors:
// In a client processing a response
if err := response.Res.Params.Error(); err != nil {
// The server returned an error
return fmt.Errorf("RPC error: %w", err)
}
// Process successful response
var result TransferResult
response.Res.Params.Translate(&result)
This method is designed to work with error params created by NewErrorParams.
func (Params) Translate ¶
Translate extracts the parameters into the provided value (typically a struct). This provides type-safe parameter extraction with automatic JSON unmarshaling.
The method works by: 1. Marshaling the Params map back to JSON 2. Unmarshaling that JSON into the target value 3. Go's JSON unmarshaling handles type conversion and validation
Example:
type BalanceRequest struct {
Address string `json:"address"`
Block string `json:"block,omitempty"`
}
// In an RPC handler:
var req BalanceRequest
if err := payload.Params.Translate(&req); err != nil {
return rpc.Errorf("invalid parameters: %v", err)
}
// req.Address and req.Block are now populated
The target value should be a pointer to the desired type. Returns an error if the parameters don't match the expected structure.
type Payload ¶
type Payload struct {
// RequestID is a unique identifier for tracking requests and matching responses.
// Clients should generate unique IDs to prevent collisions and enable proper
// request-response correlation.
RequestID uint64 `json:"request_id"`
// Method specifies the RPC method to be invoked (e.g., "wallet_transfer").
// Method names should follow a consistent naming convention, typically
// using lowercase with underscores (e.g., "module_action").
Method string `json:"method"`
// Params contains the method-specific parameters as a flexible map.
// This allows different methods to have different parameter structures
// while maintaining type safety through the Translate method.
Params Params `json:"params"`
// Timestamp is the Unix timestamp in milliseconds when the payload was created.
// This is used for replay protection and request expiration checks.
// Servers should validate that timestamps are within an acceptable time window.
Timestamp uint64 `json:"ts"`
}
Payload represents the core data structure for RPC communication. It contains all the information needed to process an RPC call or response.
Payloads are encoded as JSON arrays for compact transmission: [RequestID, Method, Params, Timestamp]
This encoding reduces message size while maintaining human readability and allows for efficient parsing. The array format is automatically handled by the custom JSON marshaling methods.
func NewPayload ¶
NewPayload creates a new Payload with the given request ID, method, and parameters. The timestamp is automatically set to the current time in Unix milliseconds.
Example:
params, _ := NewParams(map[string]string{"address": "0x123"})
payload := NewPayload(12345, "wallet_getBalance", params)
The resulting payload will have the current timestamp and can be used in either a Request or Response.
func (Payload) Hash ¶
Hash computes and returns the cryptographic hash of the payload. This hash is used for signature creation and verification.
The method works by: 1. Marshaling the payload to its JSON representation (compact array format) 2. Computing the Keccak256 hash of the JSON bytes
Returns:
- The 32-byte Keccak256 hash of the payload
- An error if JSON marshaling fails
This hash is deterministic - the same payload will always produce the same hash, which is essential for signature verification across different systems.
Example usage:
hash, err := payload.Hash()
if err != nil {
return fmt.Errorf("failed to hash payload: %w", err)
}
signature := sign.Sign(hash, privateKey)
func (Payload) MarshalJSON ¶
MarshalJSON implements the json.Marshaler interface for Payload. It always emits the compact array format: [RequestID, Method, Params, Timestamp]
This ensures consistent wire format regardless of how the Payload struct is modified in the future, maintaining protocol compatibility.
Example output:
[12345, "wallet_transfer", {"to": "0xabc", "amount": "100"}, 1634567890123]
func (*Payload) UnmarshalJSON ¶
UnmarshalJSON implements the json.Unmarshaler interface for Payload. It expects data in the compact array format: [RequestID, Method, Params, Timestamp]
This custom unmarshaling ensures backward compatibility with the array-based protocol format while providing a clean struct-based API for Go code.
The method validates that: - The input is a valid JSON array - The array contains exactly 4 elements - Each element has the correct type
Returns an error if the JSON format is invalid or any element has the wrong type.
type Request ¶
type Request struct {
// Req contains the request payload with method, parameters, and metadata
Req Payload `json:"req"`
// Sig contains one or more signatures authorizing this request.
// Multiple signatures enable multi-sig authorization scenarios.
Sig []sign.Signature `json:"sig"`
}
Request represents an RPC request message containing a payload and one or more signatures. The Request structure supports multi-signature scenarios where multiple parties need to authorize an operation. Signatures are typically created by signing the marshaled payload.
The JSON representation of a Request is:
{
"req": [requestId, method, params, timestamp],
"sig": [signature1, signature2, ...]
}
Where the "req" field contains the compactly encoded payload and "sig" contains an array of signatures authorizing the request.
func NewRequest ¶
NewRequest creates a new Request with the given payload and optional signatures. If no signatures are provided, the Sig field will be an empty slice, which typically means signatures will be added later by the transport layer.
Example usage:
// Create request without signatures (to be signed later) request := NewRequest(payload) // Create request with single signature request := NewRequest(payload, signature) // Create request with multiple signatures for multi-sig auth request := NewRequest(payload, sig1, sig2, sig3)
func (Request) GetSigners ¶
GetSigners recovers and returns the addresses of all signers from the request signatures. This method is used to identify which addresses authorized this request by recovering the public key addresses from the cryptographic signatures.
Returns:
- A slice of addresses, one for each signature in the request
- An error if signature recovery fails for any signature
Example usage:
// Verify request signers
addresses, err := request.GetSigners()
if err != nil {
return rpc.Errorf("invalid signatures: %v", err)
}
// Check if a specific address signed the request
for _, addr := range addresses {
if addr == authorizedAddress {
// Process the authorized request
}
}
type ResizeChannelRequest ¶
type ResizeChannelRequest struct {
// ChannelID identifies the channel to resize
ChannelID string `json:"channel_id" validate:"required"`
// AllocateAmount adds funds to the channel (mutually exclusive with ResizeAmount)
AllocateAmount *decimal.Decimal `json:"allocate_amount,omitempty" validate:"omitempty,required_without=ResizeAmount,bigint"`
// ResizeAmount sets the new total channel size (mutually exclusive with AllocateAmount)
ResizeAmount *decimal.Decimal `json:"resize_amount,omitempty" validate:"omitempty,required_without=AllocateAmount,bigint"`
// FundsDestination is where to send funds if reducing channel size
FundsDestination string `json:"funds_destination" validate:"required"`
}
ResizeChannelRequest modifies the funding in an existing channel.
type ResizeChannelResponse ¶
type ResizeChannelResponse ChannelOperationResponse
ResizeChannelResponse contains the updated channel state after resizing.
type Response ¶
type Response struct {
// Res contains the response payload with results or error information
Res Payload `json:"res"`
// Sig contains one or more signatures authenticating this response.
// This ensures clients can verify the response came from authorized servers.
Sig []sign.Signature `json:"sig"`
}
Response represents an RPC response message containing a payload and one or more signatures. The structure mirrors Request, allowing responses to be cryptographically signed by the server or multiple parties in a distributed system.
The JSON representation of a Response is:
{
"res": [requestId, method, params, timestamp],
"sig": [signature1, signature2, ...]
}
The response payload typically contains the same RequestID as the original request, allowing clients to match responses to their requests.
func NewErrorResponse ¶
NewErrorResponse creates a Response containing an error message. This is a convenience function that combines error parameter creation and response construction in a single call.
Parameters:
- requestID: The ID from the original request
- errMsg: The error message to send to the client
- sig: Optional signatures to authenticate the error response
Example usage:
// In an RPC handler when an error occurs
if err := validateRequest(request); err != nil {
return NewErrorResponse(request.Req.RequestID, err.Error(), serverSignature)
}
// Creating an error response without signatures
errorResponse := NewErrorResponse(12345, "insufficient balance")
The resulting response will have params in the format: {"error": "<errMsg>"}
func NewResponse ¶
NewResponse creates a new Response with the given payload and optional signatures. The payload should contain the same RequestID as the original request to enable request-response matching on the client side.
Example usage:
// Create response for a successful operation
resultParams, _ := NewParams(map[string]interface{}{"status": "success", "txHash": "0xabc"})
responsePayload := NewPayload(request.Req.RequestID, request.Req.Method, resultParams)
response := NewResponse(responsePayload, serverSignature)
// Create error response
errorParams, _ := NewParams(map[string]interface{}{"error": "insufficient funds"})
errorPayload := NewPayload(request.Req.RequestID, request.Req.Method, errorParams)
response := NewResponse(errorPayload, serverSignature)
func (Response) Error ¶
Error checks if the Response contains an error and returns it. This method extracts any error stored in the response payload's params under the standard "error" key.
Returns:
- An error if the response contains an error message
- nil if the response represents a successful operation
This is typically used by clients to check if an RPC call failed:
// After receiving and unmarshaling a response
var response Response
if err := json.Unmarshal(data, &response); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
// Check if the response contains an error
if err := response.Error(); err != nil {
return fmt.Errorf("RPC call failed: %w", err)
}
// Process successful response
var result TransferResult
response.Res.Params.Translate(&result)
This method is designed to work with error responses created by NewErrorResponse or any response where errors are stored using NewErrorParams.
func (Response) GetSigners ¶
GetSigners recovers and returns the addresses of all signers from the response signatures. This method allows clients to verify that responses came from authorized servers by recovering the public key addresses from the cryptographic signatures.
Returns:
- A slice of addresses, one for each signature in the response
- An error if signature recovery fails for any signature
Example usage:
// Verify response came from trusted server
addresses, err := response.GetSigners()
if err != nil {
return fmt.Errorf("invalid response signatures: %w", err)
}
if !contains(addresses, trustedServerAddress) {
return fmt.Errorf("response not from trusted server")
}
type RevokeSessionKeyRequest ¶ added in v0.5.0
type RevokeSessionKeyRequest struct {
// SessionKey is the address of the session key to revoke
SessionKey string `json:"session_key"`
}
RevokeSessionKeyRequest contains the parameters for revoking a session key.
type RevokeSessionKeyResponse ¶ added in v0.5.0
type RevokeSessionKeyResponse struct {
// SessionKey is the address of the revoked session key
SessionKey string `json:"session_key"`
}
RevokeSessionKeyResponse indicates successful revocation of a session key.
type SafeStorage ¶ added in v0.4.0
type SafeStorage struct {
// contains filtered or unexported fields
}
SafeStorage provides thread-safe key-value storage for connection-specific data. Each connection gets its own SafeStorage instance that persists for the connection's lifetime. This enables handlers to store and retrieve session state, authentication tokens, rate limiting counters, or any other per-connection data across multiple requests.
Common use cases:
- Storing authentication state and policies
- Caching frequently accessed data
- Maintaining request counters for rate limiting
- Storing connection-specific configuration
func NewSafeStorage ¶ added in v0.4.0
func NewSafeStorage() *SafeStorage
NewSafeStorage creates a new thread-safe storage instance. The storage starts empty and can be used immediately for storing connection-specific data.
func (*SafeStorage) Get ¶ added in v0.4.0
func (s *SafeStorage) Get(key string) (any, bool)
Get retrieves a value by key from the storage. Returns the value and true if the key exists, or nil and false if the key is not found. The caller must type-assert the returned value to the expected type.
Example:
if val, ok := storage.Get("auth_token"); ok {
token := val.(string)
// Use token...
}
func (*SafeStorage) Set ¶ added in v0.4.0
func (s *SafeStorage) Set(key string, value any)
Set stores a value with the given key in the storage. If the key already exists, its value is overwritten. The value can be of any type. This method is thread-safe and can be called concurrently from multiple goroutines.
Example:
storage.Set("auth_token", "bearer-xyz123")
storage.Set("rate_limit_count", 42)
storage.Set("user_preferences", userPrefs)
type SendResponseFunc ¶ added in v0.4.0
SendResponseFunc is a function type for sending server-initiated RPC notifications. Unlike regular responses that reply to client requests, these functions enable the server to push unsolicited messages to clients (e.g., balance updates, connection events). The method parameter specifies the notification type, and params contains the notification data.
type SessionKeyResponse ¶ added in v0.5.0
type SessionKeyResponse struct {
// ID is the internal database identifier
ID uint `json:"id"`
// SessionKey is the public key/address of the session key
SessionKey string `json:"session_key"`
// Application is the name or identifier of the application this key is for
Application string `json:"application"`
// Allowances contains spending limits per asset with usage tracking
Allowances []AllowanceUsage `json:"allowances"`
// Scope defines the permission scope for the session (e.g., "app.create", "ledger.readonly")
Scope string `json:"scope,omitempty"`
// ExpiresAt is when the session key expires
ExpiresAt time.Time `json:"expires_at"`
// CreatedAt is when the session key was created
CreatedAt time.Time `json:"created_at"`
}
SessionKeyResponse represents a single session key with its allowances and usage.
type StateAllocation ¶
type StateAllocation struct {
// Participant is the recipient wallet address
Participant string `json:"destination"`
// TokenAddress is the asset contract address
TokenAddress string `json:"token"`
// RawAmount allocated to the participant
RawAmount decimal.Decimal `json:"amount"`
}
StateAllocation defines asset distribution in a channel/app state.
type StateIntent ¶
type StateIntent uint8
StateIntent indicates the purpose of a state transition.
const ( // StateIntentOperate is for normal application operation StateIntentOperate StateIntent = 0 // StateIntentInitialize is for initial channel funding StateIntentInitialize StateIntent = 1 // StateIntentResize is for channel resize operations StateIntentResize StateIntent = 2 // StateIntentFinalize is for channel closure StateIntentFinalize StateIntent = 3 )
type SubmitAppStateRequest ¶
type SubmitAppStateRequest struct {
// AppSessionID identifies the session to update
AppSessionID string `json:"app_session_id"`
// Intent indicates the purpose of the state update (operate, deposit, withdraw)
// Required since protocol version: NitroRPC/0.4
Intent AppSessionIntent `json:"intent"`
// Version is the new state version number
// Required since protocol version: NitroRPC/0.4
Version uint64 `json:"version"`
// Allocations defines the new asset distribution
Allocations []AppAllocation `json:"allocations"`
// SessionData contains the new application state
SessionData *string `json:"session_data"`
}
SubmitAppStateRequest updates an application session's state.
type SubmitAppStateResponse ¶
type SubmitAppStateResponse AppSession
SubmitAppStateResponse contains the updated application session.
type TransferAllocation ¶
type TransferAllocation struct {
// AssetSymbol identifies the asset (e.g., "USDC")
AssetSymbol string `json:"asset"`
// Amount to transfer
Amount decimal.Decimal `json:"amount"`
}
TransferAllocation specifies an amount to transfer for a specific asset.
type TransferEventHandler ¶
type TransferEventHandler func(ctx context.Context, notif TransferNotification, resSig []sign.Signature)
TransferEventHandler processes transfer notifications from the server. These notifications are sent when transfers affect the authenticated user's account, including both incoming and outgoing transfers.
type TransferNotification ¶
type TransferNotification struct {
// Transactions contains the ledger transactions for the transfer
Transactions []LedgerTransaction `json:"transactions"`
}
TransferNotification is sent when a transfer affects the user's account. This includes both incoming and outgoing transfers.
type TransferRequest ¶
type TransferRequest struct {
// Destination is the recipient wallet address
Destination string `json:"destination"`
// DestinationUserTag is the recipient's human-readable tag
DestinationUserTag string `json:"destination_user_tag"`
// Allocations specifies amounts per asset to transfer
Allocations []TransferAllocation `json:"allocations"`
}
TransferRequest moves funds between accounts or to external addresses.
type TransferResponse ¶
type TransferResponse struct {
Transactions []LedgerTransaction `json:"transactions"`
}
TransferResponse contains the ledger transactions created by the transfer.
type UnsignedState ¶
type UnsignedState struct {
// Intent indicates the state's purpose
Intent StateIntent `json:"intent"`
// Version is the state sequence number
Version uint64 `json:"version"`
// Data contains application-specific state data
Data string `json:"state_data"`
// Allocations defines asset distribution
Allocations []StateAllocation `json:"allocations"`
}
UnsignedState represents a channel or application state before signatures.
func (*UnsignedState) Scan ¶
func (u *UnsignedState) Scan(value interface{}) error
Scan implements sql.Scanner interface for database retrieval.
type Version ¶ added in v0.4.0
type Version string
Version represents the protocol version. This is used to provide backward compatibility as the API evolves.
type WebsocketConnection ¶ added in v0.4.0
type WebsocketConnection struct {
// contains filtered or unexported fields
}
WebsocketConnection implements the Connection interface using WebSocket transport. It manages bidirectional communication, handles authentication state, and provides thread-safe operations for concurrent message processing. The connection supports graceful shutdown and automatic cleanup of resources.
Key features:
- Concurrent read/write operations with separate goroutines
- Configurable timeouts for write operations
- Buffered channels for message processing
- Thread-safe user authentication state management
- Graceful connection closure with proper resource cleanup
func NewWebsocketConnection ¶ added in v0.4.0
func NewWebsocketConnection(config WebsocketConnectionConfig) (*WebsocketConnection, error)
NewWebsocketConnection creates a new WebsocketConnection instance with the provided configuration. Returns an error if required fields (ConnectionID, WebsocketConn) are missing. Optional fields are set to sensible defaults if not provided.
func (*WebsocketConnection) ConnectionID ¶ added in v0.4.0
func (conn *WebsocketConnection) ConnectionID() string
ConnectionID returns the unique identifier for this connection.
func (*WebsocketConnection) RawRequests ¶ added in v0.4.0
func (conn *WebsocketConnection) RawRequests() <-chan []byte
RawRequests returns the channel for processing incoming requests.
func (*WebsocketConnection) Serve ¶ added in v0.4.0
func (conn *WebsocketConnection) Serve(parentCtx context.Context, handleClosure func(error))
Serve starts the connection's lifecycle by spawning concurrent goroutines. This method:
- Spawns three goroutines: one for reading messages, one for writing messages, and one for monitoring connection closure signals
- Returns immediately after starting the goroutines
- Spawns an additional goroutine that waits for all operations to complete and then invokes handleClosure with any error that occurred
The handleClosure callback is guaranteed to be called exactly once when the connection terminates. The method is idempotent - calling it multiple times will immediately invoke handleClosure without starting duplicate goroutines.
func (*WebsocketConnection) SetUserID ¶ added in v0.4.0
func (conn *WebsocketConnection) SetUserID(userID string)
SetUserID sets the UserID for this connection.
func (*WebsocketConnection) UserID ¶ added in v0.4.0
func (conn *WebsocketConnection) UserID() string
UserID returns the authenticated user's identifier for this connection.
func (*WebsocketConnection) WriteRawResponse ¶ added in v0.4.0
func (conn *WebsocketConnection) WriteRawResponse(message []byte) bool
WriteRawResponse attempts to queue a message for sending to the client. The method uses a timeout to prevent blocking on unresponsive clients. If the message cannot be queued within the timeout duration:
- The method returns false
- A close signal is sent to trigger connection shutdown
- This prevents resource exhaustion from slow or disconnected clients
Returns true if the message was successfully queued for sending.
type WebsocketConnectionConfig ¶ added in v0.4.0
type WebsocketConnectionConfig struct {
// ConnectionID is the unique identifier for this connection (required)
ConnectionID string
// UserID is the initial authenticated user ID (optional, can be set later)
UserID string
// WebsocketConn is the underlying WebSocket connection (required)
WebsocketConn GorillaWsConnectionAdapter
// WriteTimeout is the maximum duration to wait for a write operation (default: 5s)
WriteTimeout time.Duration
// WriteBufferSize is the capacity of the outgoing message buffer (default: 10)
WriteBufferSize int
// ProcessBufferSize is the capacity of the incoming message buffer (default: 10)
ProcessBufferSize int
// Logger for connection events (default: no-op logger)
Logger log.Logger
// OnMessageSentHandler is called after a message is successfully sent (optional)
OnMessageSentHandler func([]byte)
}
WebsocketConnectionConfig contains configuration options for creating a new WebsocketConnection. All fields except ConnectionID and WebsocketConn have sensible defaults.
type WebsocketDialer ¶
type WebsocketDialer struct {
// contains filtered or unexported fields
}
WebsocketDialer implements the Dialer interface using WebSocket connections. It provides thread-safe RPC communication with automatic ping/pong handling.
func NewWebsocketDialer ¶
func NewWebsocketDialer(cfg WebsocketDialerConfig) *WebsocketDialer
NewWebsocketDialer creates a new WebSocket dialer with the given configuration
func (*WebsocketDialer) Call ¶
Call sends an RPC request and waits for a response. The request must have a unique RequestID that identifies this call. The method is thread-safe and can be called concurrently.
The context can be used to set a timeout for the request:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := dialer.Call(ctx, request)
func (*WebsocketDialer) Dial ¶
func (d *WebsocketDialer) Dial(parentCtx context.Context, url string, handleClosure func(err error)) error
Dial establishes a WebSocket connection to the specified URL. This method blocks until the connection is closed, so it should typically be called in a goroutine. It starts three background goroutines: - One to handle context cancellation - One to read and route incoming messages - One to send periodic ping messages
Example:
dialer := NewWebsocketDialer(DefaultWebsocketDialerConfig)
go dialer.Dial(ctx, "ws://localhost:8080/ws", func(err error) {
if err != nil {
log.Error("Connection closed", "error", err)
}
})
func (*WebsocketDialer) EventCh ¶
func (d *WebsocketDialer) EventCh() <-chan *Response
EventCh returns a read-only channel for receiving unsolicited events. Events are responses that don't match any pending request ID. The channel will receive nil when the connection is closed.
Example:
for event := range dialer.EventCh() {
if event == nil {
// Connection closed
break
}
// Handle event
log.Info("Received event", "method", event.Res.Method)
}
func (*WebsocketDialer) IsConnected ¶
func (d *WebsocketDialer) IsConnected() bool
IsConnected returns true if the dialer has an active connection
type WebsocketDialerConfig ¶
type WebsocketDialerConfig struct {
// HandshakeTimeout is the duration to wait for the WebSocket handshake to complete
HandshakeTimeout time.Duration
// PingInterval is how often to send ping messages to keep the connection alive
PingInterval time.Duration
// PingRequestID is the request ID used for ping messages
// This should be a reserved ID that won't conflict with regular requests
PingRequestID uint64
// EventChanSize is the buffer size for the event channel
// A larger buffer prevents blocking when processing many unsolicited events
EventChanSize int
}
WebsocketDialerConfig contains configuration options for the WebSocket dialer
type WebsocketHandlerGroup ¶ added in v0.4.0
type WebsocketHandlerGroup struct {
// contains filtered or unexported fields
}
WebsocketHandlerGroup implements the HandlerGroup interface for organizing related handlers with shared middleware. Groups support nesting, allowing for hierarchical organization of endpoints with inherited middleware chains.
When a request matches a handler in a group, the middleware chain is:
- Global middleware (from Node.Use)
- Parent group middleware (if nested)
- This group's middleware
- The method handler
This enables fine-grained control over request processing pipelines.
func (*WebsocketHandlerGroup) Handle ¶ added in v0.4.0
func (hg *WebsocketHandlerGroup) Handle(method string, handler Handler)
Handle registers a handler for the specified RPC method within this group. The handler will execute after all applicable middleware:
- Global middleware
- Parent group middleware (for nested groups)
- This group's middleware
The method parameter must be unique across the entire node.
func (*WebsocketHandlerGroup) NewGroup ¶ added in v0.4.0
func (hg *WebsocketHandlerGroup) NewGroup(name string) HandlerGroup
NewGroup creates a nested handler group within this group. The nested group inherits the middleware chain from all parent groups, allowing for progressive middleware application.
Example:
api := node.NewGroup("api")
api.Use(apiVersionMiddleware)
v1 := api.NewGroup("v1")
v1.Use(v1AuthMiddleware)
// Handlers in v1 group will execute: global → api → v1 middleware
func (*WebsocketHandlerGroup) Use ¶ added in v0.4.0
func (hg *WebsocketHandlerGroup) Use(middleware Handler)
Use adds middleware to this handler group. The middleware will execute for all handlers in this group and any nested groups. Middleware is executed in the order it was added.
Panics if middleware is nil.
type WebsocketNode ¶ added in v0.4.0
type WebsocketNode struct {
// contains filtered or unexported fields
}
WebsocketNode implements the Node interface using WebSocket as the transport layer. It provides a complete RPC server implementation with the following features:
- WebSocket connection management with automatic cleanup
- Request routing based on method names
- Middleware support at global and group levels
- Cryptographic signing of all responses
- Connection authentication and re-authentication
- Server-initiated notifications to specific users
- Configurable timeouts and buffer sizes
- Structured logging for debugging and monitoring
The node automatically handles connection lifecycle, including:
- WebSocket protocol upgrade
- Concurrent message processing
- Graceful connection shutdown
- Resource cleanup on disconnection
func NewWebsocketNode ¶ added in v0.4.0
func NewWebsocketNode(config WebsocketNodeConfig) (*WebsocketNode, error)
NewWebsocketNode creates a new WebsocketNode instance with the provided configuration. The node is ready to accept WebSocket connections after creation.
Required configuration:
- Signer: Used to sign all outgoing messages
- Logger: Used for structured logging
The node automatically registers a built-in "ping" handler that responds with "pong".
Returns an error if required configuration is missing.
func (*WebsocketNode) Handle ¶ added in v0.4.0
func (wn *WebsocketNode) Handle(method string, handler Handler)
Handle registers a handler function for the specified RPC method. When a request with a matching method name is received, the handler will be invoked with a Context containing the request information.
The handler executes after all global middleware registered with Use().
Panics if:
- method is empty
- handler is nil
func (*WebsocketNode) NewGroup ¶ added in v0.4.0
func (wn *WebsocketNode) NewGroup(name string) HandlerGroup
NewGroup creates a new handler group with the specified name. Groups provide a way to organize related handlers and apply common middleware. Groups can be nested to create hierarchical structures.
Example:
// Create a group for authenticated endpoints
privateGroup := node.NewGroup("private")
privateGroup.Use(authMiddleware)
privateGroup.Handle("get_balance", handleGetBalance)
// Create a nested group with additional middleware
adminGroup := privateGroup.NewGroup("admin")
adminGroup.Use(adminAuthMiddleware)
adminGroup.Handle("manage_users", handleManageUsers)
func (*WebsocketNode) Notify ¶ added in v0.4.0
func (wn *WebsocketNode) Notify(userID, method string, params Params)
Notify sends a server-initiated notification to all connections of a specific user. This enables the server to push updates to clients without a prior request. Common use cases include:
- Balance updates after transactions
- Status changes in long-running operations
- Real-time notifications for user events
The notification is sent to all active connections for the user. If the user has no active connections, the notification is silently dropped.
Notifications have RequestID=0 to distinguish them from responses.
func (*WebsocketNode) ServeHTTP ¶ added in v0.4.0
func (wn *WebsocketNode) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler, making the node compatible with standard HTTP servers. This method:
- Upgrades incoming HTTP requests to WebSocket connections
- Creates a unique connection ID and manages connection state
- Spawns goroutines for concurrent message processing
- Invokes lifecycle callbacks (OnConnect, OnDisconnect, etc.)
- Blocks until the connection is closed
Each connection runs independently with its own goroutines for:
- Reading incoming messages
- Processing requests and routing to handlers
- Writing outgoing responses
- Monitoring connection health
The method ensures proper cleanup when connections close, including removing the connection from the hub and invoking disconnect callbacks.
func (*WebsocketNode) Use ¶ added in v0.4.0
func (wn *WebsocketNode) Use(middleware Handler)
Use adds global middleware that executes for all requests. Middleware is executed in the order it was registered, before any method-specific handlers. Common middleware includes:
- Authentication checks
- Request logging
- Rate limiting
- Request validation
Example:
node.Use(loggingMiddleware) node.Use(rateLimitMiddleware) node.Use(authMiddleware)
type WebsocketNodeConfig ¶ added in v0.4.0
type WebsocketNodeConfig struct {
// Signer is used to sign all outgoing messages (required).
// This ensures message authenticity and integrity.
Signer sign.Signer
// Logger is used for structured logging throughout the node (required).
Logger log.Logger
// OnConnectHandler is called when a new WebSocket connection is established.
// It receives a send function for pushing notifications to the new connection.
OnConnectHandler func(send SendResponseFunc)
// OnDisconnectHandler is called when a WebSocket connection is closed.
// It receives the UserID if the connection was authenticated.
OnDisconnectHandler func(userID string)
// OnMessageSentHandler is called after a message is successfully sent to a client.
// Useful for metrics and debugging.
OnMessageSentHandler func([]byte)
// OnAuthenticatedHandler is called when a connection successfully authenticates
// or re-authenticates with a different user.
OnAuthenticatedHandler func(userID string, send SendResponseFunc)
// WsUpgraderReadBufferSize sets the read buffer size for the WebSocket upgrader (default: 1024).
WsUpgraderReadBufferSize int
// WsUpgraderWriteBufferSize sets the write buffer size for the WebSocket upgrader (default: 1024).
WsUpgraderWriteBufferSize int
// WsUpgraderCheckOrigin validates the origin of incoming WebSocket requests.
// Default allows all origins; implement this for CORS protection.
WsUpgraderCheckOrigin func(r *http.Request) bool
// WsConnWriteTimeout is the maximum time to wait for a write operation (default: 5s).
// Connections that exceed this timeout are considered unresponsive and closed.
WsConnWriteTimeout time.Duration
// WsConnWriteBufferSize is the capacity of each connection's outgoing message queue (default: 10).
WsConnWriteBufferSize int
// WsConnProcessBufferSize is the capacity of each connection's incoming message queue (default: 10).
WsConnProcessBufferSize int
}
WebsocketNodeConfig contains all configuration options for creating a WebsocketNode. Required fields are Signer and Logger; all others have sensible defaults.