Skip to Content
Supra Layer 1SupraMVMTracking SUPRA and Fungible Asset Balances

Tracking SUPRA and Fungible Asset Balances

This article is for an exchange, custodian or indexer that must know every change to the SUPRA balance, and to the balances of other fungible assets, of accounts it controls. It states where a balance is held, how to collect the transactions that change it, which events and fields show each kind of change, and how to check the sum of the changes against the balance the node reports.

Amounts are in quants, the smallest unit. Endpoints are relative to the RPC host, https://rpc-<network>.supra.com. Every response sample is abridged to the fields the step uses; the node’s API page gives the full shapes, at https://rpc-mainnet.supra.com/docs/ on Mainnet, https://rpc-testnet.supra.com/docs/ on Testnet and https://rpc-devnet.supra.com/docs on Devnet.

Trust

There is no light client for Supra state, and a balance view carries no proof. A balance view, an account’s transaction list and a block are as accurate as the node that serves them. A tracker that must not depend on a third party runs its own RPC node, which serves these responses from the blocks it has executed.

GET /rpc/v4/transactions/certificates?start_height=<h>&end_height=<h+1> returns the inclusion certificates of the blocks from start_height up to but not including end_height. Both parameters are required, and start_height must be smaller than end_height; otherwise the node answers 400. A certificate carries the consensus committee’s aggregate signature over the Merkle root accumulator of the block’s Move and EVM transactions, so it establishes that the committee accepted a transaction into a block. It does not cover state, and so not a balance.

Where SUPRA Is Held

An account holds SUPRA in up to two places at once:

PlaceResource and fieldAddress
CoinStore0x1::coin::CoinStore<0x1::supra_coin::SupraCoin>, coin.valueThe account’s address.
Primary store of 0xa0x1::fungible_asset::FungibleStore, balance0x1::primary_fungible_store::primary_store_address of the account and 0xa.

0xa is the metadata object of the fungible asset paired with SupraCoin. The balance of a store that has a 0x1::fungible_asset::ConcurrentFungibleBalance resource is read through the views below rather than from the FungibleStore resource.

Which place a SUPRA movement uses depends on the account and on two feature flags: NEW_ACCOUNTS_DEFAULT_TO_FA_SUPRA_STORE (64) decides whether a new account is registered with a primary store instead of a CoinStore. With OPERATIONS_DEFAULT_TO_FA_SUPRA_STORE (65) off, 0x1::supra_account::transfer is 0x1::coin::transfer, which can emit coin events and fungible asset events. With 65 on it moves value between primary stores only, emits no coin event, and creates an empty primary store for a sender that has none. 0x1::coin::withdraw takes from the CoinStore first and the remainder from the primary store, and 0x1::coin::deposit pays a CoinStore if the account has one and otherwise its primary store. A tracker reads both places for every account.

Reading the feature flags says how to read a flag.

Reading a Balance

POST /rpc/v3/view runs a view function against the node’s current state. In the examples, <RPC_HOST> is the RPC host with its scheme, such as https://rpc-devnet.supra.com:

curl --request POST --url '<RPC_HOST>/rpc/v3/view' \ --header 'Content-Type: application/json' \ --data '{"function":"0x1::coin::balance", "type_arguments":["0x1::supra_coin::SupraCoin"], "arguments":["<ACCOUNT>"]}'
{ "result": ["<balance in quants>"] }
ViewResult
0x1::coin::balance<0x1::supra_coin::SupraCoin>(account)The CoinStore balance plus the primary store balance: the total.
0x1::primary_fungible_store::balance<0x1::fungible_asset::Metadata>(account, "0xa")The primary store balance, 0 if the store does not exist.
0x1::fungible_asset::balance<0x1::fungible_asset::FungibleStore>(store)The balance of one store. The view aborts if the store does not exist.

The CoinStore part is the total minus the primary store part, or the coin.value of GET /rpc/v3/accounts/{address}/resources/0x1::coin::CoinStore%3C0x1::supra_coin::SupraCoin%3E. That resource request answers 404 for an account that has no CoinStore, such as one that has run coin::migrate_to_fungible_store; the CoinStore part of such an account is 0.

The total does not include a secondary store of 0xa that the account owns. A secondary store is read with the third view.

A view reports the node’s current state and takes no height. Two requests can return balances of different blocks.

Other Fungible Assets

An asset is named by the address of its 0x1::fungible_asset::Metadata object. An account’s balance of it is the sum over the stores the account owns:

  • The primary store, at the address 0x1::primary_fungible_store::primary_store_address returns for the account and the metadata address, with the type argument 0x1::fungible_asset::Metadata. A primary store is created with ungated transfer disabled.
  • Any number of secondary stores. A secondary store is an object that can change owner.

A 0x1::fungible_asset::Deposit or Withdraw event names the store and not the asset or the owner. Both are read from the store:

  • GET /rpc/v3/accounts/{store}/resources/0x1::fungible_asset::FungibleStore returns metadata.inner, the asset. The metadata of a store does not change, so a tracker reads it once, when it first sees the store. A store whose balance is 0 can be deleted, after which the resource is gone.
  • GET /rpc/v3/accounts/{store}/resources/0x1::object::ObjectCore returns owner. A secondary store changes owner with 0x1::object::Transfer (fields object, from, to) and its legacy twin 0x1::object::TransferEvent. An event is attributed to the owner at the position of the event in the block, so a tracker that follows ownership changes processes a block’s events in order. A transfer of a secondary store moves its balance between accounts with no Deposit or Withdraw.
{ "type": "0x1::object::ObjectCore", "data": { "owner": "0x<owner address>" } }

Reading the Feature Flags

GET /rpc/v3/accounts/0x1/resources/0x1::features::Features returns data.features, a hex string. Feature n is on when bit n % 8 of byte n / 8 is set. The flags that change what the events below look like:

FlagNumberEffect on the events
COLLECT_AND_DISTRIBUTE_GAS_FEES6How the epilogue takes the gas and credits the refund; see Gas.
EMIT_FEE_STATEMENT27FeeStatement is emitted for every charged transaction, together with MODULE_EVENT (26).
MODULE_EVENT_MIGRATION57CoinDeposit and CoinWithdraw are emitted next to the legacy coin events.
COIN_TO_FUNGIBLE_ASSET_MIGRATION60coin::migrate_to_fungible_store is callable.
NEW_ACCOUNTS_DEFAULT_TO_FA_SUPRA_STORE64New accounts get a primary store and no CoinStore.
OPERATIONS_DEFAULT_TO_FA_SUPRA_STORE65supra_account::transfer and the gas burn use primary stores only.
FEE_PAYER_ENABLED22Transactions can name a fee payer.

Collecting Changes

Per Account

GET /rpc/v4/accounts/{address}/history lists the finalized transactions that concern an account, each once, with the categories it matched for the account:

CategoryThe transaction
sentwas sent by the account.
coinemitted a coin event that names the account.
faemitted a fungible asset event for a store the account owned at that point in its block.
daemitted a digital asset or object event that names the account.

A balance tracker uses sent, coin and fa. coin also matches events of other coin types. The query parameters are categories (comma separated, default all four), count (default 20, at most 100), ascending (default false, newest first) and start, the cursor of the previous page.

curl --request GET \ --url '<RPC_HOST>/rpc/v4/accounts/<ACCOUNT>/history?categories=sent,coin,fa' \ --header 'Accept: application/json'
[ { "categories": ["sent", "coin"], "transaction": { "txn_type": "user", "hash": "0x...", "header": { "...": "..." }, "output": { "Move": { "gas_used": 508, "events": ["..."] } } } } ]

The transaction has the shape of a transaction in a block, described under By Block.

Response headerMeaning
x-supra-cursorPass it as start to continue. A response without it ends the history.
x-supra-oldest-blockThe height of the oldest block the node holds.
x-supra-history-indexed-fromThe lowest block the history covers, present while blocks below it are not indexed. The transactions of those blocks are not listed.

A page can hold fewer entries than count, or none, and still carry a cursor, because the node filters by categories while it scans. Continue until the header is absent. 0 is a valid cursor.

For a block that the node indexed by backfill, fa attributes only primary stores and the stores whose owner the block’s own events name, so the fa entries of a secondary store can be missing there.

history does not list two kinds of transaction that change balances:

  • Automated transactions, which are listed by GET /rpc/v4/accounts/{address}/automated_transactions. It lists the automated transactions of the tasks the account owns, with the query parameters count (default 20, at most 100), ascending, block_height (where to start) and cursor (from x-supra-cursor, which takes precedence over block_height). A response without x-supra-cursor ends the list.
  • Automation registry records, which charge and refund task fees. They are listed by GET /rpc/v4/accounts/{address}/coin_transactions?type=record, and by GET /rpc/v4/accounts/{store}/fa_transactions?type=record for a store. Both endpoints take count, ascending and start (the x-supra-cursor of the previous response), and apply type after reading a page, so a page can be empty and still carry a cursor.

An automated transaction also reaches other accounts: a recipient of a transfer that a task’s payload makes finds it in coin_transactions?type=auto or fa_transactions?type=auto, not in history.

No per-account endpoint lists the gas that a fee payer pays from a CoinStore, because no event names the payer; see Gas. A fee payer’s history shows a transaction it paid for only when an event names the payer, such as a Withdraw from its primary store.

For the blocks below x-supra-history-indexed-from, use the block scan.

By Block

GET /rpc/v4/block/height/{height}?with_finalized_transactions=true returns a block and the transactions it finalized. A tracker for many accounts reads every block once, in height order, and attributes each event itself.

curl --get --url '<RPC_HOST>/rpc/v4/block/height/<HEIGHT>' \ --data 'with_finalized_transactions=true' \ --header 'Accept: application/json'
{ "header": { "height": 2099199, "hash": "0x...", "timestamp": { "...": "..." } }, "transactions": [ { "txn_type": "user", "authenticator": { "Move": { "Ed25519": { "...": "..." } } }, "hash": "0x...", "header": { "sender": { "Move": "0x..." }, "sequence_number": 0, "gas_unit_price": 100000, "max_gas_amount": 5000 }, "status": "Success", "output": { "Move": { "gas_used": 508, "vm_status": "Executed successfully", "events": [{ "type": "0x1::coin::CoinDeposit", "data": {} }] } } } ] }
  • txn_type is user, automated, block_metadata, automation_record or settlement. The type query parameter takes user, auto, meta, record or settlement and returns only that kind. The array groups the transactions by kind: block metadata, settlement, automation record, user and then automated transactions.
  • output is keyed by the VM. A balance tracker reads entries whose output is Move. An Evm output belongs to SupraEVM.
  • output.Move.events holds the events in the order they were emitted. An event has guid (creation_number and account_address), sequence_number, type, data and, in v4, hash. A module event, such as 0x1::coin::CoinDeposit, has the zero guid. A legacy event, such as 0x1::coin::DepositEvent, has the guid of the handle that emitted it.
  • status is Success, Fail or Invalid. Success and Fail are kept transactions, whose gas is charged. An Invalid transaction changed no state. PendingAfterExecution marks a transaction that was ordered and discarded without changing state, and that may be executed in a later block; Pending marks one an RPC node has accepted and not yet executed. Neither changes a balance.
  • The sender is header.sender.Move. header.gas_unit_price and header.max_gas_amount are in quants. A transaction with a fee payer has authenticator.Move.FeePayer, whose fee_payer_address pays the gas.
  • An automated transaction has authenticator.Automation, and the sender is the owner of the task.
StatusMeaning
404height is above the node’s latest executed block. Request it again later.
410height is below the oldest block the node holds. The x-supra-oldest-block header names that block.

GET /rpc/v3/block returns the latest block’s header.height. A tracker keeps last_processed_height and requests last_processed_height + 1.

Every Way a Balance Changes

An account’s total SUPRA changes only in the ways below and in the two cases that follow the table. The events are in output.Move.events. A change to a CoinStore that the table marks as having no event is derived from FeeStatement.

Events That Show a Change

ChangeTransaction kindsEventAttribution
Coin deposit to a CoinStoreuser, automated, automation_record, and the genesis block_metadata0x1::coin::CoinDeposit (coin_type, account, amount) and the legacy 0x1::coin::DepositEvent (amount)data.account. For the legacy event, guid.account_address.
Coin withdrawal from a CoinStorethe same0x1::coin::CoinWithdraw and the legacy 0x1::coin::WithdrawEventThe same.
Fungible asset deposituser, automated, automation_record, settlement, and the genesis block_metadata0x1::fungible_asset::Deposit (store, amount)The owner of data.store at that position in the block.
Fungible asset withdrawalthe same0x1::fungible_asset::Withdraw (store, amount)The same.
Move of a CoinStore balance to the primary storeuser, automated, from coin::migrate_to_fungible_store0x1::coin::CoinEventHandleDeletion (event_handle_creation_address, deleted_deposit_event_handle_creation_number, deleted_withdraw_event_handle_creation_number), then 0x1::fungible_asset::Deposit of the whole CoinStore balance, which is absent when that balance is 0event_handle_creation_address is the account. The Deposit is a move between the account’s own stores, not income.
Gasuser, and automated for a user task0x1::transaction_fee::FeeStatement, and 0x1::fungible_asset::Withdraw for the part taken from a primary storeSee Gas.
Storage fee refunduser, automatedFeeStatement.storage_fee_refund_quants, and 0x1::fungible_asset::Deposit when the payer has no CoinStore and it is credited to the primary storeSee Gas.
Automation task registrationuser, automated0x1::automation_registry::TaskRegistrationDepositFeeWithdraw (task_index, owner, registration_fee, locked_deposit_fee) and coin eventsowner. The coin events move SUPRA to the registry fee address.
Automation task fee for a cycleautomation_record0x1::automation_registry::TaskEpochFeeWithdraw (task_index, owner, fee) and coin eventsowner.
Automation task refundsuser, automated (cancel_task, stop_tasks), automation_record0x1::automation_registry::TaskDepositFeeRefund and TaskFeeRefund (task_index, owner, amount), TasksStoppedV2 (tasks, owner), and coin eventsowner. The coin events move SUPRA from the registry fee address to the owner.
Automation registry fee withdrawal by governanceuser, automated0x1::automation_registry::RegistryFeeWithdraw (to, amount) and coin eventsto.
SupraMVM to SupraEVM crossinguser, automatedCoin or fungible asset events for the withdrawal, 0x1::fungible_asset::Deposit to the escrow store, and 0x1::evm_settlement::SettlementRecorded (id, record_address, source_domain, destination_domain, epoch, sender, recipient, amount; recipient is a struct)sender.
SupraEVM to SupraMVM payoutsettlement with data.vm of moveA block’s payout transaction carries one batch 0x1::fungible_asset::Withdraw from the escrow store, 0x1::evm_settlement::EscrowBurned, SettlementBurnApplied, for each credit a 0x1::fungible_asset::Deposit and an EscrowReleased (recipient, amount, status), InboundSettlementApplied, and a Deposit that returns the unspent part to the escrow store. The recipient’s balance change is its own DepositEscrowReleased.recipient, paid when status is 0. Only the recipient’s Deposit is a change to an account’s balance.
Stake withdrawal and distributionsuser, automated0x1::coin::CoinDeposit or 0x1::fungible_asset::Deposit from stake::withdraw, staking_contract, pbo_delegation_pool and vesting payoutsThe recipient.
Staking an amountuser, automated0x1::coin::CoinWithdraw or 0x1::fungible_asset::Withdraw from stake::add_stake, with 0x1::stake::AddStakeThe staker.
Minting and burning of a fungible assetuser, automatedDeposit for a mint to a store and Withdraw for a burn from a storeThe owner of store.

Two changes to an account’s SUPRA have no Deposit or Withdraw. A transfer of a secondary store moves its balance between accounts. 0x1::object::burn of a primary store moves the store object to the burn address: coin::balance of the account is unchanged, since it reads the store at the derived address, and the events of that store are attributed by ObjectCore.owner to the burn address until 0x1::object::unburn.

A transaction that changes one CoinStore emits both the module event and the legacy event, as long as feature 57 is on. A tracker counts one of them.

coin::withdraw and coin::transfer take from the CoinStore first and the remainder from the primary store, so one call can emit a CoinWithdraw and a Withdraw. coin::deposit to an account with no CoinStore and a primary store emits a Deposit and no coin event.

CoinDeposit and CoinWithdraw carry coin_type. The coin type of SUPRA is 0x1::supra_coin::SupraCoin. A coin type that has a paired fungible asset moves through the same two kinds of event.

Accrual Without a Balance Change

These do not change a CoinStore or a primary store and so produce no balance change:

  • Staking rewards accrue to the StakePool resource of a validator pool at the epoch boundary. With feature 6 on, collected gas fees accrue the same way, after 0x1::stake::ValidatorFees holds them per block; with it off, gas is burned. They reach an account only through stake::withdraw and the distributions in the table.
  • A settlement payout whose EscrowReleased.status is not 0 pays nothing. The credit is returned to its SupraEVM sender as a new crossing in the other direction, recorded by SettlementRecorded with sender 0x1, and no Move account is credited.
  • GasAssessment (total_assessed_gas_units, execution_gas_units, io_gas_units, storage_fee_quants, storage_fee_refund_quants) is emitted for a system automation task in place of FeeStatement. The task is not charged.

SUPRA Withdrawals, Transfers and Crossings describes the events of a withdrawal as a wallet sees it before signing, and Tracing Cross-VM Transfers the stages of a crossing.

Gas

A kept transaction (status of Success or Fail) pays gas, and the amount is in 0x1::transaction_fee::FeeStatement (total_charge_gas_units, execution_gas_units, io_gas_units, storage_fee_quants, storage_fee_refund_quants). The charge is

charge = header.gas_unit_price * FeeStatement.total_charge_gas_units

and total_charge_gas_units includes the storage fee. A transaction that aborted, ran out of gas or otherwise failed (status of Fail) pays the same way, and its output.Move.events holds the events of the epilogue, with no event of the failed execution and a storage_fee_refund_quants of 0. An Invalid transaction pays nothing.

The payer is the account named by authenticator.Move.FeePayer.fee_payer_address when that exists, and header.sender.Move otherwise. For an automated transaction the payer is the owner of the task, and a system task has no payer.

The net change to the payer’s total SUPRA is

net = FeeStatement.storage_fee_refund_quants - charge

How the charge appears in the events depends on feature 6. A tracker follows the payer’s CoinStore balance through the transaction’s coin events and so knows its balance when the epilogue runs. With feature 6 off, which is the state of Mainnet, Testnet and Devnet, let

d = charge - FeeStatement.storage_fee_refund_quants
  • If d > 0, the epilogue takes min(CoinStore balance, d) from the payer’s CoinStore with no event. The remainder, if there is one, is one 0x1::fungible_asset::Withdraw from the payer’s primary store, and it is the last event before FeeStatement.
  • If d < 0, the epilogue credits -d to the payer’s CoinStore with no event if the payer has one, even when it also has a primary store. Otherwise it is one 0x1::fungible_asset::Deposit to the payer’s primary store, the last event before FeeStatement.
  • If d = 0, the epilogue emits no event.

The epilogue’s events are identified by this computation and not by their position. When the CoinStore covers d, the epilogue emits no Withdraw, and the last Withdraw before FeeStatement can be an event of the transaction’s own execution. A payer with only a CoinStore has FeeStatement as the only record of its gas.

With feature 6 on, the epilogue takes the full charge from the CoinStore first and the remainder from the primary store as a Withdraw, then credits storage_fee_refund_quants as the d < 0 case does. With feature 65 on, or when the fee module holds a fungible asset burn capability, the gas burn is taken from the primary store only.

A tracker takes the payer’s gas from FeeStatement (net) and leaves the epilogue’s Withdraw and Deposit, identified as above, out of the sum. The balance check under Reconciling finds a tracker that counts a part twice or not at all.

The sender of a transaction with a fee payer pays no gas.

Reconciling

For an account, the SUPRA total at the end of block H is the total at the end of block H0 plus the changes of the blocks in between:

total(H) = total(H0) + sum of CoinDeposit.amount - sum of CoinWithdraw.amount (account = A, coin_type = 0x1::supra_coin::SupraCoin) + sum of Deposit.amount - sum of Withdraw.amount (store = primary store of A, outside the epilogue) + sum of net (A is the payer) - the Deposit that follows each CoinEventHandleDeletion of A (when it was emitted)

Count each CoinStore change once. The legacy events carry no coin type, so while feature 57 is on count the module events and filter them by coin_type. A CoinEventHandleDeletion is followed by a Deposit only when the CoinStore balance was not 0.

Compare total(H) with 0x1::coin::balance<0x1::supra_coin::SupraCoin>(A). A view reads the node’s current state and POST /rpc/v3/view reports no height, so a comparison is meaningful only when the view reads the state of block H. POST /rpc/v4/view/batch executes all its elements against one snapshot and returns state_height, the height of the last block executed in it; compare when state_height is H. The body is {"requests": [<view>, ...]}, each view having the function, type_arguments and arguments of a POST /rpc/v3/view body, and a request may hold at most 20 views; a larger one is refused with 400. Reconciling more accounts takes several requests, each reporting its own state_height, and each account is compared at the state_height of the request that read it. Where that endpoint is absent, read header.height from GET /rpc/v3/block before and after the view and compare only when both equal H. The node records a block in the archive after it executes it, so a view can read the state of block H + 1 while GET /rpc/v3/block still reports H, and this check can be off by one block.

A mismatch has one of these causes:

  • A gas payer’s CoinStore part or refund was not added from FeeStatement.
  • The epilogue’s Withdraw or Deposit was counted in addition to net, or the last Withdraw before FeeStatement was taken for the epilogue’s when the CoinStore covered the charge, which drops a withdrawal of the transaction’s own.
  • A CoinDeposit or CoinWithdraw of another coin type was counted.
  • Both the module event and the legacy event of one change were counted.
  • A CoinEventHandleDeletion migration’s Deposit was counted as income.
  • A kind of transaction was missed: automation_record, automated, settlement, or block_metadata in the genesis block.
  • A fee payer’s gas was not attributed to the payer.
  • The account owns a secondary store whose events were not attributed to it, or its total was compared with a view that excludes secondary stores.
  • The comparison was made against state at a different height.
  • Transactions below x-supra-history-indexed-from were not listed.
  • The account’s primary store was moved to the burn address with 0x1::object::burn.

The same check applies to another fungible asset, with the sum over its stores and 0x1::fungible_asset::balance for each store. A fungible asset has no gas term. An asset whose metadata has a derived-balance dispatch function reports its balance through 0x1::dispatchable_fungible_asset::derived_balance, which can differ from the sum of the stores’ FungibleStore.balance.

The block_metadata transaction of the block at height 0 holds the events of the genesis transaction, such as CoinDeposit. A node that has pruned that block answers 410. A tracker that starts at a later height reads the balances once, with view/batch, and starts from the state_height it returns. A view without a height reads the node’s latest state, which is the balance at the starting height only when the node has executed nothing past it.

Availability

Method or eventMainnet v11.7.0After v11.7.0
GET /rpc/v4/block/height/{height}?with_finalized_transactions=true, and the same on /rpc/v3Yes. Transactions of kind user, automated, block_metadata and automation_record.Also kind settlement.
GET /rpc/v4/accounts/{address}/historyNo.Yes.
GET /rpc/v4/accounts/{address}/transactionsYes. Lists the transactions the account sent.Yes.
GET /rpc/v4/accounts/{address}/coin_transactions and fa_transactions, with typeYes. The response always carries x-supra-cursor.Yes. The header is absent at the end of the feed.
GET /rpc/v4/accounts/{address}/automated_transactionsYes. The response always carries x-supra-cursor, empty at the end.Yes. The header is absent at the end of the feed.
x-supra-history-indexed-fromNo.Yes.
POST /rpc/v3/viewYes.Yes.
POST /rpc/v4/view/batch and state_heightNo.Yes.
GET /rpc/v4/transactions/certificatesYes.Yes.
CoinDeposit, CoinWithdraw, Deposit, Withdraw, FeeStatement, GasAssessmentYes, with the flags above.Yes.
CoinEventHandleDeletion and coin::migrate_to_fungible_storeYes, with feature 60.Yes.
Automation registry events and feesYes.Yes.
0x1::evm_settlement: SettlementRecorded, EscrowReleased, the settlement kindNo.Not in framework aptosvm-v1.16_supra-v1.8.17, which Mainnet runs. In the framework releases after it, with feature 103.

A tracker that works against v11.7.0 uses the block scan. It needs only endpoints present at v11.7.0, it sees every transaction kind in a block, and it is the only method that sees the gas that a fee payer pays from a CoinStore. The per-account endpoints at v11.7.0 are transactions for the gas of the transactions the account sent, which is the account’s gas only for a transaction without a fee payer, coin_transactions and fa_transactions for deposits and withdrawals, and automated_transactions for the gas of tasks. At v11.7.0 a feed ends with an empty page.

Last updated on