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.
What changes with address balances?
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.
What changes 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. See Using Address Balances. - Read the combined balance, or the separate
coinBalanceandaddressBalancefields, from the gRPCGetBalanceandListBalancesmethods or the GraphQLbalanceandbalancesfields. See Changes to balance queries. - When sending funds that include the address balance portion, use
coinWithBalancefrom the TypeScript SDK (v2+), which draws from both coin objects and the address balance, or manual withdrawal logic as described in Using Address Balances. - If you compute balance changes from checkpoint data, process accumulator events (the records of address balance withdrawals and deposits in
TransactionEffects, available througheffects.accumulator_events()) or usederive_balance_changes. See Changes to balance computation from checkpoint data. - Replace legacy JSON-RPC
suix_getBalanceandsuix_getAllBalancescalls with the gRPCGetBalanceandListBalancesmethods fromStateService, or the GraphQLbalanceandbalancesfields.
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. Do not call suix_getBalance or suix_getAllBalances in new code. The legacy response shape helps you recognize these calls in code you are migrating to the gRPC or GraphQL fields.
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.
How do you access 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.
Do you need to convert existing coins?
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.