Skip to main content

Channel Asset Management

After connecting to a ClearNode, you'll need to monitor the off-chain balances in your state channels. This guide explains how to retrieve and work with off-chain balance information using the NitroliteRPC protocol.

Understanding Off-Chain Balances

Off-chain balances in Nitrolite represent:

  • Your current funds in the state channel
  • Balances that update in real-time as transactions occur
  • The source of truth for application operations
  • Assets that are backed by on-chain deposits

Checking Off-Chain Balances

To monitor your channel funds, you need to retrieve the current off-chain balances from the ClearNode.

Understanding the Ledger Balances Request

The get_ledger_balances request is used to retrieve the current off-chain balances for a specific participant from the ClearNode:

  • Request params: [{ participant: "0xAddress" }] where 0xAddress is the participant's address
  • Response: Array containing the balances for different assets held by the participant

The response contains a list of assets and their amounts for the specified participant. The balances are represented as strings with decimal precision, making it easier to display them directly without additional conversion.

// Example response format for get_ledger_balances
{
"res": [1, "get_ledger_balances", [[ // The nested array contains balance data
{
"asset": "usdc", // Asset identifier
"amount": "100.0" // Amount as a string with decimal precision
},
{
"asset": "eth",
"amount": "0.5"
}
]], 1619123456789], // Timestamp
"sig": ["0xabcd1234..."]
}

To retrieve these balances, use the get_ledger_balances request with the ClearNode:

import { createGetLedgerBalancesMessage } from '@erc7824/nitrolite';
import { ethers } from 'ethers';

// Your message signer function (same as in auth flow)
const messageSigner = async (payload) => {
const message = JSON.stringify(payload);
const digestHex = ethers.id(message);
const messageBytes = ethers.getBytes(digestHex);
const { serialized: signature } = stateWallet.signingKey.sign(messageBytes);
return signature;
};

// Function to get ledger balances
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);

// Check if this is a response to our get_ledger_balances request
if (message.res && message.res[1] === 'get_ledger_balances') {
// Remove the message handler to avoid memory leaks
ws.removeEventListener('message', handleMessage);

// Resolve with the balances data
resolve(message.res[2]);
}
};

// Add the message handler
ws.addEventListener('message', handleMessage);

// Create and send the ledger balances request
createGetLedgerBalancesMessage(messageSigner, participant)
.then(message => {
ws.send(message);
})
.catch(error => {
// Remove the message handler on error
ws.removeEventListener('message', handleMessage);
reject(error);
});

// Set a timeout to prevent hanging indefinitely
setTimeout(() => {
ws.removeEventListener('message', handleMessage);
reject(new Error('Timeout waiting for ledger balances'));
}, 10000); // 10 second timeout
});
}

// Usage example
const participantAddress = '0x1234567890abcdef1234567890abcdef12345678';

try {
const balances = await getLedgerBalances(ws, participantAddress);

console.log('Channel ledger balances:', balances);
// Example output:
// [
// [
// { "asset": "usdc", "amount": "100.0" },
// { "asset": "eth", "amount": "0.5" }
// ]
// ]

// Process your balances
if (balances[0] && balances[0].length > 0) {
const balanceList = balances[0]; // Array of balance entries by asset

// Display each asset balance
balanceList.forEach(balance => {
console.log(`${balance.asset.toUpperCase()} balance: ${balance.amount}`);
});

// Example: find a specific asset
const usdcBalance = balanceList.find(b => b.asset.toLowerCase() === 'usdc');
if (usdcBalance) {
console.log(`USDC balance: ${usdcBalance.amount}`);
}
} else {
console.log('No balance data available');
}
} catch (error) {
console.error('Failed to get ledger balances:', error);
}

Checking Balances for a Participant

To retrieve off-chain balances for a participant, use the createGetLedgerBalancesMessage helper function:

import { createGetLedgerBalancesMessage } from '@erc7824/nitrolite';
import { ethers } from 'ethers';

// Function to get ledger balances for a participant
async function getLedgerBalances(ws, participant, messageSigner) {
return new Promise((resolve, reject) => {
// Message handler for the response
const handleMessage = (event) => {
try {
const message = JSON.parse(event.data);

// Check if this is a response to our get_ledger_balances request
if (message.res && message.res[1] === 'get_ledger_balances') {
// Clean up by removing the event listener
ws.removeEventListener('message', handleMessage);

// Resolve with the balance data
resolve(message.res[2]);
}
} catch (error) {
console.error('Error parsing message:', error);
}
};

// Set up timeout to avoid hanging indefinitely
const timeoutId = setTimeout(() => {
ws.removeEventListener('message', handleMessage);
reject(new Error('Timeout waiting for ledger balances'));
}, 10000); // 10 second timeout

// Add the message handler
ws.addEventListener('message', handleMessage);

// Create and send the balance request
createGetLedgerBalancesMessage(messageSigner, participant)
.then(message => {
ws.send(message);
})
.catch(error => {
clearTimeout(timeoutId);
ws.removeEventListener('message', handleMessage);
reject(error);
});
});
}

// Example usage
const participantAddress = '0x1234567890abcdef1234567890abcdef12345678';

getLedgerBalances(ws, participantAddress, messageSigner)
.then(balances => {
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

console.log('My balances:');
balanceList.forEach(balance => {
console.log(`- ${balance.asset.toUpperCase()}: ${balance.amount}`);
});
} else {
console.log('No balance data available');
}
})
.catch(error => {
console.error('Failed to get ledger balances:', error);
});

Processing Balance Data

When you receive balance data from the ClearNode, it's helpful to format it for better readability:

// 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 => ({
asset: balance.asset.toUpperCase(),
amount: balance.amount,
// You can add additional formatting here if needed
displayAmount: `${balance.amount} ${balance.asset.toUpperCase()}`
}));
}

// Example usage
const myFormattedBalances = formatMyBalances(balancesFromClearNode);

if (myFormattedBalances && myFormattedBalances.length > 0) {
console.log('My balances:');
myFormattedBalances.forEach(balance => {
console.log(`- ${balance.displayAmount}`);
});
} else {
console.log('No balance data available');
}

Best Practices for Balance Checking

When working with off-chain balances, follow these best practices:

Regular Balance Polling

Set up a regular interval to check balances, especially in active applications:

// Simple balance monitoring function
function startBalanceMonitoring(ws, participantAddress, messageSigner, intervalMs = 30000) {
// Check immediately on start
getLedgerBalances(ws, participantAddress, messageSigner)
.then(displayBalances)
.catch(err => console.error('Initial balance check failed:', err));

// Set up interval for regular checks
const intervalId = setInterval(() => {
getLedgerBalances(ws, participantAddress, messageSigner)
.then(displayBalances)
.catch(err => console.error('Balance check failed:', err));
}, intervalMs); // Check every 30 seconds by default

// Return function to stop monitoring
return () => clearInterval(intervalId);
}

// Simple display function
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

console.log('My balances:');
balanceList.forEach(balance => {
console.log(`- ${balance.asset.toUpperCase()}: ${balance.amount}`);
});
} else {
console.log('No balance data available');
}
}

Common Errors and Troubleshooting

When retrieving off-chain balances, you might encounter these common issues:

Error TypeDescriptionSolution
Authentication errorsWebSocket connection loses authenticationRe-authenticate before requesting balances again
Channel not foundThe channel ID is invalid or the channel has been closedVerify the channel ID and check if the channel is still active
Connection issuesWebSocket disconnects during a balance requestImplement reconnection logic with exponential backoff
TimeoutThe ClearNode does not respond in a timely mannerSet appropriate timeouts and implement retry logic

Next Steps

Now that you understand how to monitor off-chain balances in your channels, you can:

  1. Create an application session to start transacting off-chain
  2. Learn about channel closing when you're done with the channel