Docs

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

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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.