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#
| Parameter | Type | Required | Description |
|---|---|---|---|
owner | string | Yes | Sui 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#
- TypeScript
- Python
- Go
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}`);
});
import grpc
import livedata_service_pb2
import livedata_service_pb2_grpc
ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com'
API_TOKEN = 'your_api_token_here'
def list_balances(owner: str):
credentials = grpc.ssl_channel_credentials()
channel = grpc.secure_channel(ENDPOINT, credentials)
client = livedata_service_pb2_grpc.LiveDataServiceStub(channel)
request = livedata_service_pb2.ListBalancesRequest(
owner=owner
)
metadata = [('x-api-key', API_TOKEN)]
response = client.ListBalances(request, metadata=metadata)
for balance in response.balances:
print(f'Coin: {balance.coin_type}')
print(f'Balance: {balance.balance}')
print(f'Locked: {balance.locked_balance}')
print('---')
channel.close()
return response
# Usage
balances = list_balances('0x742d35cc6634c0532925a3b844bc9e7eb503c114a04bd3e02c7681a09e58b01d')
package main
import (
"context"
"fmt"
"log"
"time"
"sui-grpc-client/config"
pb "sui-grpc-client/sui/rpc/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
func main() {
// Load configuration from .env file
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Create TLS credentials
creds := credentials.NewClientTLSFromCert(nil, "")
// Connect to Dwellir
conn, err := grpc.NewClient(
cfg.Endpoint,
grpc.WithTransportCredentials(creds),
)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Create state service client
client := pb.NewStateServiceClient(conn)
// Add authentication
ctx := metadata.AppendToOutgoingContext(
context.Background(),
"x-api-key", cfg.APIKey,
)
// Set timeout
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Example address - use an address that holds multiple coin types
// This is a well-known address on mainnet with multiple coin types
owner := "0x0000000000000000000000000000000000000000000000000000000000000000"
fmt.Printf("Listing all balances for: %s\n\n", owner)
pageSize := uint32(10)
var pageToken []byte
totalBalances := 0
pageNum := 1
for {
request := &pb.ListBalancesRequest{
Owner: &owner,
PageSize: &pageSize,
}
if pageToken != nil {
request.PageToken = pageToken
}
response, err := client.ListBalances(ctx, request)
if err != nil {
log.Fatalf("Failed to list balances: %v", err)
}
balances := response.GetBalances()
if len(balances) == 0 {
if totalBalances == 0 {
fmt.Println("No balances found (address may not hold any coins)")
}
break
}
fmt.Printf("Page %d - Found %d balance(s):\n", pageNum, len(balances))
fmt.Println("====================================")
for i, balance := range balances {
fmt.Printf("%d. Coin Type: %s\n", totalBalances+i+1, balance.GetCoinType())
balanceAmount := balance.GetBalance()
fmt.Printf(" Balance: %d (smallest unit)\n", balanceAmount)
// If it's SUI, also show in SUI units
if balance.GetCoinType() == "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI" {
fmt.Printf(" Balance: %.4f SUI\n", float64(balanceAmount)/1_000_000_000)
}
fmt.Println()
}
totalBalances += len(balances)
// Check if there are more pages
nextPageToken := response.GetNextPageToken()
if len(nextPageToken) == 0 {
break
}
pageToken = nextPageToken
pageNum++
}
fmt.Printf("\nTotal coin types found: %d\n", totalBalances)
}
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#
| 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.