From 8e3d99e4fbab74669676857253de7c50f84e5f84 Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Thu, 12 Jun 2025 13:46:43 +0300 Subject: [PATCH] feat: update docs with parsing --- docs/quick_start/application_session.md | 29 +-- docs/quick_start/balances.md | 58 +++--- docs/quick_start/close_session.md | 28 +-- docs/quick_start/connect_to_the_clearnode.md | 182 +++++++++++-------- 4 files changed, 160 insertions(+), 137 deletions(-) diff --git a/docs/quick_start/application_session.md b/docs/quick_start/application_session.md index d60cb62..08f1e1e 100644 --- a/docs/quick_start/application_session.md +++ b/docs/quick_start/application_session.md @@ -38,17 +38,18 @@ To create an application session, you'll use the `createAppSessionMessage` helpe ```javascript -import { createAppSessionMessage } from '@erc7824/nitrolite'; +import { createAppSessionMessage, parseRPCResponse, MessageSigner, CreateAppSessionRPCParams } from '@erc7824/nitrolite'; import { useCallback } from 'react'; +import { Address } from 'viem'; function useCreateApplicationSession() { const createApplicationSession = useCallback( async ( - signer, - sendRequest, - participantA, - participantB, - amount, + signer: MessageSigner, + sendRequest: (message: string) => Promise, + participantA: Address, + participantB: Address, + amount: string, ) => { try { // Define the application parameters @@ -77,7 +78,7 @@ function useCreateApplicationSession() { // Create a signed message using the createAppSessionMessage helper const signedMessage = await createAppSessionMessage( - signer.sign, + signer, [ { definition: appDefinition, @@ -90,10 +91,10 @@ function useCreateApplicationSession() { const response = await sendRequest(signedMessage); // Handle the response - if (response && response[0] && response[0].app_session_id) { + if (response.app_session_id) { // Store the app session ID for future reference - localStorage.setItem('app_session_id', response[0].app_session_id); - return { success: true, app_session_id: response[0].app_session_id, response }; + localStorage.setItem('app_session_id', response.app_session_id); + return { success: true, app_session_id: response.app_session_id, response }; } else { return { success: true, response }; } @@ -119,15 +120,15 @@ function MyComponent() { const handleCreateSession = async () => { // Define your WebSocket send wrapper - const sendRequest = async (payload) => { + const sendRequest = async (payload: string) => { return new Promise((resolve, reject) => { // Assuming ws is your WebSocket connection const handleMessage = (event) => { try { - const message = JSON.parse(event.data); - if (message.res && message.res[1] === 'create_app_session') { + const message = parseRPCResponse(event.data); + if (message.method === RPCMethod.CreateAppSession) { ws.removeEventListener('message', handleMessage); - resolve(message.res[2]); + resolve(message.params); } } catch (error) { console.error('Error parsing message:', error); diff --git a/docs/quick_start/balances.md b/docs/quick_start/balances.md index e4532fc..5245797 100644 --- a/docs/quick_start/balances.md +++ b/docs/quick_start/balances.md @@ -57,7 +57,7 @@ To retrieve these balances, use the `get_ledger_balances` request with the Clear ```javascript -import { createGetLedgerBalancesMessage } from '@erc7824/nitrolite'; +import { createGetLedgerBalancesMessage, parseRPCMessage, RPCMethod } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; // Your message signer function (same as in auth flow) @@ -74,15 +74,15 @@ async function getLedgerBalances(ws, participant) { return new Promise((resolve, reject) => { // Create a unique handler for this specific request const handleMessage = (event) => { - const message = JSON.parse(event.data); + const message = parseRPCMessage(event.data); // Check if this is a response to our get_ledger_balances request - if (message.res && message.res[1] === 'get_ledger_balances') { + if (message.method === RPCMethod.GetLedgerBalances) { // Remove the message handler to avoid memory leaks ws.removeEventListener('message', handleMessage); // Resolve with the balances data - resolve(message.res[2]); + resolve(message.params); } }; @@ -124,16 +124,14 @@ try { // ] // Process your balances - if (balances[0] && balances[0].length > 0) { - const balanceList = balances[0]; // Array of balance entries by asset - + if (balances.length > 0) { // Display each asset balance - balanceList.forEach(balance => { + balances.forEach(balance => { console.log(`${balance.asset.toUpperCase()} balance: ${balance.amount}`); }); // Example: find a specific asset - const usdcBalance = balanceList.find(b => b.asset.toLowerCase() === 'usdc'); + const usdcBalance = balances.find(b => b.asset.toLowerCase() === 'usdc'); if (usdcBalance) { console.log(`USDC balance: ${usdcBalance.amount}`); } @@ -150,12 +148,12 @@ try { ```javascript import { ethers } from 'ethers'; -import { generateRequestId, getCurrentTimestamp } from '@erc7824/nitrolite'; +import { generateRequestId, getCurrentTimestamp, generateRequestId, parseRPCMessage, RPCMethod } from '@erc7824/nitrolite'; // Function to create a signed ledger balances request async function createLedgerBalancesRequest(signer, participant) { const requestId = generateRequestId(); - const method = 'get_ledger_balances'; + const method = RPCMethod.GetLedgerBalances; // Use the RPC method enum for clarity const params = [{ participant }]; // Note: updated parameter name to 'participant' const timestamp = getCurrentTimestamp(); @@ -183,18 +181,17 @@ async function getLedgerBalances(ws, participant, signer) { // Set up message handler const handleMessage = (event) => { try { - const message = JSON.parse(event.data); + const message = parseRPCMessage(event.data); // Check if this is our response - if (message.res && - message.res[0] === requestId && - message.res[1] === 'get_ledger_balances') { + if (message.requestId === requestId && + message.method === RPCMethod.GetLedgerBalances) { // Remove the listener ws.removeEventListener('message', handleMessage); // Resolve with the balances data - resolve(message.res[2]); + resolve(message.params); } } catch (error) { console.error('Error parsing message:', error); @@ -240,7 +237,7 @@ try { To retrieve off-chain balances for a participant, use the `createGetLedgerBalancesMessage` helper function: ```javascript -import { createGetLedgerBalancesMessage } from '@erc7824/nitrolite'; +import { createGetLedgerBalancesMessage, parseRPCResponse, RPCMethod } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; // Function to get ledger balances for a participant @@ -249,15 +246,15 @@ async function getLedgerBalances(ws, participant, messageSigner) { // Message handler for the response const handleMessage = (event) => { try { - const message = JSON.parse(event.data); + const message = parseRPCResponse(event.data); // Check if this is a response to our get_ledger_balances request - if (message.res && message.res[1] === 'get_ledger_balances') { + if (message.method === RPCMethod.GetLedgerBalances) { // Clean up by removing the event listener ws.removeEventListener('message', handleMessage); // Resolve with the balance data - resolve(message.res[2]); + resolve(message.params); } } catch (error) { console.error('Error parsing message:', error); @@ -294,11 +291,9 @@ getLedgerBalances(ws, participantAddress, messageSigner) console.log('Channel balances:', balances); // Process and display your balances - if (balances[0] && balances[0].length > 0) { - const balanceList = balances[0]; // Array of balance entries by asset - + if (balances.length > 0) { console.log('My balances:'); - balanceList.forEach(balance => { + balances.forEach(balance => { console.log(`- ${balance.asset.toUpperCase()}: ${balance.amount}`); }); } else { @@ -317,15 +312,8 @@ When you receive balance data from the ClearNode, it's helpful to format it for ```javascript // Simple function to format your balance data for display function formatMyBalances(balances) { - if (!balances || !balances[0] || !Array.isArray(balances[0]) || balances[0].length === 0) { - return null; // No balance data available - } - - // Extract your balances from the nested structure - const balanceList = balances[0]; // Array of balance entries by asset - // Return formatted balance information - return balanceList.map(balance => ({ + return balances.map(balance => ({ asset: balance.asset.toUpperCase(), amount: balance.amount, // You can add additional formatting here if needed @@ -378,11 +366,9 @@ function displayBalances(balances) { console.log(`Balance update at ${new Date().toLocaleTimeString()}:`); // Format and display your balances - if (balances[0] && balances[0].length > 0) { - const balanceList = balances[0]; // Array of balance entries by asset - + if (balances.length > 0) { console.log('My balances:'); - balanceList.forEach(balance => { + balances.forEach(balance => { console.log(`- ${balance.asset.toUpperCase()}: ${balance.amount}`); }); } else { diff --git a/docs/quick_start/close_session.md b/docs/quick_start/close_session.md index 2b743e6..d933fb1 100644 --- a/docs/quick_start/close_session.md +++ b/docs/quick_start/close_session.md @@ -39,7 +39,7 @@ To close an application session, you'll use the `createCloseAppSessionMessage` h ```javascript import { useCallback } from 'react'; -import { createCloseAppSessionMessage } from '@erc7824/nitrolite'; +import { createCloseAppSessionMessage, parseRPCResponse, MessageSigner, CloseAppSessionRPCParams } from '@erc7824/nitrolite'; /** * Hook for closing an application session @@ -47,12 +47,12 @@ import { createCloseAppSessionMessage } from '@erc7824/nitrolite'; function useCloseApplicationSession() { const closeApplicationSession = useCallback( async ( - signer, - sendRequest, - appId, - participantA, - participantB, - amount + signer: MessageSigner, + sendRequest: (message: string) => Promise, + appId: string, + participantA: Address, + participantB: Address, + amount: string ) => { try { if (!appId) { @@ -81,7 +81,7 @@ function useCloseApplicationSession() { // Create the signed message const signedMessage = await createCloseAppSessionMessage( - signer.sign, + signer, [closeRequest] ); @@ -89,13 +89,13 @@ function useCloseApplicationSession() { const response = await sendRequest(signedMessage); // Check for success - if (response && response[0] && response[0].app_session_id) { + if (response.app_session_id) { // Clean up local storage localStorage.removeItem('app_session_id'); return { success: true, - app_id: response[0].app_session_id, - status: response[0].status || "closed", + app_id: response.app_session_id, + status: response.status || "closed", response }; } else { @@ -128,10 +128,10 @@ function MyComponent() { // Assuming ws is your WebSocket connection const handleMessage = (event) => { try { - const message = JSON.parse(event.data); - if (message.res && message.res[1] === 'close_app_session') { + const message = parseRPCResponse(event.data); + if (message.method === RPCMethod.CloseAppSession) { ws.removeEventListener('message', handleMessage); - resolve(message.res[2]); + resolve(message.params); } } catch (error) { console.error('Error parsing message:', error); diff --git a/docs/quick_start/connect_to_the_clearnode.md b/docs/quick_start/connect_to_the_clearnode.md index ff3b203..4a410ff 100644 --- a/docs/quick_start/connect_to_the_clearnode.md +++ b/docs/quick_start/connect_to_the_clearnode.md @@ -274,24 +274,25 @@ import { createAuthVerifyMessage, createEIP712AuthMessageSigner, parseRPCResponse, + RPCMethod, } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; +// Create and send auth_request +const authRequestMsg = await createAuthRequestMessage({ + wallet: '0xYourWalletAddress', + participant: '0xYourSignerAddress', + app_name: 'Your Domain', + expire: Math.floor(Date.now() / 1000) + 3600, // 1 hour expiration + scope: 'console', + application: '0xYourApplicationAddress', + allowances: [], +}); + // After WebSocket connection is established ws.onopen = async () => { console.log('WebSocket connection established'); - // Create and send auth_request - const authRequestMsg = await createAuthRequestMessage({ - wallet: '0xYourWalletAddress', - participant: '0xYourSignerAddress', - app_name: 'Your Domain', - expire: Math.floor(Date.now() / 1000) + 3600, // 1 hour expiration - scope: 'console', - application: '0xYourApplicationAddress', - allowances: [], - }); - ws.send(authRequestMsg); }; @@ -301,44 +302,49 @@ ws.onmessage = async (event) => { const message = parseRPCResponse(event.data); // Handle auth_challenge response - if (message.method === 'auth_challenge') { - console.log('Received auth challenge'); - - // Create EIP-712 message signer function - const eip712MessageSigner = createEIP712AuthMessageSigner( - walletClient, // Your wallet client instance - { - // EIP-712 message structure, data should match auth_request - scope: authRequestMsg.scope, - application: authRequestMsg.application, - participant: authRequestMsg.participant, - expire: authRequestMsg.expire, - allowances: authRequestMsg.allowances, - }, - { - // Domain for EIP-712 signing - name: 'Your Domain', - }, - ) - - // Create and send auth_verify with signed challenge - const authVerifyMsg = await createAuthVerifyMessage( - eip712MessageSigner, // Our custom eip712 signer function - event.data, // Raw challenge response from ClearNode - walletAddress // Client address (same as in auth_request) - ); - - ws.send(authVerifyMsg); - } - // Handle auth_success or auth_failure - else if (message.method === 'auth_verify' && message.params.success) { - console.log('Authentication successful'); - // Now you can start using the channel + switch (message.method) { + case RPCMethod.AuthChallenge: + console.log('Received auth challenge'); - window.localStorage.setItem('clearnode_jwt', message.params.jwtToken); // Store JWT token for future use - } - else if (message.method === 'error') { - console.error('Authentication failed:', message.params.error); + // Create EIP-712 message signer function + const eip712MessageSigner = createEIP712AuthMessageSigner( + walletClient, // Your wallet client instance + { + // EIP-712 message structure, data should match auth_request + scope: authRequestMsg.scope, + application: authRequestMsg.application, + participant: authRequestMsg.participant, + expire: authRequestMsg.expire, + allowances: authRequestMsg.allowances, + }, + { + // Domain for EIP-712 signing + name: 'Your Domain', + }, + ) + + // Create and send auth_verify with signed challenge + const authVerifyMsg = await createAuthVerifyMessage( + eip712MessageSigner, // Our custom eip712 signer function + message, + ); + + ws.send(authVerifyMsg); + break; + // Handle auth_success or auth_failure + case RPCMethod.AuthVerify: + if (!message.params.success) { + console.log('Authentication failed'); + return; + } + console.log('Authentication successful'); + // Now you can start using the channel + + window.localStorage.setItem('clearnode_jwt', message.params.jwtToken); // Store JWT token for future use + break; + case RPCMethod.Error: { + console.error('Authentication failed:', message.params.error); + } } } catch (error) { console.error('Error handling message:', error); @@ -357,6 +363,7 @@ import { createGetConfigMessage, createEIP712AuthMessageSigner, parseRPCResponse, + RPCMethod, } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; @@ -379,7 +386,7 @@ ws.onmessage = async (event) => { try { const message = parseRPCResponse(event.data); - if (message.method === 'auth_challenge') { + if (message.method === RPCMethod.AuthChallenge) { // Extract the challenge manually from the response if ( message.params.challengeMessage @@ -424,7 +431,7 @@ ws.onmessage = async (event) => { ```javascript -import { createAuthVerifyMessageWithJWT, parseRPCResponse } from '@erc7824/nitrolite'; +import { createAuthVerifyMessageWithJWT, parseRPCResponse, RPCMethod } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; // After WebSocket connection is established @@ -447,13 +454,17 @@ ws.onmessage = async (event) => { try { const message = parseRPCResponse(event.data); - // Handle auth_success or auth_failure - if (message.method === 'auth_verify' && message.params.success) { - console.log('Authentication successful'); - // Now you can start using the channel - } - else if (message.method === 'error') { - console.error('Authentication failed:', message.params.error); + // Handle auth_success or auth_failure + switch (message.method) { + case RPCMethod.AuthVerify: + if (message.params.success) { + console.log('Authentication successful'); + // Now you can start using the channel + } + break; + case RPCMethod.Error: + console.error('Authentication failed:', message.params.error); + break; } } catch (error) { console.error('Error handling message:', error); @@ -507,34 +518,59 @@ The format of the EIP-712 message is as follows: } ``` -## Getting Channel Information +### Message Signer + +In methods that require signing messages, that are not part of the authentication flow, you should use a custom message signer function `MessageSigner`. This function takes the payload and returns a signed message that can be sent to the ClearNode using ECDSA signature. + +There are also, several things to consider: this method SHOULD sign plain JSON payloads and NOT [ERC-191](https://eips.ethereum.org/EIPS/eip-191) data, because it allows signatures to be compatible with non-EVM chains. Since most of the libraries, like `ethers` or `viem`, use EIP-191 by default, you will need to overwrite the default behavior to sign plain JSON payloads. +The other thing to consider is that providing an EOA private key directly in the code is not recommended for production applications. Instead, we are recommending to generate session keys -- temporary keys that are used for signing messages during the session. This way, you can avoid exposing your main wallet's private key and reduce the risk of compromising your funds. + +The simpliest implementation of a message signer function looks like this: + +> **Warning** +> For this example use `ethers` library version `5.7.2`. The `ethers` library version `6.x` has breaking changes that are not allowed in this example. +```javascript +import { MessageSigner, RequestData, ResponsePayload } from '@erc7824/nitrolite'; +import { ethers } from 'ethers'; +import { Hex } from 'viem'; + +const messageSigner = async (payload: RequestData | ResponsePayload): Promise => { + try { + const wallet = new ethers.Wallet('0xYourPrivateKey'); + + const messageBytes = ethers.utils.arrayify(ethers.utils.id(JSON.stringify(payload))); + + const flatSignature = await wallet._signingKey().signDigest(messageBytes); + + const signature = ethers.utils.joinSignature(flatSignature); + + return signature as Hex; + } catch (error) { + console.error('Error signing message:', error); + throw error; + } +} +``` + +## Getting Channel Information +Так After authenticating with a ClearNode, you can request information about your channels. This is useful to verify your connection is working correctly and to retrieve channel data. ```javascript -import { createGetChannelsMessage, parseRPCResponse } from '@erc7824/nitrolite'; +import { createGetChannelsMessage, parseRPCResponse, RPCMethod } from '@erc7824/nitrolite'; // Example of using the function after authentication is complete ws.addEventListener('message', async (event) => { const message = parseRPCResponse(event.data); // Check if this is a successful authentication message - if (message.method === 'auth_verify' && message.params.success) { + if (message.method === RPCMethod.AuthVerify && message.params.success) { console.log('Successfully authenticated, requesting channel information...'); - // Create a custom message signer function if you don't already have one - const messageSigner = async (payload) => { - // This is the same message signer function used in authentication - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = client.stateWalletClient.wallet.signingKey.sign(messageBytes); - return signature; - }; - // Request channel information using the built-in helper function const getChannelsMsg = await createGetChannelsMessage( - messageSigner, + messageSigner, // Provide message signer function from previous example client.stateWalletClient.account.address ); @@ -542,9 +578,9 @@ ws.addEventListener('message', async (event) => { } // Handle get_channels response - if (message.res && message.res[1] === 'get_channels') { + if (message.method === RPCMethod.GetChannels) { console.log('Received channels information:'); - const channelsList = message.res[2][0]; // Note the response format has changed + const channelsList = message.params; if (channelsList && channelsList.length > 0) { channelsList.forEach((channel, index) => {