Migrating from Coin to Address Balances
Address balances introduce a canonical balance system for fungible assets tied to Sui addresses. This supplements the Coin<T> model with direct address-owned balances, simplifying transaction construction and eliminating coin selection complexity.
For the full specification, see SIP-58: Sui Address Balances. For implementation details on sending, withdrawing, and paying gas from address balances, see Using Address Balances.
Read this first
This rollout initially has very limited impact:
- Nothing is deprecated or removed. All transactions that are currently valid continue to be valid. Coin objects are not forcibly migrated to address balances.
- No contracts need to be rewritten. Contracts can continue to accept
Coin<T>and&mut Coin<T>as before. - Coins can still be sent through
transfer::public_transferor theTransferObjectscommand.
Impact for wallets, exchanges, and custody providers
Anyone operating a wallet (custodial, non-custodial, or exchange wallets) might be impacted by this change. Users can now send funds to your address through send_funds(), which credits your address balance, instead of transferring a coin object with TransferObjects. If a wallet receives funds this way, but does not have proper support for address balances, the user cannot see or access the funds. The user might then believe the funds have been lost. The funds cannot actually be lost, but this can cause confusion for affected users.
Initially, this should not happen often because most of the ecosystem continues to transfer coin objects. However, because there is no way to prevent funds from arriving at any given wallet through an address balance transfer, wallet implementors should be prepared.
User funds can also become split across both coins and address balances. Balance queries through gRPC or GraphQL show the combined total. To see how much of that total is held in coin objects and how much is held in the address balance, read the separate coinBalance and addressBalance fields described in Changes to balance queries. To send funds that include the address balance portion, you must either:
- Use
coinWithBalancefrom the TypeScript SDK (v2+), which automatically draws from both sources. - Implement manual withdrawal logic as described in Using Address Balances.
What should I check in my integration?
- Handle funds that arrive through
send_funds()in the address balance, not only funds that arrive as coin objects. - Read the combined balance, or the separate
coinBalanceandaddressBalancefields, from gRPC or GraphQL balance queries. See Changes to balance queries. - When sending funds that include the address balance portion, use
coinWithBalancefrom the TypeScript SDK (v2+) or manual withdrawal logic. - If you compute balance changes from checkpoint data, process accumulator events or use
derive_balance_changes. See Changes to balance computation from checkpoint data. - Replace legacy JSON-RPC balance calls with the gRPC or GraphQL equivalents.
Changes to balance queries
Balance query responses now include address balance information alongside coin object totals.
JSON-RPC (legacy)
Sui Foundation disabled JSON-RPC on Mainnet full nodes. This section describes the legacy response shape so you can recognize it in code you are migrating. For new code, use the gRPC or GraphQL fields that follow.
The legacy suix_getBalance and suix_getAllBalances response carries a fundsInAddressBalance field, and its totalBalance field combines coin objects and address balance funds:
{
"coinType": "0x2::sui::SUI",
"coinObjectCount": 2,
"totalBalance": "99998990120",
"lockedBalance": {},
"fundsInAddressBalance": "5000000"
}
Getting only the coin-based balance means subtracting fundsInAddressBalance from totalBalance. The gRPC and GraphQL responses return the two amounts as separate fields, so you do not need that arithmetic.
gRPC
The GetBalance and ListBalances methods from StateService now return separate fields for coin and address balances:
coinBalance: Total held in coin objects.addressBalance: Amount held in address balance.balance: Sum of the two.
{
"balance": {
"coinType": "0x2::sui::SUI",
"balance": "99998990120",
"addressBalance": "5000000",
"coinBalance": "99993990120"
}
}
GraphQL
The balance and balances fields on address types now include addressBalance, coinBalance, and totalBalance fields. See the IAddressable.balance entry in the GraphQL reference for the full schema.
Changes to balance computation from checkpoint data
Use the balance changes that gRPC or GraphQL provide rather than computing them yourself.
If you compute balance changes from checkpoint data, you must now process accumulator events from TransactionEffects in addition to coin object diffs. Accumulator events are the records of address balance withdrawals and deposits, so coin object diffs alone miss funds that move through address balances.
The Sui framework provides a helper for this process:
use sui_types::balance_change::{derive_balance_changes, BalanceChange};
let balance_changes: Vec<BalanceChange> = derive_balance_changes(
&effects,
&input_objects,
&output_objects,
);
Each BalanceChange contains the address, coin type, and a signed amount (negative for spent, positive for received).
See the reference implementation for the full algorithm.
Accessing accumulator events
Accumulator events are embedded in TransactionEffects. Access them through the TransactionEffectsAPI trait:
use sui_types::effects::TransactionEffectsAPI;
let events = effects.accumulator_events();
Each event contains the target address, a balance type, an operation (Split for withdrawal, Merge for deposit), and the amount. See sui-types/src/effects/mod.rs for the full API.
Backward compatibility
Existing contracts
Contracts that accept Coin<T> or Balance<T> remain callable without changes. The redeem_funds functions (in the sui::coin and sui::balance modules respectively) convert withdrawals to the expected types within the programmable transaction block (PTB). For example:
inputs: [withdrawal<0x2::sui::SUI>(100)]
commands:
0: 0x2::coin::redeem_funds(inputs(0))
1: 0x2::transfer::public_transfer(result(0))
Compatibility coin reservations for legacy clients
A JSON-RPC compatibility layer presents compatibility coin reservations to clients that cannot upgrade. A compatibility coin reservation is a synthetic object reference that reserves address balance funds for backward compatibility with clients that still expect coin object references. It is not a persisted Coin<T> object.
These reservations exist primarily to preserve basic functionality for older clients that expect coin object references. Do not treat them as normal persisted coin objects. For new development, use address balances directly, as described in Using Address Balances.
When a legacy client supplies a compatibility coin reservation as gas payment, the network resolves it as a withdrawal from the address balance rather than smashing it into a coin. And gas smashing applies depending on the position of the coin reservation in the list of gas payments. For more information, see Gas Smashing.
Converting existing coins to address balances
Migration is optional. The SDK automatically selects coins or address balances as needed. To consolidate existing coin objects into an address balance, see Converting Existing Coins to Address Balances in the usage guide.
Troubleshooting
Why are funds shown in both coin objects and the address balance?
User funds can be split across coin objects and the address balance of the same address. Balance queries through gRPC or GraphQL return the combined total, and the separate coinBalance and addressBalance fields show each portion. See Changes to balance queries.
Why can a user not see funds that were sent to their address?
If the sender used send_funds() instead of transferring a coin object, the funds are in the address balance. A wallet without address balance support cannot show or access them, but the funds are not lost. Add address balance support as described in Using Address Balances.