Entities are dirty-tracked and flushed at handler end. The most common subgraph bug isn't expressible.
Write the subgraph once.
Properly.
The most performant and secure language for authoring The Graph subgraphs. One typed source for schema, manifest, and mappings — compiled to AssemblyScript that's faster and safer than any human would hand-write. If it compiles, it works.
The pitch
Would you rather write & maintain this —
abi ERC20 from "./abis/ERC20.json"
entity Account {
id: Id<Bytes>
balance: BigInt
}
entity Transfer immutable {
id: Id<Bytes>
from: Account
to: Account
value: BigInt
timestamp: BigInt
}
source Token {
abi: ERC20
network: mainnet
address: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
startBlock: 6082465
}
handler on Token.Transfer(event) {
let sender = Account.loadOrCreate(event.params.from, { balance: BigInt.zero })
let receiver = Account.loadOrCreate(event.params.to, { balance: BigInt.zero })
sender.balance = sender.balance - event.params.value
receiver.balance = receiver.balance + event.params.value
// dirty-tracked, auto-saved — forgetting .save() can't happen
Transfer.create(event.id, {
from: event.params.from,
to: event.params.to,
value: event.params.value,
timestamp: event.block.timestamp,
})
}import { BigInt } from "@graphprotocol/graph-ts";
import {
Transfer as TransferEvent,
} from "../generated/Token/ERC20";
import { Account, Transfer } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let sender = Account.load(event.params.from);
if (sender == null) {
sender = new Account(event.params.from);
sender.balance = BigInt.zero();
}
let receiver = Account.load(event.params.to);
if (receiver == null) {
receiver = new Account(event.params.to);
receiver.balance = BigInt.zero();
}
sender.balance = sender.balance.minus(event.params.value);
receiver.balance = receiver.balance.plus(event.params.value);
sender.save(); // forget this and balances silently desync
receiver.save();
let transfer = new Transfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.value = event.params.value;
transfer.timestamp = event.block.timestamp;
transfer.save();
}Both produce the same store. Only one of them can't drift, can't forget a .save(), and can't let a renamed event compile. The AssemblyScript on the right is the genuine hand-written reference from our conformance suite — not a strawman.
Unrepresentable by construction
A whole class of bugs you can't write.
There is no null — only Option<T>. Maths on a maybe-absent value is a compile error, not a silent miscompile.
Contract calls return Result. You must match before touching the value, so a revert can't kill the handler.
Event signatures are derived from the ABI by reference. Rename an event and it's a compile error.
@derivedFrom fields are read-only by construction. Assigning to one doesn't type-check.
One equality, lowered correctly every time. The classic AssemblyScript footgun is gone.
Date.now() and Math.random() are a compile error — they diverge Proof-of-Indexing across indexers and get them slashed.
A contract call inside a loop — the documented 'stuck at 3%' sync killer — is flagged before you deploy it.
No lock-in
The eject path is the whole bet.
redstart build emits the exact files a hand-written subgraph has — readable, idiomatic AssemblyScript and GraphQL that graph build compiles unmodified. Walk away whenever you like; you keep the generated code, and it keeps working.
And it's proven, not promised: deployed to a live graph-node against an independent hand-written reference, our lowered AssemblyScript store-diffed 0 differences across every entity and field — byte-identical indexing.
See the drift disappear.
The playground runs the real compiler in your browser. Type Redstart, watch the three files generate live.