suix_getCoins
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.
Parameters​
Parameter | Type | Required | Description |
---|---|---|---|
owner | string | Yes | The Sui address to query (66-character hex string with 0x prefix) |
coinType | string | No | Filter by specific coin type (defaults to all types) |
cursor | string | No | Pagination cursor from previous response |
limit | number | No | Maximum number of coins to return (default: 50, max: 100) |
Coin Type Filter​
- All coins: Omit
coinType
parameter or passnull
- Specific type: e.g.,
"0x2::sui::SUI"
for native SUI - Custom tokens: e.g.,
"0xpackage::module::TYPE"
Pagination​
- First request: Omit
cursor
parameter - Subsequent requests: Use
nextCursor
from previous response - Page size: Set
limit
(1-100, default 50)
Returns​
Returns a paginated response containing coin objects and pagination metadata.
Field | Type | Description |
---|---|---|
data | array | Array of coin objects |
nextCursor | string | Cursor for next page (null if no more pages) |
hasNextPage | boolean | Whether more pages are available |
Coin Object Structure​
Each coin object contains:
Field | Type | Description |
---|---|---|
coinType | string | The type of coin (e.g., "0x2::sui::SUI") |
coinObjectId | string | Unique identifier of the coin object |
version | string | Current version/sequence number |
digest | string | Object content digest |
balance | string | Balance of this specific coin object |
previousTransaction | string | Previous transaction that modified this coin |
Code Examples​
- cURL
- JavaScript
- Python
# Get all coins (first page)
curl -X POST https://sui-mainnet.dwellir.com/YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "suix_getCoins",
"params": [
"0xd77955e670601c2c2e6e8637e383695c166aac0a86b741c266bdfb23c2e3369f",
null,
null,
50
],
"id": 1
}'
# Get SUI coins only
curl -X POST https://sui-mainnet.dwellir.com/YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "suix_getCoins",
"params": [
"0xd77955e670601c2c2e6e8637e383695c166aac0a86b741c266bdfb23c2e3369f",
"0x2::sui::SUI",
null,
20
],
"id": 1
}'
# Get next page using cursor
curl -X POST https://sui-mainnet.dwellir.com/YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "suix_getCoins",
"params": [
"0xd77955e670601c2c2e6e8637e383695c166aac0a86b741c266bdfb23c2e3369f",
null,
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
50
],
"id": 1
}'
import { SuiClient } from '@mysten/sui.js/client';
const client = new SuiClient({
url: 'https://sui-mainnet.dwellir.com/YOUR_API_KEY'
});
async function getCoins(owner, coinType = null, limit = 50) {
const result = await client.getCoins({
owner: owner,
coinType: coinType,
limit: limit
});
console.log(`Found ${result.data.length} coin objects`);
console.log(`Has next page: ${result.hasNextPage}`);
return result;
}
// Get all coins with pagination
async function getAllCoinsWithPagination(owner, coinType = null) {
let allCoins = [];
let cursor = null;
let hasNextPage = true;
while (hasNextPage) {
const result = await client.getCoins({
owner: owner,
coinType: coinType,
cursor: cursor,
limit: 50
});
allCoins.push(...result.data);
cursor = result.nextCursor;
hasNextPage = result.hasNextPage;
console.log(`Fetched ${result.data.length} coins, total: ${allCoins.length}`);
}
return allCoins;
}
// Analyze coin distribution
async function analyzeCoinDistribution(owner) {
const allCoins = await getAllCoinsWithPagination(owner);
const analysis = {
totalCoins: allCoins.length,
coinTypes: {},
totalValue: 0,
largestCoin: null,
smallestCoin: null
};
let maxBalance = 0;
let minBalance = Infinity;
allCoins.forEach(coin => {
const balance = parseInt(coin.balance);
// Track by coin type
if (!analysis.coinTypes[coin.coinType]) {
analysis.coinTypes[coin.coinType] = {
count: 0,
totalBalance: 0,
coins: []
};
}
analysis.coinTypes[coin.coinType].count++;
analysis.coinTypes[coin.coinType].totalBalance += balance;
analysis.coinTypes[coin.coinType].coins.push(coin);
analysis.totalValue += balance;
// Track largest and smallest
if (balance > maxBalance) {
maxBalance = balance;
analysis.largestCoin = coin;
}
if (balance < minBalance) {
minBalance = balance;
analysis.smallestCoin = coin;
}
});
return analysis;
}
// Select coins for transaction
async function selectCoinsForAmount(owner, coinType, targetAmount) {
const coins = await getAllCoinsWithPagination(owner, coinType);
// Sort coins by balance (largest first for efficient selection)
coins.sort((a, b) => parseInt(b.balance) - parseInt(a.balance));
const selectedCoins = [];
let totalSelected = 0;
for (const coin of coins) {
const balance = parseInt(coin.balance);
selectedCoins.push(coin);
totalSelected += balance;
if (totalSelected >= targetAmount) {
break;
}
}
if (totalSelected < targetAmount) {
throw new Error(`Insufficient balance: need ${targetAmount}, have ${totalSelected}`);
}
return {
selectedCoins,
totalSelected,
change: totalSelected - targetAmount,
coinCount: selectedCoins.length
};
}
// Coin consolidation helper
async function findCoinsToConsolidate(owner, coinType, minCoinCount = 5) {
const coins = await getAllCoinsWithPagination(owner, coinType);
if (coins.length < minCoinCount) {
return null;
}
// Group small coins for consolidation
const smallCoins = coins
.filter(coin => parseInt(coin.balance) < 1000000000) // Less than 1 SUI
.sort((a, b) => parseInt(a.balance) - parseInt(b.balance));
if (smallCoins.length < minCoinCount) {
return null;
}
return {
totalCoins: coins.length,
coinsToConsolidate: smallCoins.slice(0, 10), // Max 10 per tx
potentialSavings: smallCoins.slice(0, 10).length - 1 // Save storage costs
};
}
// Example usage
const owner = '0xd77955e670601c2c2e6e8637e383695c166aac0a86b741c266bdfb23c2e3369f';
// Get SUI coins
const suiCoins = await getCoins(owner, '0x2::sui::SUI');
console.log('SUI Coins:', suiCoins.data);
// Get all coins with analysis
const analysis = await analyzeCoinDistribution(owner);
console.log('Coin Distribution Analysis:', analysis);
// Select coins for a 5 SUI payment
try {
const selection = await selectCoinsForAmount(
owner,
'0x2::sui::SUI',
5_000_000_000 // 5 SUI in MIST
);
console.log('Selected coins for payment:', selection);
} catch (error) {
console.error('Coin selection failed:', error.message);
}
// Check for consolidation opportunities
const consolidation = await findCoinsToConsolidate(owner, '0x2::sui::SUI');
if (consolidation) {
console.log('Consolidation opportunity:', consolidation);
}
import requests
import json
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass
@dataclass
class CoinObject:
coin_type: str
coin_object_id: str
version: str
digest: str
balance: int
previous_transaction: str
class SuiCoinManager:
def __init__(self, rpc_url: str):
self.rpc_url = rpc_url
def get_coins(
self,
owner: str,
coin_type: Optional[str] = None,
cursor: Optional[str] = None,
limit: int = 50
) -> Dict[str, Any]:
"""Get coins with pagination support"""
params = [owner]
if coin_type:
params.append(coin_type)
else:
params.append(None)
if cursor:
params.append(cursor)
else:
params.append(None)
params.append(limit)
payload = {
"jsonrpc": "2.0",
"method": "suix_getCoins",
"params": params,
"id": 1
}
response = requests.post(
self.rpc_url,
headers={'Content-Type': 'application/json'},
data=json.dumps(payload)
)
result = response.json()
if 'error' in result:
raise Exception(f"RPC Error: {result['error']}")
return result['result']
def get_all_coins_paginated(
self,
owner: str,
coin_type: Optional[str] = None,
max_pages: int = 100
) -> List[CoinObject]:
"""Get all coins using pagination"""
all_coins = []
cursor = None
page = 0
while page < max_pages:
result = self.get_coins(owner, coin_type, cursor, 50)
# Convert to CoinObject instances
for coin_data in result['data']:
coin = CoinObject(
coin_type=coin_data['coinType'],
coin_object_id=coin_data['coinObjectId'],
version=coin_data['version'],
digest=coin_data['digest'],
balance=int(coin_data['balance']),
previous_transaction=coin_data['previousTransaction']
)
all_coins.append(coin)
print(f"Page {page + 1}: {len(result['data'])} coins")
if not result['hasNextPage']:
break
cursor = result['nextCursor']
page += 1
print(f"Total coins retrieved: {len(all_coins)}")
return all_coins
def analyze_coin_distribution(self, owner: str) -> Dict[str, Any]:
"""Analyze coin distribution across types"""
all_coins = self.get_all_coins_paginated(owner)
analysis = {
'total_coins': len(all_coins),
'coin_types': {},
'total_value': 0,
'largest_coin': None,
'smallest_coin': None,
'fragmentation_score': 0
}
max_balance = 0
min_balance = float('inf')
for coin in all_coins:
# Track by coin type
if coin.coin_type not in analysis['coin_types']:
analysis['coin_types'][coin.coin_type] = {
'count': 0,
'total_balance': 0,
'avg_balance': 0,
'coins': []
}
type_info = analysis['coin_types'][coin.coin_type]
type_info['count'] += 1
type_info['total_balance'] += coin.balance
type_info['coins'].append(coin.coin_object_id)
analysis['total_value'] += coin.balance
# Track extremes
if coin.balance > max_balance:
max_balance = coin.balance
analysis['largest_coin'] = {
'coin_object_id': coin.coin_object_id,
'balance': coin.balance,
'coin_type': coin.coin_type
}
if coin.balance < min_balance:
min_balance = coin.balance
analysis['smallest_coin'] = {
'coin_object_id': coin.coin_object_id,
'balance': coin.balance,
'coin_type': coin.coin_type
}
# Calculate average balances and fragmentation
for coin_type, info in analysis['coin_types'].items():
info['avg_balance'] = info['total_balance'] / info['count']
# Fragmentation score: higher = more fragmented
if info['count'] > 1:
analysis['fragmentation_score'] += info['count'] / info['avg_balance']
return analysis
def select_coins_for_amount(
self,
owner: str,
coin_type: str,
target_amount: int
) -> Dict[str, Any]:
"""Select optimal coins for a specific amount"""
coins = self.get_all_coins_paginated(owner, coin_type)
if not coins:
raise ValueError(f"No coins found of type {coin_type}")
# Sort by balance (largest first for greedy selection)
coins.sort(key=lambda x: x.balance, reverse=True)
selected_coins = []
total_selected = 0
for coin in coins:
selected_coins.append(coin)
total_selected += coin.balance
if total_selected >= target_amount:
break
if total_selected < target_amount:
raise ValueError(
f"Insufficient balance: need {target_amount}, have {total_selected}"
)
return {
'selected_coins': [c.coin_object_id for c in selected_coins],
'coin_objects': selected_coins,
'total_selected': total_selected,
'change': total_selected - target_amount,
'coin_count': len(selected_coins),
'efficiency': target_amount / total_selected # Closer to 1 is better
}
def find_consolidation_opportunities(
self,
owner: str,
coin_type: str,
min_coins: int = 5,
max_coins_per_tx: int = 10
) -> Optional[Dict[str, Any]]:
"""Find opportunities to consolidate small coins"""
coins = self.get_all_coins_paginated(owner, coin_type)
if len(coins) < min_coins:
return None
# Identify small coins (less than average)
total_balance = sum(coin.balance for coin in coins)
avg_balance = total_balance / len(coins)
small_coins = [coin for coin in coins if coin.balance < avg_balance]
small_coins.sort(key=lambda x: x.balance) # Smallest first
if len(small_coins) < min_coins:
return None
# Group into consolidation batches
batches = []
current_batch = []
for coin in small_coins:
current_batch.append(coin)
if len(current_batch) >= max_coins_per_tx:
batches.append(current_batch)
current_batch = []
if current_batch:
batches.append(current_batch)
return {
'total_coins': len(coins),
'small_coins_count': len(small_coins),
'consolidation_batches': len(batches),
'coins_per_batch': [len(batch) for batch in batches],
'potential_savings': sum(len(batch) - 1 for batch in batches),
'first_batch_coins': [c.coin_object_id for c in batches[0]] if batches else []
}
def get_coin_history(self, coin_object_id: str, depth: int = 10) -> List[Dict[str, Any]]:
"""Get transaction history for a specific coin (simplified)"""
# This would require additional RPC calls to get transaction history
# For now, return basic info
return [{
'coin_object_id': coin_object_id,
'note': 'Full history requires additional transaction queries'
}]
def calculate_optimal_coin_strategy(
self,
owner: str,
coin_type: str
) -> Dict[str, Any]:
"""Calculate optimal coin management strategy"""
coins = self.get_all_coins_paginated(owner, coin_type)
analysis = self.analyze_coin_distribution(owner)
type_info = analysis['coin_types'].get(coin_type, {})
if not type_info:
return {'strategy': 'no_coins', 'message': 'No coins of this type found'}
coin_count = type_info['count']
avg_balance = type_info['avg_balance']
total_balance = type_info['total_balance']
# Determine strategy based on coin distribution
if coin_count == 1:
strategy = 'single_coin'
recommendation = 'Optimal - single coin object'
elif coin_count <= 3:
strategy = 'well_consolidated'
recommendation = 'Good - few coin objects'
elif coin_count <= 10:
strategy = 'moderate_fragmentation'
recommendation = 'Consider consolidating small coins'
else:
strategy = 'high_fragmentation'
recommendation = 'Consolidation recommended to save on gas costs'
# Calculate potential gas savings from consolidation
if coin_count > 3:
potential_consolidations = coin_count - 3 # Target 3 coins
estimated_gas_savings = potential_consolidations * 1000000 # Rough estimate
else:
estimated_gas_savings = 0
return {
'strategy': strategy,
'recommendation': recommendation,
'coin_count': coin_count,
'avg_balance': avg_balance,
'total_balance': total_balance,
'fragmentation_score': coin_count / max(avg_balance, 1),
'potential_gas_savings': estimated_gas_savings,
'consolidation_priority': 'high' if coin_count > 10 else 'medium' if coin_count > 5 else 'low'
}
# Usage examples
manager = SuiCoinManager('https://sui-mainnet.dwellir.com/YOUR_API_KEY')
# Example 1: Get SUI coins with pagination
owner = '0xd77955e670601c2c2e6e8637e383695c166aac0a86b741c266bdfb23c2e3369f'
result = manager.get_coins(owner, '0x2::sui::SUI', limit=20)
print(f"Found {len(result['data'])} SUI coins")
print(f"Has next page: {result['hasNextPage']}")
# Example 2: Analyze complete coin distribution
analysis = manager.analyze_coin_distribution(owner)
print(f"\nTotal coins: {analysis['total_coins']}")
print(f"Coin types: {len(analysis['coin_types'])}")
for coin_type, info in analysis['coin_types'].items():
print(f" {coin_type}: {info['count']} coins, avg balance: {info['avg_balance']:.0f}")
# Example 3: Select coins for payment
try:
selection = manager.select_coins_for_amount(
owner,
'0x2::sui::SUI',
5_000_000_000 # 5 SUI
)
print(f"\nSelected {selection['coin_count']} coins for 5 SUI payment")
print(f"Total selected: {selection['total_selected'] / 1_000_000_000:.6f} SUI")
print(f"Change: {selection['change'] / 1_000_000_000:.6f} SUI")
print(f"Efficiency: {selection['efficiency']:.2%}")
except ValueError as e:
print(f"Payment selection failed: {e}")
# Example 4: Check consolidation opportunities
consolidation = manager.find_consolidation_opportunities(owner, '0x2::sui::SUI')
if consolidation:
print(f"\nConsolidation opportunity:")
print(f"Small coins: {consolidation['small_coins_count']}")
print(f"Batches needed: {consolidation['consolidation_batches']}")
print(f"Potential savings: {consolidation['potential_savings']} storage units")
else:
print("\nNo consolidation opportunities found")
# Example 5: Get optimal strategy
strategy = manager.calculate_optimal_coin_strategy(owner, '0x2::sui::SUI')
print(f"\nOptimal Strategy: {strategy['strategy']}")
print(f"Recommendation: {strategy['recommendation']}")
print(f"Consolidation Priority: {strategy['consolidation_priority']}")
Response Example​
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"data": [
{
"coinType": "0x2::sui::SUI",
"coinObjectId": "0x5d3c87e88bc566e3f10c66e0275a366001ffa8b86142adc78c744de6afffeb34",
"version": "31823924",
"digest": "HpSeCiMLG53N9FcHDrRTxwGhc4RVJa1seZhXYJ7KFpJe",
"balance": "1000000000",
"previousTransaction": "C2QKmxrYPNBJjNmLtZrzFt9tVfq7wo2JZBmc5nQKYFdt"
},
{
"coinType": "0x2::sui::SUI",
"coinObjectId": "0x7a8b9c0d1e2f3456789012345678901234567890123456789012345678901234",
"version": "18234567",
"digest": "8kRvNmPdY2zCQh7vWxEqGsKjFp9LtQbN5nMcAuDeF1vR",
"balance": "500000000",
"previousTransaction": "9mLkPqR7XzNy2bVcDwFeAsKjHp8LtQbN5nMcAuDeF1vR"
},
{
"coinType": "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
"coinObjectId": "0x3e4f5a6b7c8d9012345678901234567890123456789012345678901234567890",
"version": "12345678",
"digest": "4jNvPqR8YzCx5bWdEwGeAsKjHp9LtQbN5nMcAuDeF1vR",
"balance": "1000000",
"previousTransaction": "5oMpRqS9ZaDy6cXeExHfBtLkIq0MuRcO6oNdBvEgG2wS"
}
],
"nextCursor": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJvYmplY3RJZCI6IjB4M2U0ZjVhNmI3YzhkOTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwIn0.signature",
"hasNextPage": true
}
}
Common Use Cases​
1. Transaction Coin Selection​
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​
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​
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​
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​
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​
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​
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​
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​
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}`);
}
Related Methods​
- suix_getBalance - Get total balance for specific coin type
- suix_getAllBalances - Get all coin balances for address
- sui_multiGetObjects - Batch query multiple objects
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.