From 3d74c253f9960efe9427e6cfc3eed1744c828d46 Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Wed, 28 May 2025 18:11:12 +0300 Subject: [PATCH 1/3] feat: update connect section with JWT param --- docs/quick_start/connect_to_the_clearnode.md | 72 +++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/quick_start/connect_to_the_clearnode.md b/docs/quick_start/connect_to_the_clearnode.md index 740beef..8f1e8b9 100644 --- a/docs/quick_start/connect_to_the_clearnode.md +++ b/docs/quick_start/connect_to_the_clearnode.md @@ -228,10 +228,11 @@ clearNodeConnection.disconnect(); When connecting to a ClearNode, you need to follow a specific authentication flow using the NitroliteRPC utility to create properly formatted and signed messages: 1. **Initial Connection**: The client establishes a WebSocket connection to the ClearNode's URL -2. **Auth Request**: The client sends an `auth_request` message with its identity information +2. **Auth Request**: On the first connection client sends an `auth_request` message with its identity information 3. **Challenge**: The ClearNode responds with an `auth_challenge` containing a random nonce 4. **Signature Verification**: The client signs the challenge using its state wallet and sends an `auth_verify` message 5. **Auth Result**: The ClearNode verifies the signature and responds with `auth_success` or `auth_failure` +6. **Reconnection**: On success ClearNode will return the JWT Token, which can be used for subsequent reconnections without needing to re-authenticate. This flow ensures that only authorized participants with valid signing keys can connect to the ClearNode and participate in channel operations. @@ -331,6 +332,8 @@ ws.onmessage = async (event) => { else if (message.res && message.res[1] === 'auth_success') { console.log('Authentication successful'); // Now you can start using the channel + + window.localStorage.setItem('clearnode_jwt', message.res[2]["jwt_token"]); // Store JWT token for future use } else if (message.res && message.res[1] === 'auth_failure') { console.error('Authentication failed:', message.res[2]); @@ -415,6 +418,73 @@ ws.onmessage = async (event) => { console.error('Error handling message:', error); } }; +``` + + + + +```javascript +import { createAuthVerifyMessageWithJWT } from '@erc7824/nitrolite'; +import { ethers } from 'ethers'; + +// Important: Custom message signer that correctly hashes the message with ethers.id +// This is crucial for proper ClearNode communication +const messageSigner = async (payload) => { + try { + // Convert the payload to a JSON string + const message = JSON.stringify(payload); + + // Hash the message with ethers.id (keccak256 hash) + const digestHex = ethers.id(message); + + // Convert the hash to bytes + const messageBytes = ethers.getBytes(digestHex); + + // Sign the bytes with the wallet's signing key + // Note: This uses the raw signing method, not the EIP-191 prefixed signing! + const { serialized: signature } = client.stateWalletClient.wallet.signingKey.sign(messageBytes); + + return signature; + } catch (error) { + console.error("Error signing message:", error); + throw error; + } +}; + +// After WebSocket connection is established +ws.onopen = async () => { + console.log('WebSocket connection established'); + + // Step 1: Create and send auth_verify with JWT for reconnection + // Get the stored JWT token + const jwtToken = window.localStorage.getItem('clearnode_jwt'); + + const authRequestMsg = await createAuthVerifyMessageWithJWT( + messageSigner, // Our custom message signer function + jwtToken, // JWT token for reconnection + client.stateWalletClient.account.address // Client address + ); + + ws.send(authRequestMsg); +}; + +// Handle incoming messages +ws.onmessage = async (event) => { + try { + const message = JSON.parse(event.data); + + // Step 2: Handle auth_success or auth_failure + if (message.res && message.res[1] === 'auth_success') { + console.log('Authentication successful'); + // Now you can start using the channel + } + else if (message.res && message.res[1] === 'auth_failure') { + console.error('Authentication failed:', message.res[2]); + } + } catch (error) { + console.error('Error handling message:', error); + } +}; ``` From 62e3fdc62693ca4756ae16f690ae1aead1037369 Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Thu, 29 May 2025 15:27:13 +0300 Subject: [PATCH 2/3] feat: update with eip 712 data --- docs/quick_start/connect_to_the_clearnode.md | 189 +++++++++++-------- 1 file changed, 111 insertions(+), 78 deletions(-) diff --git a/docs/quick_start/connect_to_the_clearnode.md b/docs/quick_start/connect_to_the_clearnode.md index 8f1e8b9..34a02f5 100644 --- a/docs/quick_start/connect_to_the_clearnode.md +++ b/docs/quick_start/connect_to_the_clearnode.md @@ -230,7 +230,7 @@ When connecting to a ClearNode, you need to follow a specific authentication flo 1. **Initial Connection**: The client establishes a WebSocket connection to the ClearNode's URL 2. **Auth Request**: On the first connection client sends an `auth_request` message with its identity information 3. **Challenge**: The ClearNode responds with an `auth_challenge` containing a random nonce -4. **Signature Verification**: The client signs the challenge using its state wallet and sends an `auth_verify` message +4. **Signature Verification**: The client signs the challenge along with session key and allowances using EIP712 signature and sends an `auth_verify` message 5. **Auth Result**: The ClearNode verifies the signature and responds with `auth_success` or `auth_failure` 6. **Reconnection**: On success ClearNode will return the JWT Token, which can be used for subsequent reconnections without needing to re-authenticate. @@ -244,14 +244,13 @@ sequenceDiagram Client->>CN: WebSocket Connection Request CN->>Client: Connection Established - Client->>Client: Create auth_request with address - Client->>Client: Sign message with state wallet + Client->>Client: Create auth_request Client->>CN: Send auth_request CN->>CN: Generate random challenge nonce CN->>Client: Send auth_challenge with nonce - Client->>Client: Sign challenge with state wallet + Client->>Client: Sign challenge using EIP712 Client->>CN: Send auth_verify with signature CN->>CN: Verify signature against address @@ -273,22 +272,31 @@ sequenceDiagram import { createAuthRequestMessage, createAuthVerifyMessage } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; -// Important: Custom message signer that correctly hashes the message with ethers.id // This is crucial for proper ClearNode communication -const messageSigner = async (payload) => { +const eip712MessageSigner = async (payload) => { try { - // Convert the payload to a JSON string - const message = JSON.stringify(payload); - - // Hash the message with ethers.id (keccak256 hash) - const digestHex = ethers.id(message); - - // Convert the hash to bytes - const messageBytes = ethers.getBytes(digestHex); - - // Sign the bytes with the wallet's signing key - // Note: This uses the raw signing method, not the EIP-191 prefixed signing! - const { serialized: signature } = client.stateWalletClient.wallet.signingKey.sign(messageBytes); + // Extract the challenge message from the data + const parsed = JSON.parse(data); + const challenge = parsed.challenge_message; + + const walletAddress = walletClient.account.address; + + const message = { + address: walletAddress, + challenge, + session_key: walletAddress, + allowances: [], + }; + + const signature = await walletClient.signTypedData({ + account: walletClient.account!, + domain: { + name: 'Your Domain', + }, + types: AUTH_TYPES, + primaryType: 'AuthVerify', + message: message, + }); return signature; } catch (error) { @@ -302,10 +310,12 @@ ws.onopen = async () => { console.log('WebSocket connection established'); // Step 1: Create and send auth_request - const authRequestMsg = await createAuthRequestMessage( - messageSigner, // Our custom message signer function - client.stateWalletClient.account.address // Client address - ); + const authRequestMsg = await createAuthRequestMessage({ + address: walletAddress, + session_key: walletAddress, + app_name: 'Your Domain', + allowances: [], + }); ws.send(authRequestMsg); }; @@ -321,9 +331,9 @@ ws.onmessage = async (event) => { // Step 3: Create and send auth_verify with signed challenge const authVerifyMsg = await createAuthVerifyMessage( - messageSigner, // Our custom message signer function - message, // Raw challenge response from ClearNode - client.stateWalletClient.account.address // Client address (same as in auth_request) + eip712MessageSigner, // Our custom eip712 signer function + event.data, // Raw challenge response from ClearNode + walletAddress // Client address (same as in auth_request) ); ws.send(authVerifyMsg); @@ -333,7 +343,7 @@ ws.onmessage = async (event) => { console.log('Authentication successful'); // Now you can start using the channel - window.localStorage.setItem('clearnode_jwt', message.res[2]["jwt_token"]); // Store JWT token for future use + window.localStorage.setItem('clearnode_jwt', message.res[2][0]["jwt_token"]); // Store JWT token for future use } else if (message.res && message.res[1] === 'auth_failure') { console.error('Authentication failed:', message.res[2]); @@ -356,20 +366,31 @@ import { } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; -// Create the proper message signer function -const messageSigner = async (payload) => { +// This is crucial for proper ClearNode communication +const eip712MessageSigner = async (payload) => { try { - // Convert the payload to a JSON string - const message = JSON.stringify(payload); - - // Hash the message with ethers.id (keccak256 hash) - const digestHex = ethers.id(message); - - // Convert the hash to bytes - const messageBytes = ethers.getBytes(digestHex); - - // Sign the bytes with the wallet's signing key - const { serialized: signature } = client.stateWalletClient.wallet.signingKey.sign(messageBytes); + // Extract the challenge message from the data + const parsed = JSON.parse(data); + const challenge = parsed.challenge_message; + + const walletAddress = walletClient.account.address; + + const message = { + address: walletAddress, + challenge, + session_key: walletAddress, + allowances: [], + }; + + const signature = await walletClient.signTypedData({ + account: walletClient.account!, + domain: { + name: 'Your Domain', + }, + types: AUTH_TYPES, + primaryType: 'AuthVerify', + message: message, + }); return signature; } catch (error) { @@ -380,10 +401,12 @@ const messageSigner = async (payload) => { // After connection is established, send auth request ws.onopen = async () => { - const authRequestMsg = await createAuthRequestMessage( - messageSigner, - client.stateWalletClient.account.address - ); + const authRequestMsg = await createAuthRequestMessage({ + address: walletAddress, + session_key: walletAddress, + app_name: 'Your Domain', + allowances: [], + }); ws.send(authRequestMsg); }; @@ -404,8 +427,7 @@ ws.onmessage = async (event) => { // Create auth_verify with the explicitly provided challenge const authVerifyMsg = await createAuthVerifyMessageFromChallenge( - messageSigner, - client.stateWalletClient.account.address, + eip712MessageSigner, challenge ); @@ -427,30 +449,6 @@ ws.onmessage = async (event) => { import { createAuthVerifyMessageWithJWT } from '@erc7824/nitrolite'; import { ethers } from 'ethers'; -// Important: Custom message signer that correctly hashes the message with ethers.id -// This is crucial for proper ClearNode communication -const messageSigner = async (payload) => { - try { - // Convert the payload to a JSON string - const message = JSON.stringify(payload); - - // Hash the message with ethers.id (keccak256 hash) - const digestHex = ethers.id(message); - - // Convert the hash to bytes - const messageBytes = ethers.getBytes(digestHex); - - // Sign the bytes with the wallet's signing key - // Note: This uses the raw signing method, not the EIP-191 prefixed signing! - const { serialized: signature } = client.stateWalletClient.wallet.signingKey.sign(messageBytes); - - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } -}; - // After WebSocket connection is established ws.onopen = async () => { console.log('WebSocket connection established'); @@ -460,9 +458,7 @@ ws.onopen = async () => { const jwtToken = window.localStorage.getItem('clearnode_jwt'); const authRequestMsg = await createAuthVerifyMessageWithJWT( - messageSigner, // Our custom message signer function jwtToken, // JWT token for reconnection - client.stateWalletClient.account.address // Client address ); ws.send(authRequestMsg); @@ -490,6 +486,43 @@ ws.onmessage = async (event) => { +### EIP-712 Signature + +In the authentication process, the client must sign messages using EIP-712 structured data signatures. This ensures that the messages are tamper-proof and verifiable by the ClearNode. + +The format of the EIP-712 message is as follows: + +```javascript +{ + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + ], + "AuthVerify": [ + { "name": "address", "type": "address" }, + { "name": "challenge", "type": "string" }, + { "name": "session_key", "type": "address" }, + { "name": "allowances", "type": "Allowances[]" } + ], + "Allowance": [ + { "name": "asset", "type": "string" }, + { "name": "amount", "type": "uint256" } + ], + }, + // Domain and primary type + domain: { + name: 'Your Domain', + }, + primaryType: 'AuthVerify', + message: { + address: '0xYourAddress', + challenge: 'RandomChallengeString', + session_key: '0xYourSessionKey', + allowances: [] // Add allowances if needed + } +} +``` + ## 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. @@ -1726,13 +1759,13 @@ When working with ClearNodes and state channels, keep these security best practi ## Troubleshooting Common Issues -| Issue | Possible Causes | Solution | -|-------|----------------|----------| -| Connection timeout | Network latency, ClearNode unavailable | Implement retry logic with exponential backoff | -| Authentication failure | Invalid state wallet, incorrect signing | Verify your state wallet is properly initialized and signing correctly | -| Frequent disconnections | Unstable network, server-side issues | Monitor connection events and implement automatic reconnection | -| Message delivery failures | Connection issues, invalid message format | Add message queuing and confirmation mechanism | -| Invalid signature errors | EIP-191 prefix issues | Ensure you're signing raw message bytes without the EIP-191 prefix | +| Issue | Possible Causes | Solution | +| ------------------------- | ----------------------------------------- | ---------------------------------------------------------------------- | +| Connection timeout | Network latency, ClearNode unavailable | Implement retry logic with exponential backoff | +| Authentication failure | Invalid state wallet, incorrect signing | Verify your state wallet is properly initialized and signing correctly | +| Frequent disconnections | Unstable network, server-side issues | Monitor connection events and implement automatic reconnection | +| Message delivery failures | Connection issues, invalid message format | Add message queuing and confirmation mechanism | +| Invalid signature errors | EIP-191 prefix issues | Ensure you're signing raw message bytes without the EIP-191 prefix | ## Next Steps From 5b7500ccd33b43b1fb08f722b1d8c304933d9ff9 Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Thu, 29 May 2025 16:42:02 +0300 Subject: [PATCH 3/3] feat: update docs with Policy --- docs/quick_start/connect_to_the_clearnode.md | 62 +++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/docs/quick_start/connect_to_the_clearnode.md b/docs/quick_start/connect_to_the_clearnode.md index 34a02f5..5432ceb 100644 --- a/docs/quick_start/connect_to_the_clearnode.md +++ b/docs/quick_start/connect_to_the_clearnode.md @@ -282,9 +282,12 @@ const eip712MessageSigner = async (payload) => { const walletAddress = walletClient.account.address; const message = { - address: walletAddress, - challenge, - session_key: walletAddress, + challenge: challenge, + scope: 'console', + wallet: '0xYourWalletAddress', + application: '0xYourApplicationAddress', // Your application address + participant: '0xYourSignerAddress', // The address of the signer + expire: Math.floor(Date.now() / 1000) + 3600, // 1 hour expiration allowances: [], }; @@ -294,7 +297,7 @@ const eip712MessageSigner = async (payload) => { name: 'Your Domain', }, types: AUTH_TYPES, - primaryType: 'AuthVerify', + primaryType: 'Policy', message: message, }); @@ -311,9 +314,12 @@ ws.onopen = async () => { // Step 1: Create and send auth_request const authRequestMsg = await createAuthRequestMessage({ - address: walletAddress, - session_key: walletAddress, + wallet: '0xYourWalletAddress', + participant: '0xYourSignerAddress', app_name: 'Your Domain', + expire: Math.floor(Date.now() / 1000) + 3600, // 1 hour expiration + scope: 'console', + application: '0xYourApplicationAddress', allowances: [], }); @@ -376,9 +382,12 @@ const eip712MessageSigner = async (payload) => { const walletAddress = walletClient.account.address; const message = { - address: walletAddress, - challenge, - session_key: walletAddress, + challenge: challenge, + scope: 'console', + wallet: '0xYourWalletAddress', + application: '0xYourApplicationAddress', // Your application address + participant: '0xYourSignerAddress', // The address of the signer + expire: Math.floor(Date.now() / 1000) + 3600, // 1 hour expiration allowances: [], }; @@ -388,7 +397,7 @@ const eip712MessageSigner = async (payload) => { name: 'Your Domain', }, types: AUTH_TYPES, - primaryType: 'AuthVerify', + primaryType: 'Policy', message: message, }); @@ -402,9 +411,12 @@ const eip712MessageSigner = async (payload) => { // After connection is established, send auth request ws.onopen = async () => { const authRequestMsg = await createAuthRequestMessage({ - address: walletAddress, - session_key: walletAddress, + 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); @@ -496,29 +508,35 @@ The format of the EIP-712 message is as follows: { "types": { "EIP712Domain": [ - { "name": "name", "type": "string" }, + { "name": "name", "type": "string" } ], - "AuthVerify": [ - { "name": "address", "type": "address" }, + "Policy": [ { "name": "challenge", "type": "string" }, - { "name": "session_key", "type": "address" }, + { "name": "scope", "type": "string" }, + { "name": "wallet", "type": "address" }, + { "name": "application", "type": "address" }, + { "name": "participant", "type": "address" }, + { "name": "expire", "type": "uint256" }, { "name": "allowances", "type": "Allowances[]" } ], "Allowance": [ { "name": "asset", "type": "string" }, { "name": "amount", "type": "uint256" } - ], + ] }, // Domain and primary type domain: { - name: 'Your Domain', + name: 'Your Domain' }, - primaryType: 'AuthVerify', + primaryType: 'Policy', message: { - address: '0xYourAddress', challenge: 'RandomChallengeString', - session_key: '0xYourSessionKey', - allowances: [] // Add allowances if needed + scope: 'console', + wallet: '0xYourWalletAddress', + application: '0xYourApplicationAddress', + participant: '0xYourSignerAddress', + expire: 100500, + allowances: [] } } ```