Skip to main content

ListBalances

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.v2beta2.LiveDataService Method: ListBalances Type: Unary RPC

Parameters#

ParameterTypeRequiredDescription
ownerstringYesSui address to query balances for

Response Structure#

message ListBalancesResponse {
repeated Balance balances = 1;
}

message Balance {
string coin_type = 1;
string balance = 2;
uint64 locked_balance = 3;
}

Field Descriptions#

  • coin_type: Full coin type identifier (e.g., 0x2::sui::SUI)
  • balance: Available balance as string (in smallest denomination)
  • locked_balance: Balance locked in staking or other protocols

Code Examples#

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';

const ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com';
const API_TOKEN = 'your_api_token_here';

const packageDefinition = protoLoader.loadSync('./protos/livedata.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
includeDirs: ['./protos']
});

const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
const credentials = grpc.credentials.createSsl();
const client = new protoDescriptor.sui.rpc.v2beta2.LiveDataService(ENDPOINT, credentials);

const metadata = new grpc.Metadata();
metadata.add('x-api-key', API_TOKEN);

async function listBalances(owner: string): Promise<any> {
return new Promise((resolve, reject) => {
const request = { owner: owner };

client.ListBalances(request, metadata, (error: any, response: any) => {
if (error) {
console.error('ListBalances error:', error.message);
reject(error);
return;
}

resolve(response);
});
});
}

// Usage
const balances = await listBalances('0x742d35cc6634c0532925a3b844bc9e7eb503c114a04bd3e02c7681a09e58b01d');

balances.balances.forEach((balance: any) => {
console.log(`Coin: ${balance.coin_type}`);
console.log(`Balance: ${balance.balance}`);
console.log(`Locked: ${balance.locked_balance}`);
});

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 seconds

Token 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#

MetricValue
Typical Latency20-50ms
Response Size100-2000 bytes (varies by token count)
Cache RecommendedYes (5-10s TTL)
Rate Limit ImpactLow

Common Errors#

Error CodeScenarioSolution
INVALID_ARGUMENTInvalid address formatVerify address is valid Sui address
NOT_FOUNDAddress has no balancesHandle empty response gracefully
UNAUTHENTICATEDMissing/invalid tokenVerify x-api-key header

Comparison with GetBalance#

FeatureListBalancesGetBalance
Query TypeAll tokensSingle token
Use CasePortfolio viewSpecific token check
Response SizeLargerSmaller
LatencySlightly higherSlightly lower
When to UseInitial load, full viewBalance checks, validation

Need help? Contact support@dwellir.com or check the gRPC overview.