Entities are dirty-tracked and flushed at handler end. The most common subgraph bug isn't expressible.
Write the subgraph once.
Not three times.
A subgraph is three files held together by stringly-typed names. Redstart unifies schema, manifest, and mappings into one typed language — then transpiles to AssemblyScript the canonical toolchain compiles unmodified.
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.
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.
That claim is continuously checked: a field-level store-diff against independently hand-written subgraphs is the project's stated kill/pivot gate.
See the drift disappear.
The playground runs the real compiler in your browser. Type Redstart, watch the three files generate live.