diff --git a/content/api-reference/pricing-resources/pricing/compute-unit-costs.mdx b/content/api-reference/pricing-resources/pricing/compute-unit-costs.mdx index 7c40101b2..146176c70 100644 --- a/content/api-reference/pricing-resources/pricing/compute-unit-costs.mdx +++ b/content/api-reference/pricing-resources/pricing/compute-unit-costs.mdx @@ -158,6 +158,16 @@ For more details, check out the [Compute Units](/docs/reference/compute-units#wh * To view the batch request breakdown in the Alchemy Dashboard, click on "raw request" +# Solana: Account Archive + +The [Solana Account Archive](/docs/solana/account-archive) is served through the standard Solana Alchemy endpoint (`https://solana-mainnet.g.alchemy.com/v2/{apiKey}`). It extends `getAccountInfo` with `slot`, `lastUpdateBeforeSlot`, and `firstUpdateAfterSlot` parameters for historical queries. Per-method compute unit pricing for archive lookups is being finalized and will be published here once confirmed. Contact your Alchemy point of contact for interim pricing details. + +| Method | CU | Throughput CU | +| ----------------------------------------------- | --- | ------------- | +| getAccountInfo (archive: `slot` set) | TBD | | +| getAccountInfo (archive: `lastUpdateBeforeSlot`) | TBD | | +| getAccountInfo (archive: `firstUpdateAfterSlot`) | TBD | | + # Solana: DAS APIs (NFT/Token) {/* cu:auto product="solana-das" */} diff --git a/content/api-reference/solana/account-archive.mdx b/content/api-reference/solana/account-archive.mdx new file mode 100644 index 000000000..f51621912 --- /dev/null +++ b/content/api-reference/solana/account-archive.mdx @@ -0,0 +1,217 @@ +--- +title: Solana Account Archive +description: Query the state of any Solana account at any historical slot with a drop-in extension to getAccountInfo that adds slot, lastUpdateBeforeSlot, and firstUpdateAfterSlot cursor parameters. +subtitle: Answer "what did this account look like at slot N" with a single getAccountInfo call +slug: docs/solana/account-archive +--- + +Solana Account Archive answers `getAccountInfo` for any Solana account at any historical slot (vote accounts and per-slot sysvars excepted), with a deep, ever-growing window of historical coverage that is never pruned. Use the `slot` parameter to read state at a point in time, or `lastUpdateBeforeSlot` and `firstUpdateAfterSlot` to walk an account's update history. Reads are served with median latency in the microseconds. + +## Solana only remembers the present + +Here's a detail about Solana that surprises even experienced builders: **a validator stores exactly one copy of each account, the latest one.** Every write overwrites the previous state in place, and so the moment an account is touched, whatever it looked like before is gone. Even for a slot the node still has full blocks for, the *account state* at that slot no longer exists anywhere on the machine. + +This is a feature, not a bug. It's part of why Solana is fast. But it means a whole class of very reasonable questions have no answer, like: + +* **Debugging:** "Our liquidation fired at slot 285,401,337. What was the oracle price account at that exact slot?" +* **Backtesting:** "Reconstruct this pool's reserves at every point over the last six months." +* **Audits:** "Prove what this token account held on March 3rd." +* **Recovery:** "Our indexer was down for four hours. Rebuild exactly the state transitions we missed." + +You might think `getBlock` covers this, but it doesn't. It returns a past slot's transactions and their pre/post *balances*, not account *data*. For most applications, the state that matters lives in the data bytes: the order book, price feed, position, or configuration. + +Solana Account Archive brings the full history back. It answers `getAccountInfo` for any account at any slot since July 2025, using the same request and response shapes you already use. The archive never prunes, so the queryable window only grows over time. + +## The API: `getAccountInfo`, extended + +We deliberately did not invent a new method. The archive speaks standard JSON-RPC and implements [`getAccountInfo`](/docs/chains/solana/solana-api-endpoints/get-account-info) with all the config options you already know: `encoding` (`base64`, `base58`, `base64+zstd`, `jsonParsed`), `commitment`, `dataSlice`, and `minContextSlot`, plus three new, mutually exclusive parameters: + +| Parameter | Semantics | Use it for | +| --- | --- | --- | +| `slot` | State *as of* slot `S` (inclusive: the latest write with `slot <= S`) | Point-in-time snapshots | +| `lastUpdateBeforeSlot` | The most recent write *strictly before* `S` | Walking an account's history backward | +| `firstUpdateAfterSlot` | The first write *strictly after* `S` | Walking an account's history forward | + +These three are also mutually exclusive with `minContextSlot`; pairing it with any of them is rejected. They work only with `finalized` commitment (the default): combining a historical parameter with `processed` or `confirmed` is rejected as invalid params. Omit all three and you get a normal, latest-state `getAccountInfo`; the archive is a drop-in superset. Here is a complete point-in-time read, a token account's state as of slot 400,000,000: + +```bash +curl https://solana-mainnet.g.alchemy.com/v2/{apiKey} \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "getAccountInfo", + "params": [ + "3emsAVdmGKERbHjmGfQ6oZ1e35dkf5iYcS6U4CPKFVaa", + { "encoding": "jsonParsed", "slot": 400000000 } + ] + }' +``` + +The response is the standard `getAccountInfo` shape: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "context": { + "apiVersion": "4.1.0", + "slot": 400000000 + }, + "value": { + "data": { + "parsed": { + "info": { + "isNative": false, + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "owner": "7VHUFJHWu2CuExkJcJrzhQPJ2oygupTWkL2A2For4BmE", + "state": "initialized", + "tokenAmount": { + "amount": "1056153754430274", + "decimals": 6, + "uiAmount": 1056153754.430274, + "uiAmountString": "1056153754.430274" + } + }, + "type": "account" + }, + "program": "spl-token", + "space": 165 + }, + "executable": false, + "lamports": 2040821, + "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "rentEpoch": 18446744073709551615, + "space": 165 + } + } +} +``` + +Note the semantics: `slot: S` means *state as of S*. If the account was last written at slot `S - 40,000` and untouched since, you get that write, exactly what any program executing at slot `S` would have seen. + +## An iterator over an account's history + +`lastUpdateBeforeSlot` and `firstUpdateAfterSlot` turn the archive into an *iterator over every state transition of an account*. + +`lastUpdateBeforeSlot` returns the *actual slot* of the write it found in `context.slot`, so each response is the cursor for the next request: + +```python +# Walk every state transition of an account, newest to oldest. +cursor = current_slot + 1 +while True: + try: + result = rpc("getAccountInfo", [pubkey, {"lastUpdateBeforeSlot": cursor}]) + except RpcError as e: + if e.code == -32020: # walked past the edge of coverage; done + break + raise + slot = result["context"]["slot"] # the slot this write landed at + handle(slot, result["value"]) # value is null if this write closed the account + cursor = slot # strictly-before: no overlap, no gaps +``` + +The walk has exactly one stopping point: error `-32020`, meaning the cursor has stepped past the edge of the archive's coverage. A `null` value along the way means what it always means in `getAccountInfo`: the account did not exist as of that slot — here, because the write at `context.slot` deleted it. Feed that slot back in as the cursor and the walk continues through the account's earlier life. + +`firstUpdateAfterSlot` does the reverse: instead of stepping back in time, it moves forward, returning the next write at or after a given slot. It's ideal for an indexer catching up from a known slot that wants every intermediate state, not just the latest. + +Between the two, you can walk an account's entire lifecycle: every balance change, every data mutation, every ownership change, with the exact slot each one landed at. + +## How the archive stays trustworthy + +The archive listens to the network through a Geyser stream and records account writes as they happen, with two rules that keep the record trustworthy: + +**Only finalized slots enter the archive.** Solana forks constantly at the tip, and a block that looks real for a few seconds can simply vanish. The archive holds each slot's updates back until the network finalizes it, so the permanent record contains only what actually happened. + +**Nothing is silently lost.** Every Solana block names its parent, so any block the stream misses (a hiccup, a reconnect, a service deploy) is detected immediately and backfilled. + +The storage layer underneath is a story of its own. What matters here is the result: point-in-time reads are fast enough to sit in a hot path, with **median latency in the microseconds**, measured at our internal service layer. + +## Rebuilding the past year + +Ingesting from today onward gets you an archive that's useful *next* year. We wanted the past year too, which means reconstructing account state for tens of millions of slots that had already happened, on a chain that keeps no history. And it's a lot of history: Solana produces a block roughly every 400 ms, about 216,000 slots per day. Over a year that's **~78 million slots, hundreds of billions of account updates, and more than a petabyte of raw account data**, each slot carrying thousands of account writes with the full data payload attached. + +### Why "just parse the transactions" doesn't work + +There is a tempting shortcut to fetch historical blocks and decode what each transaction did to each account. It fails for a fundamental reason that's worth internalizing: **a Solana transaction is not a description of a state change; it's a program invocation.** + +What actually happens to an account depends on the program's execution: CPIs fanning out into other programs, sysvars read mid-flight, compute metering, the precise semantics of the runtime *at that slot*. The block records which accounts a transaction touched and how balances moved, but the data bytes, the part you actually want, are determined only by running the code. + +There is only one faithful way to know what a transaction did to an account: **execute it, with the real runtime, against the real state it executed against.** + +### Replay: running the chain again + +So that's what we do. The backfill pipeline is, in essence, a validator that relives history: + +1. **Boot from a trusted snapshot.** The Solana Foundation maintains public archives of historical snapshots: full captures of every account at a given slot. Loading the snapshot at slot `A` gives us the complete, canonical state of several hundred million accounts at that moment. +2. **Replay every block forward.** From `A + 1`, each historical block runs through the actual validator runtime: real execution, not simulation, not log-parsing. Every transaction executes; the account writes the runtime produces are captured and indexed, slot by slot. +3. **Stop at the next snapshot and prove it.** This is the step that makes the whole thing trustworthy. When replay reaches slot `B`, where the next canonical snapshot exists, we compare our replayed end-state against it, including the **accounts lattice hash**: a cryptographic commitment to the entire account set that the network itself computes and agrees on. If a single byte of a single account diverged anywhere in the range, the hashes won't match and the range is rejected. We don't *assume* replay is correct; every backfilled range is checked against consensus ground truth before it's trusted. + +### Old blocks need the runtime from their era + +Here's the wrinkle that makes replay genuinely hard: **Solana's execution semantics are versioned in time.** Feature gates activate at specific epochs, compute budget rules change, syscalls get added, edge-case behaviors get fixed. A transaction from twelve months ago must be replayed by a runtime that behaves exactly as the cluster did *at that slot*. Replay it with today's validator and step 3 will tell you, loudly, that you manufactured a history that never happened. + +In practice, the backfill fleet runs several pinned validator lineages, each responsible for the era it can faithfully reproduce, with work chunked along epoch boundaries so that every chunk begins and ends at a verifiable snapshot. Extending the archive further back is largely a matter of standing up the right runtime era and paying the replay compute. And because each epoch-aligned chunk is independent, history is perfectly parallel; backfilling a year is mostly a question of how many replay workers you run at once. + +## Trust, continuously verified + +Hash-verified backfill covers the past; a separate concern is whether the *serving path* stays honest in production. An independent watchdog continuously cross-validates the archive's answers against live RPC nodes, through the same public API you'd use. Correctness is paramount to us, and so it's re-verified every minute the service runs. A multi-layer approach, consensus-anchored hashes for backfilled history, finalization-gated writes at the tip, and continuous live cross-validation on top, ensures that you can trust a record of truth moving forward in time. + +## What you can build with this + +Every question from the top of this page is now an API call: + +* **"What was the oracle account at that exact slot?"** One `getAccountInfo` with `slot` set. Pull the exact state of every account a failing transaction read, at the slot it executed. No more reconstructing oracle inputs from screenshots and guesswork. +* **"Reconstruct this pool's reserves over six months."** Sample the pool account at a fixed slot cadence and backtest against what was actually on chain, not an approximation stitched together from trade events. +* **"Prove what this token account held on March 3rd."** "As of slot S, account X contained exactly these bytes." That is answerable, and anchored to hashes the network itself agreed on. +* **"Rebuild the state transitions our indexer missed."** Page forward with `firstUpdateAfterSlot` from your last known slot and receive every intermediate state, in order. + +And one the intro didn't ask: **balance and position history without running an indexer at all.** Walk a token account backward with `lastUpdateBeforeSlot` and you have its complete timeline. + +## Get started + +Solana stays fast by keeping validators lean and focused on current state. Historical account state has simply lived elsewhere, and until now the ecosystem has worked around that. Solana Account Archive changes that. With finalization-gated live ingestion and a backfill pipeline that re-executes history through era-faithful runtimes and proves the result against consensus hashes, historical `getAccountInfo` becomes just another RPC call: same method, same response shape, one extra parameter. + +Everything the archive indexes is compressed, stored with at least 3 replicas for redundancy, and never pruned, so the queryable window only grows. + +[Get started on Solana](/docs/reference/solana-api-quickstart), and [reach out to us](https://www.alchemy.com/contact-sales) if your use case needs deeper history, or you want to explore specialized solutions and pricing at scale. + +## FAQ + +### How do I get a Solana account's state at a specific slot? + +Call `getAccountInfo` with the account's pubkey and a `slot` parameter in the config object: `getAccountInfo(pubkey, { "slot": S })`. The response is the standard `getAccountInfo` shape and returns the account's state as of slot `S`, the latest write at or before `S`. + +### How do I get the full update history of a Solana account? + +Page with the cursor parameters. `getAccountInfo(pubkey, { "lastUpdateBeforeSlot": S })` returns the most recent write strictly before `S`, with the write's actual slot in `context.slot`; feed that slot back in as the next cursor to walk backward through every state transition. `firstUpdateAfterSlot` walks forward the same way. + +### What's the difference between `slot`, `lastUpdateBeforeSlot`, and `firstUpdateAfterSlot`? + +`slot` is a point-in-time read: state *as of* slot `S`, inclusive. `lastUpdateBeforeSlot` and `firstUpdateAfterSlot` are exclusive history cursors: the nearest write strictly before or strictly after `S`. The three are mutually exclusive; omit all of them for a normal latest-state read. + +### Does `jsonParsed` encoding work for historical account state? + +Yes. The same account decoders a standard Solana RPC node uses are applied to the historical bytes, so token accounts, mints, and other known program accounts come back parsed at any slot in coverage. All standard encodings work: `base64`, `base58`, `base64+zstd`, and `jsonParsed`, plus `dataSlice`. + +### How far back can I query? + +Coverage currently extends back to July 2025, with backfill extending it further back over time. History is never pruned, so everything indexed stays queryable and the window only grows. + +### How is the historical data known to be correct? + +Backfilled ranges are produced by re-executing every transaction through the validator runtime of the corresponding era, and the resulting state is verified against the network's own consensus artifacts, including the accounts lattice hash from canonical snapshots. Live ingestion commits only finalized slots, and an independent watchdog continuously cross-checks served results against live RPC nodes. + +### Are all accounts covered? + +All accounts except pure per-slot chain bookkeeping: vote accounts and the three sysvars rewritten every slot (`SlotHashes`, `SlotHistory`, `RecentBlockhashes`) are excluded from the index. Program accounts, token accounts, mints, PDAs, wallets, and the remaining sysvars (Clock, Rent, and so on) are all covered. + +### Do I need a new SDK or client? + +No. Any Solana JSON-RPC client works. The archive implements standard `getAccountInfo`, and the historical parameters are just extra fields in the existing config object. + +### What error do I get when I query past the archive's coverage? + +Error code `-32020`, and it is the only terminal for a cursor walk — a never-created account errors the same way. A `value: null` response means something different: the account did not exist as of the resolved slot. For point-in-time reads that covers both "not created yet" and "already closed"; for cursor reads it means the write found at `context.slot` deleted the account, and the walk can continue from that slot. diff --git a/content/api-reference/solana/historical-account-state.mdx b/content/api-reference/solana/historical-account-state.mdx deleted file mode 100644 index 0b5cc3f85..000000000 --- a/content/api-reference/solana/historical-account-state.mdx +++ /dev/null @@ -1,257 +0,0 @@ ---- -title: Snapshotting historical Solana account state -description: Patterns for capturing Solana account state at regular intervals (hourly, daily, or arbitrary slot cadence) using Alchemy's Yellowstone gRPC streams and archival JSON-RPC methods. -subtitle: Capture point-in-time Solana account state at any cadence using gRPC subscriptions and archival JSON-RPC -slug: docs/solana/historical-account-state ---- - -Many Solana workloads need to know what an account looked like at a previous point in time: portfolio analytics, TVL history, vault accounting, oracle audits, treasury reporting, ML feature stores. Standard Solana JSON-RPC does not expose a "read this account as of slot N" primitive. This page covers the two patterns Alchemy supports today for reconstructing historical account state, how to combine them, and how to land at common cadences like hourly snapshots. - -## Why Solana RPC alone is not enough - -`getAccountInfo` returns the **current** state of an account. The `minContextSlot` parameter is a freshness floor (the read must be evaluated at a slot greater than or equal to `minContextSlot`), not a historical lookup. Solana validators do not retain prior versions of an account's data once the account is updated, so no JSON-RPC method can answer "what did this account hold at slot N" directly. - -What Solana **does** retain is the full transaction and block history. Alchemy's archival infrastructure exposes that history through `getTransaction`, `getBlock`, and `getSignaturesForAddress`, which means you can reconstruct historical account state by: - -1. **Capturing it forward** as the chain advances, persisting periodic snapshots to your own store. -2. **Reconstructing it backward** by replaying the transactions that wrote to the account. - -Most production indexers combine both: stream forward from "now" while backfilling history for the period before the stream started. - -## Workflow A: forward snapshotting via Yellowstone gRPC - -[Yellowstone gRPC](/docs/reference/yellowstone-grpc-overview) is the recommended path for ongoing state capture. A single subscription receives every account update for the addresses or programs you care about, with the slot number attached to each update, and persists at minimal latency. - -### How it works - -1. Open a Yellowstone gRPC subscription with an account filter ([`accounts`](/docs/reference/yellowstone-grpc-subscribe-accounts) in the [`SubscribeRequest`](/docs/reference/yellowstone-grpc-subscribe-request)). -2. On every `SubscribeUpdateAccount` your handler receives, write a row containing `(pubkey, slot, write_version, data, lamports, owner, txn_signature)` to your store. -3. Index the resulting table by `(pubkey, slot, write_version)` so you can query the latest version at or before any target slot. - -A single Solana slot can contain multiple transactions that write to the same account. The `write_version` field is a monotonically increasing counter that disambiguates those intra-slot updates: the final state for a slot is the row with the highest `write_version` at that slot. Always tie-break by `(slot, write_version)` rather than `slot` alone. - -To produce hourly snapshots, you do not need a separate sampling job. Solana blocks land at roughly 400 ms, so an active account may have many writes per hour and a quiet account may have none. Both cases are handled the same way: a query for "the state of this account at hour H" returns the row with the largest `(slot, write_version)` pair where `slot` is less than or equal to the slot corresponding to the timestamp at the end of hour H. In SQL that is `ORDER BY slot DESC, write_version DESC LIMIT 1`. Use `getBlockTime` or `getBlocks` to map wall-clock timestamps to slots. - -### Filter options - -The Yellowstone account filter supports three styles, in order of selectivity: - -* **Specific addresses** ([`account`](/docs/reference/yellowstone-grpc-subscribe-accounts#account-address-filter)): pass a list of pubkeys to watch. Best when you know the accounts upfront. -* **Program owner** ([`owner`](/docs/reference/yellowstone-grpc-subscribe-accounts#owner-filter)): receive every account owned by a given program. Use this when the account set is dynamic (for example, all vault accounts under a single program). -* **Memcmp / data-size / lamports / `token_account_state`** ([`filters`](/docs/reference/yellowstone-grpc-subscribe-accounts#memcmp-filter)): narrow further by byte patterns, account discriminator, or balance ranges. Combine with `owner` to watch a specific subset of a program's accounts. - -If you need to enumerate the initial set of accounts before subscribing (for example, all program-derived accounts under a vault program), use `getProgramAccounts` paginated via Alchemy's [AccountsDB Infrastructure](/docs/solana/accounts-db-infra). The `pageKey` and `order` parameters let you scan large account sets without timing out. - -### Minimal Rust example - -```rust -use anyhow::Result; -use futures::{sink::SinkExt, stream::StreamExt}; -use std::collections::HashMap; -use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient}; -use yellowstone_grpc_proto::geyser::{ - CommitmentLevel, SubscribeRequest, SubscribeRequestFilterAccounts, - subscribe_update::UpdateOneof, -}; - -#[tokio::main] -async fn main() -> Result<()> { - let endpoint = "https://solana-mainnet.g.alchemy.com"; - let x_token = "YOUR_ALCHEMY_API_KEY"; - - let mut client = GeyserGrpcClient::build_from_shared(endpoint)? - .tls_config(ClientTlsConfig::new().with_native_roots())? - .x_token(Some(x_token))? - .connect() - .await?; - - let (mut tx, mut stream) = client.subscribe().await?; - - // Subscribe to a specific set of account pubkeys - let mut accounts = HashMap::new(); - accounts.insert( - "accounts_to_snapshot".to_string(), - SubscribeRequestFilterAccounts { - account: vec![ - "AccountPubkey1...".to_string(), - "AccountPubkey2...".to_string(), - ], - owner: vec![], - filters: vec![], - nonempty_txn_signature: Some(true), - }, - ); - - tx.send(SubscribeRequest { - accounts, - commitment: Some(CommitmentLevel::Confirmed as i32), - ..Default::default() - }) - .await?; - - while let Some(Ok(msg)) = stream.next().await { - if let Some(UpdateOneof::Account(update)) = msg.update_oneof { - if let Some(info) = update.account { - // Persist this snapshot. Index by (pubkey, slot, write_version). - println!( - "slot={} pubkey={} lamports={} data_len={}", - update.slot, - bs58::encode(&info.pubkey).into_string(), - info.lamports, - info.data.len() - ); - } - } - } - - Ok(()) -} -``` - -For client setup, authentication details, and language samples in TypeScript and Go, see the [Yellowstone gRPC Quickstart](/docs/reference/yellowstone-grpc-quickstart). - -### Recovering from disconnects - -Yellowstone supports replaying historical updates by setting `from_slot` on the `SubscribeRequest`. The replay window is up to **6000 slots (~40 minutes)** of history. Persist the slot of the last update you successfully wrote, and on reconnect resubscribe with `from_slot` set to that slot. If your downtime exceeds the replay window, fall back to the backfill workflow below for the gap. - -## Workflow B: historical backfill via transaction replay - -Snapshotting forward only captures state from the moment your subscription starts. For periods before that, the available primitive is the transaction history exposed through `getTransaction`. This workflow is best understood by what each `getTransaction` response actually contains and what it does not. - -### What `getTransaction` provides - -For each transaction, `getTransaction` returns: - -* `meta.preBalances` and `meta.postBalances`: SOL lamport balances per account (indexed against `transaction.message.accountKeys`). -* `meta.preTokenBalances` and `meta.postTokenBalances`: SPL token balances per account. -* `meta.innerInstructions`: CPIs invoked by the top-level instructions. -* `meta.logMessages`: program log output. -* `transaction.message.instructions` and `transaction.message.accountKeys`: the instructions and the accounts they touched. - -What `getTransaction` does **not** return: pre or post account `data` blobs. There is no `preAccountData` / `postAccountData` field. That is the central constraint on this workflow. - - - Transaction replay can reliably reconstruct three things: **SOL lamport balances** (from pre/post balances), **SPL token balances** (from pre/post token balances), and **events emitted via program logs** (Anchor `emit!` macros, custom `msg!` lines). - - It **cannot** reconstruct arbitrary program account `data` blobs from `getTransaction` alone. To rebuild full account `data` for historical periods, you need either (a) a starting `data` snapshot from before the period of interest, plus program-specific decoding of each touching instruction to derive the new state, or (b) the program emitting comprehensive event logs that cover every state mutation. If neither applies, the supported path is to start workflow A as early as possible and accept that history before the subscription start is not recoverable from transactions alone. - - -### Pattern - -1. Call [`getSignaturesForAddress`](/docs/chains/solana/solana-api-endpoints/get-signatures-for-address) for the target account, paging through history with `before` and `until` cursors. Returns signatures plus block times. -2. For each signature, call [`getTransaction`](/docs/chains/solana/solana-api-endpoints/get-transaction) and locate the target account's index in `transaction.message.accountKeys`. -3. Read `meta.postBalances[i]` (and `meta.postTokenBalances` for token accounts) for the state immediately after this transaction's writes. Persist a row keyed by `(pubkey, slot, signature)` so you can later answer "balance at hour H" as the row with the largest slot less than or equal to `block_at(H)`. - -`meta.preBalances` and `meta.postBalances` are consistent across adjacent transactions: the `preBalances[i]` of the transaction at slot T+1 equals the `postBalances[i]` of the most recent earlier transaction that touched account `i`. Use the post-side of each transaction as the canonical value for its slot. - -Alchemy's Solana archival surface is optimized for this kind of historical scan. Per the [Built for Solana](https://www.alchemy.com/blog/solana-infrastructure) release, `getTransaction` is up to 20x faster than other providers on historical calls, and `getSignaturesForAddress` supports recency-first ordering so you can walk from the present backward without scanning from genesis. - -### When you need program-account state, not just balances - -For lending position values, vault share prices, oracle values, market state, and any other field that lives inside a program account's `data` blob, workflow B is not sufficient on its own (see the warning above). The supported path is: - -1. Enumerate the account set with paginated [`getProgramAccounts`](/docs/chains/solana/solana-api-endpoints/get-program-accounts) calls using Alchemy's [AccountsDB Infrastructure](/docs/solana/accounts-db-infra) (`pageKey` + `order`). This gives you every account and its **current** `data`. -2. Start a Yellowstone gRPC subscription (workflow A) filtered by program owner. From that point forward, every state change is captured with the full new `data` bytes. -3. Treat the moment the subscription starts as your "history begins here" marker. Reads at any slot at or after that marker resolve against the snapshot table; reads before it are not supported by this workflow. - -The earlier you start workflow A, the smaller the unsupported window. If a backfill before the subscription start is required, the practical options are to bootstrap from an external historical-snapshot source for the program or to model the program's instructions program-side and replay them against a starting snapshot. Both are out of scope for this guide. - -### Minimal TypeScript example: lamport balance backfill - -This example walks one account's signature history and persists post-balance per transaction, which is what workflow B reliably supports. - -```typescript -import { Connection, PublicKey } from "@solana/web3.js"; - -const connection = new Connection( - "https://solana-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY", - "confirmed" -); - -type BalanceRow = { - pubkey: string; - slot: number; - signature: string; - lamports: number; -}; - -async function backfillLamportHistory( - address: string, - untilSlot: number -): Promise { - const pubkey = new PublicKey(address); - const rows: BalanceRow[] = []; - let before: string | undefined = undefined; - - while (true) { - const sigs = await connection.getSignaturesForAddress(pubkey, { - before, - limit: 1000, - }); - if (sigs.length === 0) break; - - for (const sig of sigs) { - if (sig.slot < untilSlot) return rows; - - const tx = await connection.getTransaction(sig.signature, { - maxSupportedTransactionVersion: 0, - }); - if (!tx || !tx.meta) continue; - - // Locate the target account's index in the message account keys. - const keys = tx.transaction.message.getAccountKeys({ - accountKeysFromLookups: tx.meta.loadedAddresses, - }); - const index = keys - .keySegments() - .flat() - .findIndex((k) => k.equals(pubkey)); - if (index === -1) continue; - - // Post-balance is the canonical lamport value after this transaction's writes. - rows.push({ - pubkey: address, - slot: sig.slot, - signature: sig.signature, - lamports: tx.meta.postBalances[index], - }); - } - - before = sigs[sigs.length - 1].signature; - } - - return rows; -} -``` - -For SPL token balances, swap `meta.postBalances[index]` for the matching entry in `meta.postTokenBalances` (which is indexed by `accountIndex` and includes `mint`, `owner`, and `uiTokenAmount`). For program log events, parse `meta.logMessages` against your program's known event format. - -## Choosing a sampling cadence - -The two workflows above give you per-update granularity. Picking a coarser cadence (hourly, daily) is a query-time concern, not an ingest-time one. Two common approaches: - -* **Store every update, derive snapshots at query time.** Recommended when the account set is small or update volume is moderate. Lets you change cadence later without re-ingesting. -* **Materialize fixed-cadence rollups.** Run a periodic job that, for each tracked account, writes the latest value at or before each cadence boundary into a separate table. Reduces query cost when you only ever read at fixed intervals. - -To map wall-clock timestamps to slots for the boundaries, call [`getBlockTime`](/docs/chains/solana/solana-api-endpoints/get-block-time) on a candidate slot, or use [`getBlocks`](/docs/chains/solana/solana-api-endpoints/get-blocks) to find the slot range that bounds a target timestamp. - -## Workflow comparison - -| Aspect | Yellowstone gRPC (Workflow A) | Transaction replay (Workflow B) | -| --------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- | -| Time horizon | From subscription start, forward | From any point in history, backward | -| Latency to new data | Real-time (sub-second) | As fast as you can page archival history | -| Filter granularity | Address, owner, memcmp, data size, lamports | Per-address (via `getSignaturesForAddress`) | -| Data captured | Full account bytes (data, lamports, owner) | SOL balance, SPL token balance, program log events only (no account `data` blobs) | -| Replay after gap | `from_slot`, up to 6000 slots (~40 min) | Unbounded, limited only by archival depth | -| Best for | Ongoing capture of a known account or program set | Backfilling SOL/token balance history; not sufficient for arbitrary program state | -| Plan requirement | PAYG or Enterprise | Available on all paid plans | - -Combine the two: turn on the gRPC subscription first, persist its slot as your "snapshot start", then run workflow B in parallel to backfill SOL and SPL token balance history before that slot. For full account `data` history before the subscription start, see the warning under [Workflow B](#workflow-b-historical-backfill-via-transaction-replay): start workflow A as early as possible, or source a starting snapshot from an external historical-data product. - -## Related references - -* [AccountsDB Infrastructure](/docs/solana/accounts-db-infra) — paginated `getProgramAccounts` and `getTokenLargestAccounts` for enumerating large account sets. -* [Yellowstone gRPC Overview](/docs/reference/yellowstone-grpc-overview) and [Subscribe to Accounts](/docs/reference/yellowstone-grpc-subscribe-accounts) — full filter reference and protobuf definitions. -* [Solana API FAQ — Historical Data (Archival)](/docs/solana-api-faq#historical-data-archival) — the full set of archival JSON-RPC methods. -* [Built for Solana](https://www.alchemy.com/blog/solana-infrastructure) and [How Alchemy Built the Fastest Archival Methods on Solana](https://www.alchemy.com/blog/how-alchemy-built-the-fastest-archival-methods-on-solana) — background on the archival stack powering the methods above. diff --git a/content/docs.yml b/content/docs.yml index 6b546499c..6471775a0 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -326,12 +326,12 @@ navigation: href: https://solana-demo-sigma.vercel.app/ - page: Accounts DB Infrastructure path: api-reference/solana/accounts-db-infra.mdx - - page: Historical account state - path: api-reference/solana/historical-account-state.mdx - page: Solana API Overview path: api-reference/solana/solana-api-overview.mdx - api: Solana API Endpoints api-name: solana + - page: Solana Account Archive + path: api-reference/solana/account-archive.mdx - link: Solana DAS APIs href: /docs/reference/alchemy-das-apis-for-solana - link: Solana DAS APIs v2 diff --git a/content/redirects.yml b/content/redirects.yml index 36260316e..533f54b05 100644 --- a/content/redirects.yml +++ b/content/redirects.yml @@ -16,6 +16,10 @@ redirects: permanent: true # ========================================= Removed Page Redirects ========================================== + - source: /docs/solana/historical-account-state + destination: /docs/solana/account-archive + permanent: true + - source: /docs/reference/new-pricing-for-existing-scale-and-growth-customers destination: /docs/reference/pay-as-you-go-pricing-faq permanent: true diff --git a/src/openrpc/chains/_components/solana/account.yaml b/src/openrpc/chains/_components/solana/account.yaml index 8164d8023..35b7b8f9d 100644 --- a/src/openrpc/chains/_components/solana/account.yaml +++ b/src/openrpc/chains/_components/solana/account.yaml @@ -40,6 +40,35 @@ components: $ref: "./base-types.yaml#/components/schemas/DataSlice" minContextSlot: $ref: "./base-types.yaml#/components/schemas/MinContextSlot" + slot: + type: integer + minimum: 0 + description: >- + [Account Archive](/docs/solana/account-archive) parameter. Return + the account's state as of slot `S` (inclusive): the latest write + with `slot <= S`. Mutually exclusive with `lastUpdateBeforeSlot`, + `firstUpdateAfterSlot`, and `minContextSlot`; requires + `finalized` commitment (the default). + lastUpdateBeforeSlot: + type: integer + minimum: 0 + description: >- + [Account Archive](/docs/solana/account-archive) parameter. Return + the most recent write to this account strictly before slot `S`. + The response's `context.slot` is the actual slot at which that + write landed and can be used as the cursor for the next call. + Mutually exclusive with `slot`, `firstUpdateAfterSlot`, and + `minContextSlot`; requires `finalized` commitment (the default). + firstUpdateAfterSlot: + type: integer + minimum: 0 + description: >- + [Account Archive](/docs/solana/account-archive) parameter. Return + the first write to this account strictly after slot `S`. The + response's `context.slot` is the actual slot at which that write + landed and can be used as the cursor for the next call. Mutually + exclusive with `slot`, `lastUpdateBeforeSlot`, and + `minContextSlot`; requires `finalized` commitment (the default). GetBalanceConfig: title: GetBalance Configuration type: object diff --git a/src/openrpc/chains/_components/solana/methods.yaml b/src/openrpc/chains/_components/solana/methods.yaml index 0f19698ba..07ba1ae47 100644 --- a/src/openrpc/chains/_components/solana/methods.yaml +++ b/src/openrpc/chains/_components/solana/methods.yaml @@ -20,11 +20,36 @@ components: params: - name: Pubkey value: "GwsPP9HHhCvEQeu3HTFzsVL6DEtnnYw4ALEtA3fMBC9Q" + - name: Historical read at slot 400,000,000 (Account Archive) + params: + - name: Pubkey + value: "3emsAVdmGKERbHjmGfQ6oZ1e35dkf5iYcS6U4CPKFVaa" + - name: Configuration + value: + encoding: jsonParsed + slot: 400000000 result: name: Account information description: Returns details of the account including balance, ownership, and other relevant data. schema: $ref: "./account.yaml#/components/schemas/AccountInfo" + errors: + - code: -32020 + message: >- + Out of coverage. The requested historical slot is outside the + [Account Archive](/docs/solana/account-archive)'s coverage + window: below the earliest indexed slot, above the latest + finalized slot, or a `lastUpdateBeforeSlot` / + `firstUpdateAfterSlot` cursor stepped past the edge of coverage + (the terminal for a history walk). + - code: -32602 + message: >- + Invalid params. `slot`, `lastUpdateBeforeSlot`, + `firstUpdateAfterSlot`, and `minContextSlot` are mutually + exclusive. The historical parameters require `finalized` + commitment (the default); combining them with `processed` or + `confirmed` is rejected. Also returned for a malformed Pubkey + or configuration value. getBalance: name: getBalance