Docs

suix_getCoins - Get Coin Objects with Fi...

Retrieve and filter coin objects on Sui blockchain using suix_getCoins RPC method. Query specific coin types, paginate results, and access detailed coin object data with Dwellir's high-performance Sui infrastructure.

Retrieves coin objects owned by an address with optional filtering by coin type and pagination support, providing detailed information about individual coin objects rather than aggregated balances.

Overview

The suix_getCoins method is essential for applications that need to work with individual coin objects rather than just aggregate balances. Unlike suix_getBalance which returns totals, this method returns actual coin objects that can be used in transactions, split, merged, or transferred. It supports filtering by coin type and pagination for efficient data retrieval, making it perfect for transaction construction, coin management, and detailed portfolio analysis.

Code Examples

Common Use Cases

1. Transaction Coin Selection

JavaScript
async function buildTransaction(sender, recipient, amount, coinType = '0x2::sui::SUI') {
  // Select appropriate coins for the transaction
  const coinSelection = await selectCoinsForAmount(sender, coinType, amount);
  
  const txb = new TransactionBlock();
  
  if (coinSelection.coinCount === 1) {
    // Simple case: use single coin
    const [coin] = coinSelection.selectedCoins;
    
    if (coinSelection.change > 0) {
      // Split coin if change is needed
      const [splitCoin] = txb.splitCoins(coin.coinObjectId, [amount]);
      txb.transferObjects([splitCoin], recipient);
    } else {
      // Transfer entire coin
      txb.transferObjects([coin.coinObjectId], recipient);
    }
  } else {
    // Multiple coins: merge first, then transfer
    const [primaryCoin, ...coinsToMerge] = coinSelection.selectedCoins;
    
    if (coinsToMerge.length > 0) {
      txb.mergeCoins(
        primaryCoin.coinObjectId, 
        coinsToMerge.map(c => c.coinObjectId)
      );
    }
    
    const [splitCoin] = txb.splitCoins(primaryCoin.coinObjectId, [amount]);
    txb.transferObjects([splitCoin], recipient);
  }
  
  return txb;
}

2. Coin Consolidation

JavaScript
async function consolidateCoins(owner, coinType, maxCoinsPerTx = 10) {
  const coins = await getAllCoinsWithPagination(owner, coinType);
  
  if (coins.length <= 1) {
    console.log('No consolidation needed');
    return [];
  }
  
  // Sort coins by balance (keep largest as primary)
  coins.sort((a, b) => parseInt(b.balance) - parseInt(a.balance));
  
  const transactions = [];
  const primaryCoin = coins[0];
  
  // Group remaining coins into batches
  for (let i = 1; i < coins.length; i += maxCoinsPerTx - 1) {
    const batch = coins.slice(i, i + maxCoinsPerTx - 1);
    
    const txb = new TransactionBlock();
    txb.mergeCoins(
      primaryCoin.coinObjectId,
      batch.map(coin => coin.coinObjectId)
    );
    
    transactions.push({
      transaction: txb,
      description: `Merge ${batch.length} coins into primary coin`,
      coinsToMerge: batch.map(c => c.coinObjectId)
    });
  }
  
  return transactions;
}

3. Coin Splitting Strategy

JavaScript
async function splitCoinOptimally(owner, coinType, targetSizes) {
  const coins = await getAllCoinsWithPagination(owner, coinType);
  
  if (coins.length === 0) {
    throw new Error('No coins available for splitting');
  }
  
  // Find the largest coin that can accommodate all splits
  const totalTargetAmount = targetSizes.reduce((sum, size) => sum + size, 0);
  const suitableCoin = coins.find(coin => 
    parseInt(coin.balance) >= totalTargetAmount
  );
  
  if (!suitableCoin) {
    throw new Error('No single coin large enough for all splits');
  }
  
  const txb = new TransactionBlock();
  
  // Split the coin into target sizes
  const splitCoins = txb.splitCoins(
    suitableCoin.coinObjectId,
    targetSizes
  );
  
  return {
    transaction: txb,
    sourceCoin: suitableCoin.coinObjectId,
    targetSizes: targetSizes,
    splitCoins: splitCoins,
    remainingBalance: parseInt(suitableCoin.balance) - totalTargetAmount
  };
}

4. Portfolio Rebalancing

JavaScript
async function rebalancePortfolio(owner, targetAllocations) {
  const portfolioCoins = {};
  
  // Get coins for each type in target allocation
  for (const coinType of Object.keys(targetAllocations)) {
    portfolioCoins[coinType] = await getAllCoinsWithPagination(owner, coinType);
  }
  
  // Calculate current balances
  const currentBalances = {};
  let totalValue = 0;
  
  for (const [coinType, coins] of Object.entries(portfolioCoins)) {
    const balance = coins.reduce((sum, coin) => sum + parseInt(coin.balance), 0);
    currentBalances[coinType] = balance;
    totalValue += balance; // Simplified - would need price conversion
  }
  
  // Calculate rebalancing requirements
  const rebalancingActions = [];
  
  for (const [coinType, targetPercent] of Object.entries(targetAllocations)) {
    const currentBalance = currentBalances[coinType] || 0;
    const targetBalance = (totalValue * targetPercent) / 100;
    const difference = targetBalance - currentBalance;
    
    if (Math.abs(difference) > totalValue * 0.01) { // 1% threshold
      rebalancingActions.push({
        coinType,
        currentBalance,
        targetBalance,
        difference,
        action: difference > 0 ? 'buy' : 'sell',
        amount: Math.abs(difference)
      });
    }
  }
  
  return {
    currentBalances,
    targetAllocations,
    totalValue,
    rebalancingActions,
    needsRebalancing: rebalancingActions.length > 0
  };
}

Advanced Coin Management

1. Gas-Efficient Coin Strategy

JavaScript
class GasOptimizedCoinManager {
  constructor(client) {
    this.client = client;
    this.gasPrice = 750; // MIST per gas unit
    this.baseTransactionCost = 1000000; // Base transaction cost
  }
  
  async calculateConsolidationValue(owner, coinType) {
    const coins = await getAllCoinsWithPagination(owner, coinType);
    
    if (coins.length <= 1) return null;
    
    // Storage cost saved per coin consolidated
    const storageRefundPerCoin = 988000; // Approximate MIST
    const potentialSavings = (coins.length - 1) * storageRefundPerCoin;
    
    // Gas cost of consolidation
    const gasUnitsPerMerge = 1000; // Approximate
    const totalGasCost = gasUnitsPerMerge * (coins.length - 1) * this.gasPrice;
    
    const netBenefit = potentialSavings - totalGasCost;
    
    return {
      coinCount: coins.length,
      potentialSavings,
      consolidationCost: totalGasCost,
      netBenefit,
      worthConsolidating: netBenefit > 0,
      breakEvenCoinCount: Math.ceil(totalGasCost / storageRefundPerCoin) + 1
    };
  }
  
  async optimizeCoinSelection(owner, coinType, amount) {
    const coins = await getAllCoinsWithPagination(owner, coinType);
    
    // Sort by balance descending for greedy selection
    coins.sort((a, b) => parseInt(b.balance) - parseInt(a.balance));
    
    // Try different selection strategies
    const strategies = [
      this.greedySelection(coins, amount),
      this.exactFitSelection(coins, amount),
      this.minimizeChangeSelection(coins, amount)
    ];
    
    // Evaluate each strategy's gas efficiency
    const evaluatedStrategies = strategies.map(strategy => {
      if (!strategy.coins) return { ...strategy, efficiency: 0 };
      
      const mergeOperations = Math.max(0, strategy.coins.length - 1);
      const gasCost = (this.baseTransactionCost + mergeOperations * 500) * this.gasPrice;
      
      return {
        ...strategy,
        gasCost,
        efficiency: amount / gasCost, // Amount per gas unit
        wasteRatio: strategy.change / amount
      };
    });
    
    // Select the most efficient strategy
    return evaluatedStrategies
      .filter(s => s.coins && s.coins.length > 0)
      .reduce((best, current) => 
        current.efficiency > best.efficiency ? current : best
      );
  }
  
  greedySelection(coins, amount) {
    const selected = [];
    let total = 0;
    
    for (const coin of coins) {
      selected.push(coin);
      total += parseInt(coin.balance);
      if (total >= amount) break;
    }
    
    return {
      strategy: 'greedy',
      coins: total >= amount ? selected : null,
      total,
      change: Math.max(0, total - amount),
      success: total >= amount
    };
  }
  
  exactFitSelection(coins, amount) {
    // Try to find exact or near-exact match
    const exactMatch = coins.find(coin => parseInt(coin.balance) === amount);
    if (exactMatch) {
      return {
        strategy: 'exact',
        coins: [exactMatch],
        total: amount,
        change: 0,
        success: true
      };
    }
    
    // Find smallest coin that covers the amount
    const singleCoin = coins.find(coin => parseInt(coin.balance) >= amount);
    if (singleCoin) {
      return {
        strategy: 'single',
        coins: [singleCoin],
        total: parseInt(singleCoin.balance),
        change: parseInt(singleCoin.balance) - amount,
        success: true
      };
    }
    
    return { strategy: 'exact', coins: null, success: false };
  }
  
  minimizeChangeSelection(coins, amount) {
    // Use dynamic programming to minimize change
    let bestSelection = null;
    let minChange = Infinity;
    
    // Try combinations up to 5 coins (gas efficiency limit)
    for (let maxCoins = 1; maxCoins <= Math.min(5, coins.length); maxCoins++) {
      const result = this.findOptimalCombination(coins, amount, maxCoins);
      
      if (result.success && result.change < minChange) {
        minChange = result.change;
        bestSelection = result;
      }
    }
    
    return bestSelection || { strategy: 'minimize', coins: null, success: false };
  }
  
  findOptimalCombination(coins, amount, maxCoins, index = 0, current = []) {
    if (current.length > maxCoins) return { success: false };
    
    const currentTotal = current.reduce((sum, coin) => sum + parseInt(coin.balance), 0);
    
    if (currentTotal >= amount) {
      return {
        strategy: 'minimize',
        coins: [...current],
        total: currentTotal,
        change: currentTotal - amount,
        success: true
      };
    }
    
    if (index >= coins.length) return { success: false };
    
    // Try including current coin
    const withCoin = this.findOptimalCombination(
      coins, amount, maxCoins, index + 1, [...current, coins[index]]
    );
    
    // Try without current coin
    const withoutCoin = this.findOptimalCombination(
      coins, amount, maxCoins, index + 1, current
    );
    
    // Return better result
    if (!withCoin.success) return withoutCoin;
    if (!withoutCoin.success) return withCoin;
    
    return withCoin.change < withoutCoin.change ? withCoin : withoutCoin;
  }
}

2. Coin Health Monitoring

JavaScript
class CoinHealthMonitor {
  constructor(client) {
    this.client = client;
    this.thresholds = {
      fragmentationWarning: 10,
      fragmentationCritical: 20,
      dustThreshold: 1000000, // 0.001 SUI
      consolidationThreshold: 5
    };
  }
  
  async assessCoinHealth(owner, coinType) {
    const coins = await getAllCoinsWithPagination(owner, coinType);
    
    if (coins.length === 0) {
      return { status: 'no_coins', message: 'No coins found' };
    }
    
    const totalBalance = coins.reduce((sum, coin) => sum + parseInt(coin.balance), 0);
    const avgBalance = totalBalance / coins.length;
    const balances = coins.map(coin => parseInt(coin.balance)).sort((a, b) => b - a);
    
    // Calculate fragmentation metrics
    const fragmentationScore = coins.length / Math.sqrt(avgBalance);
    const dustCoins = coins.filter(coin => parseInt(coin.balance) < this.thresholds.dustThreshold);
    const largestCoin = Math.max(...balances);
    const smallestCoin = Math.min(...balances);
    const balanceDistribution = this.calculateBalanceDistribution(balances);
    
    // Determine health status
    let status, recommendations = [];
    
    if (coins.length === 1) {
      status = 'optimal';
    } else if (coins.length <= this.thresholds.consolidationThreshold) {
      status = 'good';
    } else if (coins.length <= this.thresholds.fragmentationWarning) {
      status = 'warning';
      recommendations.push('Consider consolidating coins to reduce gas costs');
    } else {
      status = 'critical';
      recommendations.push('Urgent: High fragmentation detected');
      recommendations.push('Consolidate coins to optimize gas usage');
    }
    
    // Add dust-specific recommendations
    if (dustCoins.length > 0) {
      recommendations.push(`${dustCoins.length} dust coins detected (< 0.001 SUI)`);
      if (dustCoins.length >= 3) {
        recommendations.push('Consolidate dust coins to recover storage deposits');
      }
    }
    
    return {
      status,
      coinCount: coins.length,
      totalBalance,
      avgBalance,
      largestCoin,
      smallestCoin,
      dustCoins: dustCoins.length,
      fragmentationScore,
      balanceDistribution,
      recommendations,
      healthScore: this.calculateHealthScore(coins.length, fragmentationScore, dustCoins.length)
    };
  }
  
  calculateBalanceDistribution(balances) {
    const total = balances.reduce((sum, b) => sum + b, 0);
    const distribution = [];
    
    for (let i = 0; i < Math.min(5, balances.length); i++) {
      distribution.push({
        rank: i + 1,
        balance: balances[i],
        percentage: (balances[i] / total) * 100
      });
    }
    
    return distribution;
  }
  
  calculateHealthScore(coinCount, fragmentationScore, dustCount) {
    let score = 100;
    
    // Penalize for too many coins
    if (coinCount > 5) score -= (coinCount - 5) * 5;
    
    // Penalize for fragmentation
    score -= Math.min(40, fragmentationScore * 2);
    
    // Penalize for dust
    score -= dustCount * 3;
    
    return Math.max(0, Math.min(100, score));
  }
}

Performance Optimization Strategies

1. Intelligent Caching

JavaScript
class SmartCoinCache {
  constructor(client, cacheTimeout = 30000) {
    this.client = client;
    this.cache = new Map();
    this.cacheTimeout = cacheTimeout;
  }
  
  generateCacheKey(owner, coinType, cursor, limit) {
    return `${owner}:${coinType || 'all'}:${cursor || 'start'}:${limit}`;
  }
  
  async getCoinsWithCache(owner, coinType, cursor, limit) {
    const key = this.generateCacheKey(owner, coinType, cursor, limit);
    const cached = this.cache.get(key);
    
    if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
      return cached.data;
    }
    
    const result = await this.client.getCoins({
      owner,
      coinType,
      cursor,
      limit
    });
    
    // Only cache successful results
    if (result.data) {
      this.cache.set(key, {
        data: result,
        timestamp: Date.now()
      });
    }
    
    return result;
  }
  
  invalidateOwnerCache(owner) {
    // Remove all cached entries for a specific owner
    for (const [key] of this.cache) {
      if (key.startsWith(`${owner}:`)) {
        this.cache.delete(key);
      }
    }
  }
  
  getCacheStats() {
    return {
      size: this.cache.size,
      keys: Array.from(this.cache.keys())
    };
  }
}

2. Batch Processing for Multiple Owners

JavaScript
async function batchProcessOwners(owners, coinType = null) {
  const batchSize = 5; // Process 5 owners concurrently
  const results = new Map();
  
  for (let i = 0; i < owners.length; i += batchSize) {
    const batch = owners.slice(i, i + batchSize);
    
    const batchPromises = batch.map(async owner => {
      try {
        const coins = await getAllCoinsWithPagination(owner, coinType);
        return { owner, coins, success: true };
      } catch (error) {
        return { owner, error: error.message, success: false };
      }
    });
    
    const batchResults = await Promise.all(batchPromises);
    
    batchResults.forEach(result => {
      results.set(result.owner, result);
    });
    
    console.log(`Processed batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(owners.length / batchSize)}`);
    
    // Rate limiting between batches
    if (i + batchSize < owners.length) {
      await new Promise(resolve => setTimeout(resolve, 200));
    }
  }
  
  return results;
}

Error Handling and Edge Cases

1. Robust Error Handling

JavaScript
async function robustGetCoins(owner, coinType = null, maxRetries = 3) {
  let attempt = 0;
  let lastError;
  
  while (attempt < maxRetries) {
    try {
      // Validate inputs
      if (!/^0x[a-fA-F0-9]{64}$/.test(owner)) {
        throw new Error('Invalid owner address format');
      }
      
      if (coinType && !/^0x[a-fA-F0-9]+::\w+::\w+$/.test(coinType)) {
        throw new Error('Invalid coin type format');
      }
      
      const result = await client.getCoins({
        owner,
        coinType,
        limit: 50
      });
      
      // Validate response structure
      if (!result.data || !Array.isArray(result.data)) {
        throw new Error('Invalid response structure');
      }
      
      // Validate coin objects
      const validCoins = result.data.filter(coin => {
        return coin.coinObjectId && 
               coin.coinType && 
               coin.balance && 
               !isNaN(parseInt(coin.balance));
      });
      
      if (validCoins.length !== result.data.length) {
        console.warn(`Filtered ${result.data.length - validCoins.length} invalid coins`);
      }
      
      return {
        ...result,
        data: validCoins
      };
      
    } catch (error) {
      attempt++;
      lastError = error;
      
      console.warn(`Attempt ${attempt} failed:`, error.message);
      
      if (attempt < maxRetries) {
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  throw new Error(`Failed after ${maxRetries} attempts: ${lastError?.message}`);
}

Best Practices

1. Efficient Pagination

  • Use appropriate page sizes: 20-50 items for UI, larger for batch processing
  • Implement cursor-based navigation for better user experience
  • Cache pagination results to avoid repeated queries
  • Handle incomplete pages gracefully at the end of datasets

2. Transaction Optimization

  • Select minimal coins needed for transactions to reduce gas costs
  • Prefer single coin transactions when possible
  • Consolidate fragmented coins during low network activity
  • Consider gas costs when selecting between multiple viable coin combinations

3. Performance Management

  • Cache frequently accessed data with appropriate TTL
  • Batch multiple owner queries to reduce API load
  • Use filtering to reduce unnecessary data transfer
  • Implement retry logic with exponential backoff

4. User Experience

  • Show loading states during pagination
  • Display meaningful coin information (balance, type, age)
  • Provide consolidation suggestions for fragmented portfolios
  • Handle edge cases like zero balances gracefully

5. Security Considerations

  • Validate all inputs before making RPC calls
  • Handle rate limiting appropriately
  • Don't expose sensitive coin selection logic to untrusted code
  • Implement proper error boundaries to prevent application crashes

Need help? Contact our support team or check the Sui documentation.