ListBalances - Query All Token Balances
Retrieve all token balances for a Sui address via gRPC including multiple coin types. Essential for portfolio displays and multi-token wallets with Dwellir.
Query All Token Balances for an Address
The ListBalances method retrieves all token balances for a specific Sui address across all coin types. This is essential for portfolio displays, multi-token wallets, and applications that need to show a complete view of an address's holdings without knowing which tokens it owns in advance.
Method Signature
Service: sui.rpc.v2.StateService
Method: ListBalances
Type: Unary RPC
Use Cases
Portfolio Display
interface PortfolioToken {
coinType: string;
symbol: string;
balance: string;
balanceFormatted: string;
lockedBalance: string;
iconUrl?: string;
usdValue?: number;
}
async function getPortfolio(owner: string): Promise<PortfolioToken[]> {
const balancesResponse = await listBalances(owner);
// Enrich with coin metadata
const enrichedBalances = await Promise.all(
balancesResponse.balances.map(async (balance: any) => {
try {
const coinInfo = await getCoinInfo(balance.coin_type);
const balanceNum = Number(balance.balance) / Math.pow(10, coinInfo.decimals);
return {
coinType: balance.coin_type,
symbol: coinInfo.symbol,
balance: balance.balance,
balanceFormatted: balanceNum.toFixed(coinInfo.decimals),
lockedBalance: balance.locked_balance.toString(),
iconUrl: coinInfo.icon_url
};
} catch (error) {
console.warn(`Failed to get info for ${balance.coin_type}`);
return {
coinType: balance.coin_type,
symbol: 'UNKNOWN',
balance: balance.balance,
balanceFormatted: balance.balance,
lockedBalance: balance.locked_balance.toString()
};
}
})
);
// Filter out zero balances
return enrichedBalances.filter(
token => Number(token.balance) > 0 || Number(token.lockedBalance) > 0
);
}
// Usage
const portfolio = await getPortfolio(userAddress);
console.log('Portfolio:');
portfolio.forEach(token => {
console.log(`${token.symbol}: ${token.balanceFormatted}`);
if (Number(token.lockedBalance) > 0) {
console.log(` (${token.lockedBalance} locked)`);
}
});Multi-Token Transfer Validation
interface TransferValidation {
canTransfer: boolean;
availableBalance: bigint;
reason?: string;
}
async function validateMultiTokenTransfer(
owner: string,
transfers: Array<{ coinType: string; amount: bigint }>
): Promise<Map<string, TransferValidation>> {
const balances = await listBalances(owner);
const balanceMap = new Map(
balances.balances.map((b: any) => [b.coin_type, BigInt(b.balance)])
);
const validations = new Map<string, TransferValidation>();
for (const transfer of transfers) {
const available = balanceMap.get(transfer.coinType) || 0n;
if (available >= transfer.amount) {
validations.set(transfer.coinType, {
canTransfer: true,
availableBalance: available
});
} else {
validations.set(transfer.coinType, {
canTransfer: false,
availableBalance: available,
reason: `Insufficient balance: need ${transfer.amount}, have ${available}`
});
}
}
return validations;
}
// Usage
const transfers = [
{ coinType: '0x2::sui::SUI', amount: 5_000_000_000n }, // 5 SUI
{ coinType: 'USDC_TYPE', amount: 100_000_000n } // 100 USDC
];
const validations = await validateMultiTokenTransfer(userAddress, transfers);
for (const [coinType, validation] of validations) {
if (!validation.canTransfer) {
console.error(`Cannot transfer ${coinType}: ${validation.reason}`);
}
}Portfolio Value Calculator
interface PortfolioValue {
tokens: Array<{
coinType: string;
symbol: string;
balance: string;
usdValue: number;
}>;
totalUsdValue: number;
}
async function calculatePortfolioValue(
owner: string,
priceOracle: (coinType: string) => Promise<number>
): Promise<PortfolioValue> {
const balances = await listBalances(owner);
const tokensWithValue = await Promise.all(
balances.balances.map(async (balance: any) => {
const coinInfo = await getCoinInfo(balance.coin_type);
const price = await priceOracle(balance.coin_type).catch(() => 0);
const balanceNum = Number(balance.balance) / Math.pow(10, coinInfo.decimals);
const usdValue = balanceNum * price;
return {
coinType: balance.coin_type,
symbol: coinInfo.symbol,
balance: balanceNum.toFixed(coinInfo.decimals),
usdValue
};
})
);
const totalUsdValue = tokensWithValue.reduce(
(sum, token) => sum + token.usdValue,
0
);
return {
tokens: tokensWithValue,
totalUsdValue
};
}
// Usage
const portfolio = await calculatePortfolioValue(
userAddress,
async (coinType) => {
// Fetch price from your preferred oracle
return fetchPrice(coinType);
}
);
console.log(`Total Portfolio Value: $${portfolio.totalUsdValue.toFixed(2)}`);
portfolio.tokens.forEach(token => {
console.log(`${token.symbol}: ${token.balance} ($${token.usdValue.toFixed(2)})`);
});Balance Change Monitoring
class BalanceMonitor {
private previousBalances = new Map<string, string>();
async checkForChanges(owner: string): Promise<Array<{
coinType: string;
previousBalance: string;
currentBalance: string;
change: string;
}>> {
const current = await listBalances(owner);
const changes: any[] = [];
for (const balance of current.balances) {
const previous = this.previousBalances.get(balance.coin_type);
if (previous && previous !== balance.balance) {
changes.push({
coinType: balance.coin_type,
previousBalance: previous,
currentBalance: balance.balance,
change: (BigInt(balance.balance) - BigInt(previous)).toString()
});
}
this.previousBalances.set(balance.coin_type, balance.balance);
}
return changes;
}
}
// Usage
const monitor = new BalanceMonitor();
// Poll for changes
setInterval(async () => {
const changes = await monitor.checkForChanges(userAddress);
if (changes.length > 0) {
console.log('Balance changes detected:');
changes.forEach(change => {
const isPositive = BigInt(change.change) > 0n;
const direction = isPositive ? '+' : '';
console.log(`${change.coinType}: ${direction}${change.change}`);
});
}
}, 10000); // Check every 10 secondsToken Discovery
async function discoverNewTokens(
owner: string,
knownTokens: Set<string>
): Promise<string[]> {
const balances = await listBalances(owner);
const newTokens = balances.balances
.map((b: any) => b.coin_type)
.filter(coinType => !knownTokens.has(coinType));
if (newTokens.length > 0) {
console.log(`Discovered ${newTokens.length} new tokens`);
for (const coinType of newTokens) {
try {
const info = await getCoinInfo(coinType);
console.log(` - ${info.symbol} (${info.name})`);
knownTokens.add(coinType);
} catch (error) {
console.warn(` - Unknown token: ${coinType}`);
}
}
}
return newTokens;
}Best Practices
Caching Strategy
class BalanceCache {
private cache = new Map<string, { data: any; timestamp: number }>();
private ttl = 5000; // 5 seconds
async getBalances(owner: string): Promise<any> {
const cached = this.cache.get(owner);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const balances = await listBalances(owner);
this.cache.set(owner, {
data: balances,
timestamp: Date.now()
});
return balances;
}
invalidate(owner: string): void {
this.cache.delete(owner);
}
}
// Usage
const cache = new BalanceCache();
// Initial fetch
const balances1 = await cache.getBalances(owner); // Network request
// Subsequent fetches within TTL
const balances2 = await cache.getBalances(owner); // Cached
// After transaction, invalidate
await executeTransaction(tx);
cache.invalidate(owner);Handle Missing Metadata
async function listBalancesWithFallback(owner: string): Promise<any[]> {
const balances = await listBalances(owner);
return Promise.all(
balances.balances.map(async (balance: any) => {
try {
const info = await getCoinInfo(balance.coin_type);
return {
...balance,
symbol: info.symbol,
decimals: info.decimals,
name: info.name
};
} catch (error) {
// Fallback for unknown tokens
return {
...balance,
symbol: balance.coin_type.split('::').pop() || 'UNKNOWN',
decimals: 9, // Assume standard decimals
name: 'Unknown Token'
};
}
})
);
}Filter Zero Balances
async function listNonZeroBalances(owner: string): Promise<any[]> {
const balances = await listBalances(owner);
return balances.balances.filter((balance: any) => {
const hasAvailable = BigInt(balance.balance) > 0n;
const hasLocked = balance.locked_balance && BigInt(balance.locked_balance) > 0n;
return hasAvailable || hasLocked;
});
}Performance Characteristics
| Metric | Value |
|---|---|
| Typical Latency | 20-50ms |
| Response Size | 100-2000 bytes (varies by token count) |
| Cache Recommended | Yes (5-10s TTL) |
| Rate Limit Impact | Low |
Common Errors
| Error Code | Scenario | Solution |
|---|---|---|
INVALID_ARGUMENT | Invalid address format | Verify address is valid Sui address |
NOT_FOUND | Address has no balances | Handle empty response gracefully |
UNAUTHENTICATED | Missing/invalid token | Verify x-api-key header |
Comparison with GetBalance
| Feature | ListBalances | GetBalance |
|---|---|---|
| Query Type | All tokens | Single token |
| Use Case | Portfolio view | Specific token check |
| Response Size | Larger | Smaller |
| Latency | Slightly higher | Slightly lower |
| When to Use | Initial load, full view | Balance checks, validation |
Related Methods
- GetBalance - Query single token balance
- GetCoinInfo - Get token metadata
- ListOwnedObjects - List all owned objects
Need help? Contact support@dwellir.com or check the gRPC overview.
GetCoinInfo
Retrieve comprehensive metadata for Sui coin types including name, symbol, decimals, and supply information via gRPC. Essential for token displays and DeFi applications with Dwellir.
ListDynamicFields
Retrieve dynamic fields attached to Sui objects via gRPC with pagination support. Essential for exploring extensible objects and collections with Dwellir.