Using Address Balances
Address balances provide a canonical balance for each coin type tied directly to a Sui address. Funds sent through sui::coin::send_funds and sui::balance::send_funds merge automatically into a single balance per coin type, with no object management required. For the full specification, see SIP-58: Sui Address Balances.
Sending funds
Use send_funds to deposit directly into a recipient address balance. The recipient balance increases without creating new objects, and multiple deposits from different senders all merge into one balance.
TypeScript SDK
To deposit tokens into a recipient's address balance use tx.balance() with balance::send_funds instead of creating a coin object:
const tx = new Transaction();
tx.moveCall({
target: '0x2::balance::send_funds',
typeArguments: ['0x2::sui::SUI'],
arguments: [tx.balance({ balance: 1_000_000_000n }), tx.pure.address('0xRecipientAddress')],
});
Transactions built using only tx.balance() and eligible Move calls like send_funds and redeem_funds might qualify for zero gas fees when the network supports it.
CLI
To send funds using Sui CLI:
# Send from gas coin to address balance
sui client ptb \
--split-coins gas '[5000000]' \
--assign coin \
--move-call 0x2::coin::send_funds '<0x2::sui::SUI>' coin @<RECIPIENT_ADDRESS>
# Send from another coin
sui client ptb \
--split-coins @<COIN_ID> '[5000000]' \
--assign coin \
--move-call 0x2::coin::send_funds '<COIN_TYPE>' coin @<RECIPIENT_ADDRESS>
| Flag | Description |
|---|---|
--to | Recipient address. |
--amount | Amount to send in MIST. |
--coin-type | Coin type to send. Defaults to 0x2::sui::SUI. |
--all-coins | Send all coin balance of the specified type. Cannot combine with --stateless. |
--stateless | Build a stateless transaction using address balance only (no owned object inputs). Uses ValidDuring expiration for replay protection. |
By default, the command selects coins first and falls back to address balance if coins are insufficient. With --stateless, it draws exclusively from the address balance.
Move functions
The Sui framework provides send_funds in both the balance and coin modules:
// Send a Balance<T> to an address balance
public fun send_funds<T>(balance: Balance<T>, recipient: address)
// Send a Coin<T> to an address balance (converts to balance internally)
public fun send_funds<T>(coin: Coin<T>, recipient: address)
Learn more at sui-framework/sources/balance.move) and sui-framework/sources/coin.move.
Withdrawing funds
Create a withdrawal input and redeem it to get a Coin<T>:
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');
Get a Balance<T> directly:
const [balance] = tx.moveCall({
target: '0x2::balance::redeem_funds',
typeArguments: ['0x2::sui::SUI'],
arguments: [tx.withdrawal({ amount: 1_000_000_000 })],
});
For coin types other than SUI, pass the type parameter:
const [coin] = tx.moveCall({
target: '0x2::coin::redeem_funds',
typeArguments: ['0xPackageId::module::USDC'],
arguments: [tx.withdrawal({ amount: 1_000_000, type: '0xPackageId::module::USDC' })],
});
To use address balance funds in a transaction, withdraw them and redeem them using Coin<T> or Balance<T>.
TypeScript SDK
If you are not explicitly selecting coin objects for gas or as transaction arguments, the TypeScript SDK automatically checks both coin objects and address balances and draws from the correct sources.
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
tx.transferObjects([tx.coin({ balance: requiredAmount })], recipient);
TypeScript SDK: Manual withdrawals
For explicit control over address balance withdrawals, use tx.withdrawal(). The withdrawal must be redeemed through redeem_funds to produce a usable Coin<T> or Balance<T>:
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
// Withdraw SUI from address balance and redeem to Coin
const [coin] = tx.moveCall({
target: '0x2::coin::redeem_funds',
typeArguments: ['0x2::sui::SUI'],
arguments: [tx.withdrawal({ amount: 1_000_000_000 })], // 1 SUI in MIST
});
// Withdraw a custom coin type
const coinType = '0xPackageId::module::CoinType';
const [customCoin] = tx.moveCall({
target: '0x2::coin::redeem_funds',
typeArguments: [coinType],
arguments: [tx.withdrawal({ amount: 1_000_000, type: coinType })],
});
// Transfer to recipient
tx.transferObjects([coin], recipient);
tx.withdrawal() creates a Withdrawal<Balance<T>> capability that must be redeemed to create a coin or balance. Use 0x2::coin::redeem_funds() or 0x2::balance::redeem_funds() to do this. The coinWithBalance intent does this automatically.
Rust SDK: Manual withdrawals
Use FundsWithdrawalArg to withdraw from an address balance in Rust. The helper FundsWithdrawalArg::balance_from_sender(amount, balance_type) provides a convenient constructor:
use sui_types::transaction::{FundsWithdrawalArg, WithdrawalTypeArg, Reservation, WithdrawFrom};
let withdrawal_arg = FundsWithdrawalArg {
reservation: Reservation::MaxAmountU64(amount),
type_arg: WithdrawalTypeArg::Balance(coin_type),
withdraw_from: WithdrawFrom::Sender,
};
In your PTB, pass the withdrawal input to either:
0x2::balance::redeem_funds<T>()to obtain aBalance<T>.0x2::coin::redeem_funds<T>()to obtain aCoin<T>.
You can have multiple FundsWithdrawalArg inputs in a PTB, even for the same coin type:
let mut builder = ProgrammableTransactionBuilder::new();
let withdraw_arg = FundsWithdrawalArg::balance_from_sender(
withdraw_amount,
sui_types::gas_coin::GAS::type_tag(),
);
let withdraw_input = builder.funds_withdrawal(withdraw_arg).unwrap();
// Redeem to coin
let coin = builder.programmable_move_call(
SUI_FRAMEWORK_PACKAGE_ID,
Identifier::new("coin").unwrap(),
Identifier::new("redeem_funds").unwrap(),
vec!["0x2::sui::SUI".parse().unwrap()],
vec![withdraw_input],
);
builder.transfer_arg(recipient, coin);
Learn more at sui-types/src/programmable_transaction_builder.rs.
Splitting and joining withdrawals
You can split and merge withdrawals within a PTB:
// Split a sub-withdrawal from an existing withdrawal
public fun withdrawal_split<T: store>(withdrawal: &mut Withdrawal<T>, sub_limit: u256): Withdrawal<T>
// Join two withdrawals together (must have same owner)
public fun withdrawal_join<T: store>(withdrawal: &mut Withdrawal<T>, other: Withdrawal<T>)
Paying for gas from address balances
With address balance gas payments, pass an empty array to setGasPayment. This enables fully offline transaction building because there are no coin object versions to look up:
const tx = new Transaction();
tx.setGasPayment([]); // Empty array = use address balance for gas
In Rust, set gas_data.payment to an empty vector and use ValidDuring expiration:
TransactionData::V1(TransactionDataV1 {
kind: tx_kind,
sender,
gas_data: GasData {
payment: vec![], // Empty - gas paid from address balance
owner: sender,
price: rgp,
budget: 10_000_000,
},
expiration: TransactionExpiration::ValidDuring {
min_epoch: Some(current_epoch),
max_epoch: Some(current_epoch + 1),
min_timestamp: None,
max_timestamp: None,
chain: chain_identifier,
nonce: unique_nonce,
},
})
Requirements for address balance gas payments:
gas_data.paymentmust be empty.expirationmust beValidDuringwith bothmin_epochandmax_epochspecified.max_epochmust be at mostmin_epoch + 1(single epoch or 1-epoch range).- Timestamp-based expiration is not currently supported.
- The transaction kind must be
ProgrammableTransaction.
The nonce field differentiates otherwise identical transactions. Unlike EVM chains, the nonce has no semantic requirements. It does not need to be sequential, and there is no nonce gap problem. It simply allows you to submit two transactions that would otherwise have the same digest. For most use cases, generate the nonce randomly or use an incrementing counter in your application:
// Random nonce
let nonce: u32 = rand::random();
// Or incrementing counter
let nonce: u32 = self.next_nonce.fetch_add(1, Ordering::SeqCst);
Verify that address balance gas payments are enabled
Before using address balance gas payments, you can verify that the feature is enabled on the network by checking the protocol configuration flag:
const network = 'testnet';
const networkUrls = {
mainnet: 'https://fullnode.mainnet.sui.io:443',
testnet: 'https://fullnode.testnet.sui.io:443',
devnet: 'https://fullnode.devnet.sui.io:443',
};
const client = new SuiGrpcClient({
network,
baseUrl: networkUrls[network],
});
const { response } = await client.ledgerService.getEpoch({
readMask: {
paths: ['protocol_config.feature_flags'],
},
});
const enabled =
response.epoch?.protocolConfig?.featureFlags['enable_address_balance_gas_payments'] ?? false;
console.log(`enable_address_balance_gas_payments on ${network}:`, enabled);
Sponsored transactions
Address balances simplify sponsored transactions compared to coin-based sponsorship.
- No gas coin locking risk.
- The sponsor does not need to manage gas coin inventory.
- The user can sign before the sponsor, enabling simpler async flows.
- Enables permissionless public gas stations.
The GasCoin argument is still valid here, and usage should still be monitored in sponsored transactions even when using address balance gas payment.
With address balances, the user signs first and the sponsor signs afterward:
// 1. User builds and signs the transaction first
const tx = new Transaction();
tx.setSender(userAddress);
tx.setGasOwner(sponsorAddress);
tx.setGasPayment([]); // Empty array = sponsor pays from address balance
// ... add commands ...
const bytes = await tx.build({ client });
const { signature: userSignature } = await userKeypair.signTransaction(bytes);
// Option A: Send transaction and signature to sponsor, sponsor can sign and execute immediately:
// (`send_tx_to_sponsor` is a placeholder, there is no such API in the SDK)
await send_tx_to_sponsor(userSignature, bytes);
// Option B: Get signature from sponsor and submit it ourselves. Sponsor signs (can happen asynchronously)
const { signature: sponsorSignature } = await sponsorKeypair.signTransaction(bytes);
const result = await client.executeTransaction({
transaction: bytes,
signatures: [userSignature, sponsorSignature],
});
Both sender and sponsor must sign. Storage rebates are credited to the sponsor address balance.
Querying balances
TypeScript SDK
Use getBalance to see both coin objects and address balance:
const { balance } = await grpcClient.getBalance({
owner: '0xMyAddress',
});
console.log(balance.balance); // total balance as string (coin objects + address balance)
console.log(balance.coinBalance); // balance from coin objects only
console.log(balance.addressBalance); // balance from address balance only
console.log(balance.coinType); // e.g. "0x2::sui::SUI"
All balance values are returned as strings. Use BigInt(balance.balance) for numerical types.
gRPC
Use GetBalance and ListBalances from StateService:
buf curl --protocol grpc https://fullnode.testnet.sui.io/sui.rpc.v2.StateService/GetBalance \
-d '{
"owner": "<ADDRESS>",
"coin_type": "0x2::sui::SUI"
}'
# List all balances
buf curl --protocol grpc https://fullnode.testnet.sui.io/sui.rpc.v2.StateService/ListBalances \
-d '{"owner": "<ADDRESS>"}'
GraphQL
Copy the examples below into GraphQL IDE (Testnet) to try them out:
# Single balance
{
address(address: "0xe4ee9c157b5eb185c2df885bd7dcb4be493630a913f4b0e0c7e8ecf77930a878") {
balance(coinType: "0x2::sui::SUI") {
coinType {
repr
}
addressBalance
coinBalance
totalBalance
}
}
}
# All balances
{
address(address: "0xe4ee9c157b5eb185c2df885bd7dcb4be493630a913f4b0e0c7e8ecf77930a878") {
balances {
nodes {
coinType {
repr
}
addressBalance
coinBalance
totalBalance
}
}
}
}
For more details, see GraphQL Reference - IAddressable.balance.
Computing balance changes from checkpoint data
With address balances, you must also process accumulator events from TransactionEffects when computing balance changes:
use sui_types::balance_change::{derive_balance_changes, BalanceChange};
let balance_changes: Vec<BalanceChange> = derive_balance_changes(
&effects,
&input_objects,
&output_objects,
);
The algorithm:
- Subtract input coins: For each input coin object, subtract its value from
(owner, coin_type). - Add output coins: For each mutated or created coin object, add its value to
(owner, coin_type). - Process accumulator events: For each accumulator event with a
Balance<T>type:Splitoperation: Subtract the amount (funds withdrawn from address balance).Mergeoperation: Add the amount (funds deposited to address balance).
See the reference implementation for the full algorithm.
Accessing accumulator events
Accumulator events are embedded in TransactionEffects:
use sui_types::effects::TransactionEffectsAPI;
let events = effects.accumulator_events();
for event in events {
let address = event.write.address.address;
let balance_type = &event.write.address.ty;
// Only Balance<T> types represent balance changes
if let Some(coin_type) = Balance::maybe_get_balance_type_param(balance_type) {
let amount = match &event.write.value {
AccumulatorValue::Integer(v) => *v as i128,
_ => continue,
};
let signed_amount = match event.write.operation {
AccumulatorOperation::Split => -amount, // Withdrawal
AccumulatorOperation::Merge => amount, // Deposit
};
// (address, coin_type, signed_amount) represents the balance change
}
}
The BalanceChange struct represents the result:
pub struct BalanceChange {
pub address: SuiAddress,
pub coin_type: TypeTag,
pub amount: i128, // Negative = spent, positive = received
}
Converting existing coins to address balances
Migration is optional. To consolidate coins into address balances using Sui CLI:
sui client ptb \
--merge-coins @<COIN_1> '[@<COIN_2>, @<COIN_3>]' \
--move-call 0x2::coin::send_funds '<0x2::sui::SUI>' @<COIN_1> @<YOUR_ADDRESS>
For SUI coins, you can smash them as gas and use --move-call 0x2::coin::send_funds --gas-coin <COIN>.
The TypeScript SDK automatically selects coins or address balances as needed. Contracts that accept Coin<T> or Balance<T> remain callable. The redeem_funds function converts withdrawals to the expected types within the PTB.
Using the TypeScript SDK:
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
tx.moveCall({
target: '0x2::coin::send_funds',
typeArguments: ['0x2::sui::SUI'],
arguments: [tx.coin({ balance: totalAmount }), tx.pure.address(yourAddress)],
});
This might earn storage rebates from deleted coin objects.