Graphite

Write The Graph subgraph handlers in Rust. The compiled WASM is AssemblyScript-ABI-compatible — unmodified graph-node accepts it as a standard subgraph.

Live on The Graph Studio. ERC20 and ERC721 subgraphs are deployed and indexing on Arbitrum One. Zero graph-node changes required.


Why Rust?

AssemblyScript is a subset of TypeScript that compiles to WASM. It works, but it gives up most of what makes typed languages useful: no closures, no iterators, no algebraic types, no real ecosystem. Graphite lets you write the same subgraph mappings in Rust and get all of that back — plus cargo test without Docker.

How It Works

Graph-node identifies WASM subgraphs by the structure of their memory and the names of their exported functions — not by who wrote them. The graph-as-runtime crate implements the AssemblyScript memory model in Rust: 20-byte object headers, UTF-16LE strings, TypedMap entity layout, the full set of host function imports. The resulting WASM is structurally indistinguishable from AssemblyScript output. The manifest declares language: wasm/assemblyscript and graph-node accepts it without any special handling.

Your Rust handler
      │
      ▼
graphite-macros  (#[handler], #[derive(Entity)])
      │
      ▼
graph-as-runtime  (AS ABI: allocator, UTF-16LE strings, TypedMap, host imports)
      │
      ▼
WASM binary  ──────────────────►  unmodified graph-node / The Graph Studio

Quick Example

#![allow(unused)]
#![cfg_attr(target_arch = "wasm32", no_std)]
fn main() {
extern crate alloc;

use alloc::format;
use graphite_macros::handler;

mod generated;
use generated::{ERC20TransferEvent, Transfer};

#[handler]
pub fn handle_transfer(event: &ERC20TransferEvent, ctx: &graphite::EventContext) {
    let id = format!("{}-{}", hex(&ctx.tx_hash), hex(&ctx.log_index));
    Transfer::new(&id)
        .set_from(event.from.to_vec())
        .set_to(event.to.to_vec())
        .set_value(event.value.clone())
        .set_block_number(ctx.block_number.clone())
        .set_timestamp(ctx.block_timestamp.clone())
        .save();
}
}

Test it natively — no Docker, no PostgreSQL:

#![allow(unused)]
fn main() {
#[test]
fn transfer_creates_entity() {
    mock::reset();
    handle_transfer_impl(&event, &graphite::EventContext::default());
    assert_eq!(mock::entity_count("Transfer"), 1);
}
}

Feature Parity

FeatureStatus
Event / Call / Block / File handlers
store.set / store.get / store.remove / store.getInBlock
ethereum.call, ethereum.encode, ethereum.decode
log.info / log.warning / log.error / log.critical
ipfs.cat, json.fromBytes, ens.nameByAddress
dataSource.create / createWithContext / context accessors
crypto.keccak256 / sha256 / sha3 / secp256k1.recover
BigInt — full arithmetic, bitwise, shifts
BigDecimal — full arithmetic
All GraphQL scalar types
Block handler filters (polling, every: N)
Native cargo test (no Docker)
Non-fatal errors

Crates

CratePurpose
graph-as-runtimeno_std AS ABI layer: allocator, type layout, host FFI
graphite-macros#[handler], #[derive(Entity)] proc macros
graphite-cliCLI: init, codegen, manifest, build, test, deploy
graphite-sdkUser-facing SDK and MockHost for native testing

The SDK crate is published as graphite-sdk but imported as graphite:

[dependencies]
graphite = { package = "graphite-sdk", version = "1", default-features = false }