Skip to content
Merged
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
8 changes: 4 additions & 4 deletions apps/indexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
"typecheck": "tsc"
},
"dependencies": {
"@ponder/core": "0.5.0-next.1",
"hono": "^4.4.7",
"hono": "^4.9.4",
"ponder": "^0.12.16",
"shared": "workspace:*",
"viem": "^1.19.3"
"viem": "^2.8.2"
},
"devDependencies": {
"@types/node": "^20.9.0",
Expand All @@ -25,4 +25,4 @@
"engines": {
"node": ">=18.14"
}
}
}
36 changes: 12 additions & 24 deletions apps/indexer/ponder-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
// This file enables type checking and editor autocomplete for this Ponder project.
// After upgrading, you may find that changes have been made to this file.
// If this happens, please commit the changes. Do not manually edit this file.
// See https://ponder.sh/docs/getting-started/installation#typescript for more information.

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;
/// <reference types="ponder/virtual" />

export const ponder: Virtual.Registry<config, schema>;
declare module "ponder:internal" {
const config: typeof import("./ponder.config.ts");
const schema: typeof import("./ponder.schema.ts");
}

export type EventNames = Virtual.EventNames<config>;
export type Event<name extends EventNames = EventNames> = Virtual.Event<
config,
name
>;
export type Context<name extends EventNames = EventNames> = Virtual.Context<
config,
schema,
name
>;
export type IndexingFunctionArgs<name extends EventNames = EventNames> =
Virtual.IndexingFunctionArgs<config, schema, name>;
export type Schema = Virtual.Schema<schema>;
declare module "ponder:schema" {
export * from "./ponder.schema.ts";
}

// This file enables type checking and editor autocomplete for this Ponder project.
// After upgrading, you may find that changes have been made to this file.
// If this happens, please commit the changes. Do not manually edit this file.
// See https://ponder.sh/docs/requirements#typescript for more information.
10 changes: 5 additions & 5 deletions apps/indexer/ponder.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { createConfig } from '@ponder/core'
import { createConfig } from 'ponder'
import { erc20MultiDelegateContract } from 'shared/contracts'
import { http } from 'viem'

export default createConfig({
networks: {
chains: {
mainnet: {
chainId: 1,
transport: http(process.env.PONDER_RPC_URL_1),
id: 1,
rpc: process.env.PONDER_RPC_URL_1,
},
},
contracts: {
MultiDelegate: {
...erc20MultiDelegateContract,
network: 'mainnet',
chain: 'mainnet',
startBlock: erc20MultiDelegateContract.deployedBock,
},
},
Expand Down
102 changes: 40 additions & 62 deletions apps/indexer/ponder.schema.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,45 @@
import { createSchema } from '@ponder/core'
import { onchainTable } from 'ponder'

/*
I want to be able to query the following:

{
account ("0x123") {
delegates {
id
amount
}
}
}

or if that doesn't work, this is ok:

{
account ("0x123") {
delegates
values
}
}

last option is a custom GET endpoint that returns something like this:

[
{
"delegate": "0x534631bcf33bdb069fb20a93d2fdb9e4d4dd42cf",
"tokenId": "475411618940684652382658899876961866559843549903",
"amount": "25000000000000000000"
},
{
"delegate": "0xa7860e99e3ce0752d1ac53b974e309fff80277c6",
"tokenId": "956391030522194004329440103514838893413546489798",
"amount": "10000000000000000000"
},
]
*/

export default createSchema((p) => ({
Account: p.createTable({
id: p.hex(),
delegates: p.hex().list(),
}),
export const Account = onchainTable('Account', (t) => ({
id: t.hex().primaryKey(),
delegates: t.hex().array().notNull(),
}))

DelegationProcessedEvent: p.createTable({
id: p.string(),
from: p.hex(),
to: p.hex(),
amount: p.bigint(),
}),
export const DelegationProcessedEvent = onchainTable(
'DelegationProcessedEvent',
(t) => ({
id: t.text().primaryKey(),
timestamp: t.bigint().notNull(),
owner: t.hex().notNull(),
from: t.hex().notNull(),
to: t.hex().notNull(),
amount: t.bigint().notNull(),
})
)

export const ProxyDeployedEvent = onchainTable('ProxyDeployedEvent', (t) => ({
id: t.text().primaryKey(),
timestamp: t.bigint().notNull(),
delegate: t.hex().notNull(),
proxyAddress: t.hex().notNull(),
}))

ProxyDeployedEvent: p.createTable({
id: p.string(),
delegate: p.hex(),
proxyAddress: p.hex(),
}),
export const TransferBatchEvent = onchainTable('TransferBatchEvent', (t) => ({
id: t.text().primaryKey(),
timestamp: t.bigint().notNull(),
operator: t.hex().notNull(),
from: t.hex().notNull(),
to: t.hex().notNull(),
ids: t.bigint().array().notNull(),
values: t.bigint().array().notNull(),
}))

TransferBatchEvent: p.createTable({
id: p.string(),
operator: p.hex(),
from: p.hex(),
to: p.hex(),
ids: p.bigint().list(),
values: p.bigint().list(),
}),
export const TransferEvent = onchainTable('TransferEvent', (t) => ({
key: t.text().primaryKey(),
timestamp: t.bigint().notNull(),
operator: t.hex().notNull(),
from: t.hex().notNull(),
to: t.hex().notNull(),
id: t.bigint().notNull(),
value: t.bigint().notNull(),
}))
44 changes: 27 additions & 17 deletions apps/indexer/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
import { graphql } from '@ponder/core'
import { cors } from 'hono/cors'
import { Hono } from 'hono'
import { eq, graphql } from 'ponder'
import { db, publicClients } from 'ponder:api'
import schema, { Account, DelegationProcessedEvent } from 'ponder:schema'
import { erc20MultiDelegateContract } from 'shared/contracts'
import { createPublicClient, isAddress } from 'viem'
import { isAddress } from 'viem'

import { ponder } from '@/generated'
const app = new Hono()

import ponderConfig from '../../ponder.config'
app.use('/', graphql({ db, schema }))

ponder.use('*', cors())
app.get('/:address', async (ctx) => {
const { address } = ctx.req.param()

ponder.use('/', graphql())
if (!isAddress(address)) {
return ctx.json({ error: 'Invalid address' })
}

ponder.get('/:address', async (c) => {
const { address } = c.req.param()
const { Account } = c.get('db')
const client = createPublicClient(ponderConfig.networks.mainnet)
const result = await db
.select()
.from(Account)
.where(eq(Account.id, address))
.limit(1)

if (!isAddress(address)) {
return c.json({ error: 'Invalid address' })
if (result.length === 0 || result[0]?.delegates?.length == null) {
return ctx.json([])
}

const delegates = (await Account.findUnique({ id: address }))?.delegates || []
const tokenIds = delegates.map((item) => BigInt(item))
const { delegates } = result[0]
const tokenIds = delegates.map((item: string | number | bigint | boolean) =>
BigInt(item)
)

const balanceOf = await client.readContract({
const balanceOf = await publicClients.mainnet.readContract({
...erc20MultiDelegateContract,
functionName: 'balanceOfBatch',
args: [new Array(tokenIds.length).fill(address), tokenIds],
Expand All @@ -36,7 +44,7 @@ ponder.get('/:address', async (c) => {
}))

// remove delegates with no balance
return c.json(
return ctx.json(
data
.filter((item) => item.amount !== '0')
.map((item) => ({
Expand All @@ -45,3 +53,5 @@ ponder.get('/:address', async (c) => {
}))
)
})

export default app
87 changes: 58 additions & 29 deletions apps/indexer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,79 @@
import { toHex } from 'viem'

import { ponder } from '@/generated'
import { ponder } from 'ponder:registry'
import {
Account,
DelegationProcessedEvent,
ProxyDeployedEvent,
TransferBatchEvent,
TransferEvent,
} from 'ponder:schema'
import { toHex, zeroAddress } from 'viem'

ponder.on('MultiDelegate:DelegationProcessed', async ({ event, context }) => {
const { DelegationProcessedEvent } = context.db

await DelegationProcessedEvent.create({
id: event.log.id,
data: event.args,
await context.db.insert(DelegationProcessedEvent).values({
id: event.id,
timestamp: event.block.timestamp,
...event.args,
})
})

ponder.on('MultiDelegate:ProxyDeployed', async ({ event, context }) => {
const { ProxyDeployedEvent } = context.db

await ProxyDeployedEvent.create({
id: event.log.id,
data: event.args,
await context.db.insert(ProxyDeployedEvent).values({
id: event.id,
timestamp: event.block.timestamp,
...event.args,
})
})

ponder.on('MultiDelegate:TransferBatch', async ({ event, context }) => {
const { Account, TransferBatchEvent } = context.db
const { to, ids, values } = event.args
const delegates = ids.map((id) => toHex(id))

await TransferBatchEvent.create({
id: event.log.id,
data: {
...event.args,
ids: ids.map((id) => id),
values: values.map((value) => value),
},
await context.db.insert(TransferBatchEvent).values({
id: event.id,
timestamp: event.block.timestamp,
...event.args,
ids: ids.map((id) => id),
values: values.map((value) => value),
})

if (to === '0x0000000000000000000000000000000000000000') return
if (to === zeroAddress) return

// Store any address that an account has ever delegated to, even if it's currently not
// TODO: Store `amount` here as well so we don't need a separate endpoint
await Account.upsert({
id: to,
create: {
await context.db
.insert(Account)
.values({
id: to,
delegates,
},
update: ({ current }) => ({
delegates: Array.from(new Set([...current.delegates, ...delegates])),
}),
})
.onConflictDoUpdate((current) => ({
delegates: Array.from(
new Set([...(current.delegates || []), ...delegates])
),
}))
})

ponder.on('MultiDelegate:TransferSingle', async ({ event, context }) => {
const { to, id, value } = event.args
const delegate = toHex(id)

await context.db.insert(TransferEvent).values({
key: event.id,
timestamp: event.block.timestamp,
...event.args,
})

if (to === zeroAddress) return

// Store any address that an account has ever delegated to, even if it's currently not
// TODO: Store `amount` here as well so we don't need a separate endpoint
await context.db
.insert(Account)
.values({
id: to,
delegates: [delegate],
})
.onConflictDoUpdate((current) => ({
delegates: Array.from(new Set([...(current.delegates || []), delegate])),
}))
})
Loading
Loading