Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experimental offchain tables #1120

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"@ponder/common": "0.0.0",
"@ponder/core": "0.6.2",
"create-ponder": "0.6.2",
"eslint-config-ponder": "0.6.2",
"@ponder/utils": "0.2.1"
},
"changesets": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE SCHEMA "offchain";
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "offchain"."metadata" (
"id" serial PRIMARY KEY NOT NULL,
"account" "bytea" NOT NULL
);
39 changes: 39 additions & 0 deletions examples/feature-api-functions/migrations/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"id": "a14201cb-be7c-4893-b690-69665c18479c",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"offchain.metadata": {
"name": "metadata",
"schema": "offchain",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"account": {
"name": "account",
"type": "bytea",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {
"offchain": "offchain"
},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
13 changes: 13 additions & 0 deletions examples/feature-api-functions/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1726868577670,
"tag": "0000_equal_tarantula",
"breakpoints": true
}
]
}
3 changes: 3 additions & 0 deletions examples/feature-api-functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
"start": "ponder start",
"codegen": "ponder codegen",
"serve": "ponder serve",
"generate": "drizzle-kit generate --dialect postgresql --schema ./ponder.schema.ts --out migrations",
"lint": "eslint .",
"typecheck": "tsc"
},
"dependencies": {
"@ponder/core": "workspace:*",
"drizzle-kit": "0.22.8",
"drizzle-orm": "^0.33.0",
"hono": "^4.5.0",
"viem": "^2.21.3"
},
Expand Down
4 changes: 1 addition & 3 deletions examples/feature-api-functions/ponder-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ declare module "@/generated" {
import type { Virtual } from "@ponder/core";

type config = typeof import("./ponder.config.ts").default;
type schema = typeof import("./ponder.schema.ts").default;
type schema = typeof import("./ponder.schema.ts");

export const ponder: Virtual.Registry<config, schema>;

Expand All @@ -21,8 +21,6 @@ declare module "@/generated" {
schema,
name
>;
export type ApiContext = Virtual.Drizzle<schema>;
export type IndexingFunctionArgs<name extends EventNames = EventNames> =
Virtual.IndexingFunctionArgs<config, schema, name>;
export type Schema = Virtual.Schema<schema>;
}
104 changes: 56 additions & 48 deletions examples/feature-api-functions/ponder.schema.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,58 @@
import { createSchema } from "@ponder/core";

export default createSchema((p) => ({
Account: p.createTable({
id: p.hex(),
balance: p.bigint(),
isOwner: p.boolean(),

allowances: p.many("Allowance.ownerId"),
approvalOwnerEvents: p.many("ApprovalEvent.ownerId"),
approvalSpenderEvents: p.many("ApprovalEvent.spenderId"),
transferFromEvents: p.many("TransferEvent.fromId"),
transferToEvents: p.many("TransferEvent.toId"),
}),
Allowance: p.createTable({
id: p.string(),
amount: p.bigint(),

ownerId: p.hex().references("Account.id"),
spenderId: p.hex().references("Account.id"),

owner: p.one("ownerId"),
spender: p.one("spenderId"),
import {
boolean,
evmBigint,
evmHex,
index,
integer,
offchainSchema,
onchainTable,
primaryKey,
serial,
} from "@ponder/core/db";

export const account = onchainTable("account", {
address: evmHex("address").primaryKey(),
balance: evmBigint("balance").notNull(),
isOwner: boolean("is_owner").notNull(),
});

export const allowance = onchainTable(
"allowance",
{
owner: evmHex("owner"),
spender: evmHex("spender"),
amount: evmBigint("amount").notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.owner, table.spender] }),
}),
TransferEvent: p.createTable(
{
id: p.string(),
amount: p.bigint(),
timestamp: p.int(),

fromId: p.hex().references("Account.id"),
toId: p.hex().references("Account.id"),

from: p.one("fromId"),
to: p.one("toId"),
},
{ fromIdIndex: p.index("fromId") },
),
ApprovalEvent: p.createTable({
id: p.string(),
amount: p.bigint(),
timestamp: p.int(),

ownerId: p.hex().references("Account.id"),
spenderId: p.hex().references("Account.id"),

owner: p.one("ownerId"),
spender: p.one("spenderId"),
);

export const transferEvent = onchainTable(
"transfer_event",
{
id: serial("id").primaryKey(),
amount: evmBigint("amount").$type<bigint>(),
timestamp: integer("timestamp"),
from: evmHex("from"),
to: evmHex("to"),
},
(table) => ({
fromIdx: index("from_index").on(table.from),
}),
}));
);

export const approvalEvent = onchainTable("approval_event", {
id: serial("id").primaryKey(),
amount: evmBigint("amount"),
timestamp: integer("timestamp"),
owner: evmHex("from"),
spender: evmHex("to"),
});

export const schema = offchainSchema("offchain");

export const metadata = schema.table("metadata", {
id: serial("id").primaryKey(),
account: evmHex("account").notNull(),
});
57 changes: 44 additions & 13 deletions examples/feature-api-functions/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,78 @@
import { ponder } from "@/generated";
import { count, desc, eq, graphql, or, replaceBigInts } from "@ponder/core";
import { replaceBigInts } from "@ponder/core";
import { count, desc, eq, or } from "@ponder/core/db";
import { formatEther, getAddress } from "viem";
import * as schema from "../../ponder.schema";

ponder.use("/graphql", graphql());
// ponder.use("/graphql", graphql());

ponder.get("/count", async (c) => {
const result = await c.db
.select({ count: count() })
.from(c.tables.TransferEvent);
.from(schema.transferEvent);

if (result.length === 0) return c.text("0");
return c.text(String(result[0]!.count));
});

ponder.get("/count/:address", async (c) => {
const account = getAddress(c.req.param("address"));
const { TransferEvent } = c.tables;

const result = await c.db
.select({ count: count() })
.from(c.tables.TransferEvent)
.from(schema.transferEvent)
.where(
or(eq(TransferEvent.fromId, account), eq(TransferEvent.toId, account)),
or(
eq(schema.transferEvent.from, account),
eq(schema.transferEvent.to, account),
),
);

if (result.length === 0) return c.text("0");
return c.text(String(result[0]!.count));
});

ponder.get("/whale-transfers", async (c) => {
const { TransferEvent, Account } = c.tables;

// Top 10 transfers from whale accounts
const result = await c.db
.select({
amount: TransferEvent.amount,
senderBalance: Account.balance,
sender: schema.account.address,
senderBalance: schema.account.balance,
amount: schema.transferEvent.amount,
})
.from(TransferEvent)
.innerJoin(Account, eq(TransferEvent.fromId, Account.id))
.orderBy(desc(Account.balance))
.from(schema.transferEvent)
.innerJoin(
schema.account,
eq(schema.transferEvent.from, schema.account.address),
)
.orderBy(desc(schema.account.balance))
.limit(10);

if (result.length === 0) return c.text("Not found", 500);
return c.json(replaceBigInts(result, (b) => formatEther(b)));
});

ponder.get("/register/:address", async (c) => {
const account = getAddress(c.req.param("address"));
await c.db.insert(schema.metadata).values({ account });

return c.text("Success", 200);
});

ponder.get("/user-transfers", async (c) => {
// Top 20 largest transfers to registered users
const result = await c.db
.select({
amount: schema.transferEvent.amount,
account: schema.metadata.account,
})
.from(schema.transferEvent)
.innerJoin(
schema.metadata,
eq(schema.transferEvent.to, schema.metadata.account),
)
.orderBy(desc(schema.transferEvent.amount))
.limit(20);

return c.json(replaceBigInts(result, (b) => formatEther(b)));
});
Loading