Data Analyst Guide to Aptos (pt.1)
By Ying Wu, Data Engineer @ Aptos Labs
Intro
Aptos generates multiple blocks a second with immediate finality (using AptosBFT consensus). The most interesting transaction type within a block is a user transaction. Transactions within a block are executed in parallel using BlockSTM inside a MoveVM. Block generation is usually optimized for latency (fast blocks) but can slow down to optimize for throughput during higher load with up to several thousand transactions per block (ex. 79432868, 140517934). Blocks are grouped into epochs (every ~2 hours on mainnet) and this is when validator changes can occur.
We have 100+ validators spread out geographically for decentralization and decrease latency for transaction submissions. There is a pretty high stake requirement to run a validator but only 10 APT is needed for delegated staking.
We currently host 3 networks: Mainnet, Testnet, and Devnet (resets every ~2 weeks).
User Transactions
For user transactions, an address submits a transaction with
- contract being called (along with contract payload) — aka entry_function
- signers on the transaction (often only the sender)
- status of transaction (success or VM abort message)
- max gas limit and transaction expiration timestamp
Gas is paid for the transaction in octa (1e8 octa = 1 APT) using the following formula: gas_fee = gas_unit_price * gas_used - storage_refund
. Where gas_unit_price
defaults to 100 octa. Prioritization within blocks can be done by increasing gas_unit_price
and wallets will recommend higher gas_unit_price
during congestion (when blockspace is limited).
Transactions will have at least one event (gas fee statement that lists how much gas is spent on I/O, storage, and storage refund) and have some changes in resources. Since these changes only show diffs caused by the transaction, it is preferable for contracts to emit events to provide context around what actions occur (instead of trying to infer from changes). However, events cannot be accessed by other transactions and instead is meant for consumption by an indexer.
Transactions can be identified using transaction hash, however, Aptos transactions also include a transaction version that is a monotonically increasing number and version is often used for transaction identification.
Coins and Tokens
When representing assets on a chain, one consideration can be how ‘stackable’ an asset is. Assets representing coins are interchangeable (fungible) and should be stacked together whereas assets representing art are unique (and should a higher order collection grouping for stacking). Representing items with durability is a grey area.
We have two representations for ‘stackable’ assets
- Stored in
0x1::coin::CoinStore<type>
(default) - Manipulated using
0x1::coin::
functions - Uses
0xa
to migrate from coins to fungible asset
2. Fungible assets aka FA
- Manipulated using
0x1::fungible_asset::
functions
We also have two representations of non-stackable assets
- Tokens (legacy) aka token v1
- Stored in
0x3::token::TokenStore
- Manipulated using
0x3::
functions
2. Digital assets aka token v2
- Manipulated using
0x4::
functions
Supply tracking on coins is optional. Non-legacy representations are objects, making them easier to track and allowing for more complex permissioning capabilities. Objects can also lead to interesting representations like sending coins to a fungible assets store and also necessitates an indexer to find all of the objects owned by an account (address of object is generated via consistent hash).
When stackable assets move in/out of CoinStore and FungibleAssetStore, withdraw and deposit events are emitted (double entry bookkeeping).
When non-stackable assets move, they will emit Transfer events or Deposit events. Additionally these assets also emit mint and burn events.
Aptos has an open source indexer implementation (docs) that provides tables for
- activity (transfers)
- balance (ownership)
- metadata (number of decimals, name, etc)
for assets (processor code for fungible asset, processor code for digital asset).
Public query access to these tables is via graphql endpoint (table reference) although it is highly recommended to request a free dev API key for higher rate limits.
Data Access
There are multiple vendors providing access to Aptos data. Some of the more popular ones are dune, flipside, bigquery (example queries and their rust ETL). The raw tables are structured similarly to data returned by API with the following rough relationship
Going through the daily active user query from dune (we often use user and account interchangeably)
WITH final AS (
SELECT
tx_version,
block_date,
signer_address,
tx_success,
signer_type,
type
FROM aptos.signatures s
)
SELECT
block_date,
COUNT(1) as n_sig,
COUNT(DISTINCT tx_version) as n_txn,
COUNT(DISTINCT signer_address) as daily_active_user,
MIN(tx_version),
MAX(tx_version)
FROM final
WHERE 1=1
AND block_date
BETWEEN DATE_ADD('day', -31, current_date)
AND DATE_ADD('day', -1, current_date)
-- AND tx_version BETWEEN 1669545227 AND 1670475367
AND tx_success
AND signer_type != 'fee_payer'
GROUP BY 1
ORDER BY 1
On Aptos we define an active account as an address that signs a transaction. We can have multiple signatures on a transaction (multi-sig) so one transaction can represent more than one active account even though it can only have one sending account (sender is often used as proxy).
The query above gets data from signatures table and filters to successful transactions for non-fee payer signatures in last 30 days and aggregates by day to get total number of signatures (n_sig
), number of transactions (n_txn
), number of active accounts (daily_active_user
) as well as min/max version that is useful for debugging.
To spot check individual transactions (ex. 2203280000), lookups can be performed against explorer and other Aptos apis
- Explorer
- Full Node (docs, ref)
- GRPC:
docker run — rm fullstorydev/grpcurl:v1.8.7 -d '{ "starting_version": 2203280000, "transactions_count": 1 }' -max-msg-sz 30000000 -H "authorization:Bearer aptoslabs_SECRET_KEY_HERE" grpc.mainnet.aptoslabs.com:443 aptos.indexer.v1.RawData/GetTransactions
(docs) - GraphQL (docs)
Conclusion
I hope this overview helps others get started on querying Aptos data. In part 2, I’ll be doing a deeper dive on topics such as
- Data changes for defi AMM transaction
- Data changes for NFT marketplace transaction
- Tracking coin transfers that don’t go into a CoinStore
- Estimating supply on coins
- Tracking delegated staking and reward %
- Understanding different signature types
- Semi-fungible tokens (ex. game items as assets with durability)
Remember to make a free dev account for higher rate limits for querying. I look forward to seeing the dashboards that the community make.