Skip to content

Time Travel

A StoreKit store’s full history is already on chain. A store’s boxes only change through its own ABI calls — set, set_bytes, concat and remove, which only the creator can make — and through its deletion. Every confirmed call succeeded, so replaying the app’s calls in chain order rebuilds the exact boxes at every round. Any indexer can serve that history; no extra contract state, events or services are needed.

Try it

Pick a network and a store’s app id (a store you deploy on the Quick Start is filled in), then step through its history.

Replay a store’s history

import {
fetchStoreHistory,
replay,
stateFromBoxes,
diffBoxes,
} from "@awesome-algorand/store-kit/history";
// Every create / set / concat / remove / meta update / delete, in chain order
const history = await fetchStoreHistory(algorand.client.indexer, appId);
// The store at the end of a round...
const then = stateFromBoxes(replay(history, { round: 1234n }));
// ...or after its first n operations, and what the n-th one changed
const before = replay(history, { count: n - 1 });
const after = replay(history, { count: n });
const changes = diffBoxes(before, after); // [{ path, kind: "added" | "changed" | "removed", ... }]
  • fetchStoreHistory(indexer, appId, { minRound, maxRound, onPage, signal }) pages through the indexer and also decodes inner calls, so stores written by other contracts replay too.
  • replay(history, { round | count }) returns the raw boxes by path; stateFromBoxes assembles them into the object, decodeBoxes gives decoded values by path. Pass { schema } for stores packed with a JSON Schema.
  • replayMeta(history, until) gives the store’s metadata at that point: whether it exists, its creation/deletion round, contract_name, contract_type and schema_app_id.
  • changeRounds(history) lists the rounds at which the store changed.

Read from a time travel node

A go-algorand node with EnableTimeTravel answers box reads for any round and lists each box’s versions. A stock node only serves recent rounds.

import {
fetchBoxesAtRound,
fetchBoxVersions,
isTimeTravelNode,
stateFromBoxes,
} from "@awesome-algorand/store-kit/history";
const node = { baseUrl: "https://explorer.shore-tech.net/tt/fnet-v1" };
if (await isTimeTravelNode(node)) {
// GET /v2/applications/{id}/boxes?round=R&include=values
const state = stateFromBoxes(await fetchBoxesAtRound(node, appId, 1234n));
// GET /v2/applications/{id}/box/versions?name=…
const versions = await fetchBoxVersions(node, appId, "count");
// [{ round, timestamp, deleted, value }, ...]
}

Both sources give the same boxes, so an app can replay from the indexer and check any round against a time travel node.

Find stores

A store declares itself with a protocol global state key set to storekit/1 when it is created. Stores deployed before that are recognized by the StoreKit ABI selectors (set, get, remove) in their approval program.

import { inspectStoreApp } from "@awesome-algorand/store-kit/history";
const { application } = await indexer.lookupApplications(appId).do();
const info = inspectStoreApp(application.params);
// { isStore: true, via: "protocol" | "abi", protocol: "storekit/1",
// methods: ["set", "get", "remove", ...], contractName, contractType, schemaAppId }