@mysten/sui v2.0 and a new dApp Kit are here! Check out the migration guide
Mysten Labs SDKs
Transactions

Coins and Balances

Tokens are the most common thing you work with in transactions. Sui has two systems for holding fungible token balances: coin objects and address balances. Understanding both is important for building reliable transactions.

Address balances are currently only available on devnet and testnet. They are not yet supported on mainnet.

Coin objects vs address balances

Coin objects are individual on-chain objects, each with its own ID, version, and balance. You own specific coin objects and need to split, merge, and track them.

Address balances are an accumulator per address per coin type. There are no objects to manage — deposits automatically merge into a single balance, and you withdraw from it as needed.

Both systems coexist. An address's total balance for a given coin type is the sum of its coin object balances and its address balance.

Checking balances

Use getBalance to see the full picture (see client docs for setup):

import { SuiGrpcClient } from '@mysten/sui/grpc';

const grpcClient = new SuiGrpcClient({
	network: 'mainnet',
	baseUrl: 'https://fullnode.mainnet.sui.io:443',
});

const { balance } = await grpcClient.getBalance({
	owner: '0xMyAddress',
});

console.log(balance.coinType); // '0x2::sui::SUI'
console.log(balance.balance); // total balance (coin objects + address balance)
console.log(balance.coinBalance); // balance from coin objects only
console.log(balance.addressBalance); // balance from address balance only

For a specific token type:

const { balance } = await grpcClient.getBalance({
	owner: '0xMyAddress',
	coinType: '0xPackageId::module::CoinType',
});

coinWithBalance is the recommended way to get a coin of any type in a transaction. It automatically draws from both coin objects and address balances:

import { coinWithBalance, Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();

// SUI (balance is in MIST — 1 SUI = 1,000,000,000 MIST)
tx.transferObjects([coinWithBalance({ balance: 1_000_000_000 })], '0xRecipientAddress');

// Another coin type
tx.transferObjects(
	[coinWithBalance({ balance: 1_000_000, type: '0xPackageId::module::CoinType' })],
	'0xRecipientAddress',
);

Under the hood, coinWithBalance looks up the sender's coins, finds enough to cover the requested balance, merges them if needed, and splits off the exact amount. You don't need to manage any of this yourself.

To force coinWithBalance to use only address balance withdrawals (skipping coin object lookups), set forceAddressBalance: true. This is useful for offline transaction building:

tx.transferObjects(
	[coinWithBalance({ balance: 1_000_000_000, forceAddressBalance: true })],
	'0xRecipientAddress',
);

Getting a Balance from coinWithBalance

Some Move functions expect a Balance<T> instead of a Coin<T>. Use 0x2::coin::into_balance to convert the coin:

import { coinWithBalance, Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();

const [balance] = tx.moveCall({
	target: '0x2::coin::into_balance',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [coinWithBalance({ balance: 1_000_000_000 })],
});

// Use the Balance<T> in a Move call that expects it
tx.moveCall({
	target: '0xPackage::module::deposit',
	arguments: [tx.object('0xPoolId'), balance],
});

Sending to address balances

To deposit tokens directly into a recipient's address balance (instead of creating a new coin object for them), use 0x2::coin::send_funds:

import { coinWithBalance, Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();

tx.moveCall({
	target: '0x2::coin::send_funds',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [coinWithBalance({ balance: 1_000_000_000 }), tx.pure.address('0xRecipientAddress')],
});

This avoids creating a new coin object for the recipient. The funds go directly into their address balance accumulator.

Manual coin operations

For fine-grained control, you can split and merge coins manually.

Splitting coins

splitCoins creates new coins from an existing coin:

const tx = new Transaction();

// Split specific amounts from a coin you own
const [coin1, coin2] = tx.splitCoins('0xMyCoinId', [1_000_000, 2_000_000]);

tx.transferObjects([coin1], '0xAlice');
tx.transferObjects([coin2], '0xBob');

You can also split the gas coin for SUI:

const [coin] = tx.splitCoins(tx.gas, [1_000_000_000]);
tx.transferObjects([coin], '0xRecipientAddress');

tx.gas does not work with address balances. If your SUI is held in address balances rather than coin objects, use coinWithBalance instead.

Merging coins

mergeCoins combines multiple coins into one. Source coins can be object IDs, tx.object() references, coinWithBalance results, or results from other commands:

import { coinWithBalance, Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();

// Merge by object ID
tx.mergeCoins('0xCoin1', ['0xCoin2', '0xCoin3']);

// Merge a coinWithBalance result into an existing coin
tx.mergeCoins(tx.object('0xCoin1'), [coinWithBalance({ balance: 500_000 })]);

// Merge a coin returned from a Move call
const [mintedCoin] = tx.moveCall({ target: '0xPackage::token::mint' });
tx.mergeCoins(tx.object('0xCoin1'), [mintedCoin]);

Working with address balances directly

Withdrawing from address balance

tx.withdrawal() creates a Withdrawal<Balance<T>> input that draws from your address balance. This is a withdrawal capability — to get a usable value, you must redeem it.

To get a Coin<T> (for transfers and most operations):

const tx = new Transaction();

const [coin] = tx.moveCall({
	target: '0x2::coin::redeem_funds',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [tx.withdrawal({ amount: 1_000_000_000 })],
});

tx.transferObjects([coin], '0xRecipientAddress');

To get a Balance<T> (for Move functions that expect a balance directly):

const [balance] = tx.moveCall({
	target: '0x2::balance::redeem_funds',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [tx.withdrawal({ amount: 1_000_000_000 })],
});

// Use the Balance<T> in a Move call
tx.moveCall({
	target: '0xPackage::module::deposit',
	arguments: [tx.object('0xPoolId'), balance],
});

For non-SUI coin types, pass the type parameter:

const withdrawal = tx.withdrawal({
	amount: 1_000_000,
	type: '0xPackageId::module::CoinType',
});

Depositing coins into address balances

Use 0x2::coin::send_funds to deposit a coin into a recipient's address balance. The coin object is consumed and the funds are added to the address balance accumulator:

const tx = new Transaction();

tx.moveCall({
	target: '0x2::coin::send_funds',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [tx.object('0xMyCoinObjectId'), tx.pure.address('0xRecipientAddress')],
});

You can also deposit into your own address balance to consolidate coin objects:

tx.moveCall({
	target: '0x2::coin::send_funds',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [
		tx.object('0xMyCoinObjectId'),
		tx.pure.address('0xMyAddress'), // send to yourself
	],
});

Converting coins to Balance objects

Balance<T> is a Move struct that holds a fungible token amount without being a standalone object. It's commonly used inside shared objects (like liquidity pools) to store funds. Use 0x2::coin::into_balance to convert a coin:

const tx = new Transaction();

const [balance] = tx.moveCall({
	target: '0x2::coin::into_balance',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [tx.object('0xMyCoinId')],
});

// Store the balance in a shared object
tx.moveCall({
	target: '0xPackage::pool::add_liquidity',
	arguments: [tx.object('0xPoolId'), balance],
});

To convert a Balance<T> back into a Coin<T>:

const [coin] = tx.moveCall({
	target: '0x2::coin::from_balance',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [balance],
});

Balance<T> values cannot be transferred between addresses — they must be stored inside objects or converted to Coin<T> first. Use send_funds to deposit directly into an address balance, or into_balance / from_balance for conversions within a transaction.

Listing coin objects

When you need to see specific coin objects (not just aggregate balances), use listCoins:

const { objects, hasNextPage, cursor } = await grpcClient.listCoins({
	owner: '0xMyAddress',
});

for (const coin of objects) {
	console.log(coin.objectId, coin.balance);
}

// For a specific type
const { objects: usdcCoins } = await grpcClient.listCoins({
	owner: '0xMyAddress',
	coinType: '0xPackageId::module::USDC',
});

Each coin object includes objectId, version, digest, balance, and type.

On this page