-
Notifications
You must be signed in to change notification settings - Fork 1
feat: integrate The Graph support into EAS client #1239
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
e7c008c
f21cf2c
49ebfe3
2d82eb0
a349fb7
9e521cb
64c80fb
cba36d8
3672801
02b0ceb
a206d06
3c0ebe9
491b624
01aa92e
d101259
d4098aa
f13b6a7
3386506
ad111ca
12b6d26
3845d8a
81f7463
e604503
726bed4
392afe4
0171839
897d246
3237aba
c068ec2
d7d769b
072d6e1
e4798ad
c07c52c
6acb92e
3e80644
b873421
a397b1d
90af661
7e21d52
2aa8063
683f19f
3b8b564
01e81cd
f23b11d
f5b243f
039dc56
ce88136
3b46e8c
f3212b5
ed5daff
a1956e3
536f1ff
dffd89d
be41a39
bf5b47a
7887161
fbc830f
f5c640f
de359a4
2cf73de
75464f3
aac4392
1aebb7d
a1816f0
4efab84
7e2352c
2957a77
3ae1093
7a6a712
5abdb50
f0165b5
4a058b2
a560d1b
94771a0
3eeeb90
9dac83d
ab871b2
ef5913c
27665ac
07f1627
14c645b
2d2b6fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { createPortalClient, waitForTransactionReceipt } from "@settlemint/sdk-portal"; | ||
| import { createTheGraphClient, type ResultOf } from "@settlemint/sdk-thegraph"; | ||
| import { createLogger, requestLogger } from "@settlemint/sdk-utils/logging"; | ||
| import { validate } from "@settlemint/sdk-utils/validation"; | ||
| import type { Address, Hex } from "viem"; | ||
|
|
@@ -46,6 +47,7 @@ export class EASClient { | |
| private readonly portalClient: PortalClient["client"]; | ||
| private readonly portalGraphql: PortalClient["graphql"]; | ||
| private deployedAddresses?: DeploymentResult; | ||
| private theGraph?: ReturnType<typeof createTheGraphClient<any>>; | ||
|
|
||
| /** | ||
| * Create a new EAS client instance | ||
|
|
@@ -74,6 +76,19 @@ export class EASClient { | |
|
|
||
| this.portalClient = portalClient; | ||
| this.portalGraphql = portalGraphql; | ||
|
|
||
| // Initialize The Graph client if configured | ||
| if (this.options.theGraph) { | ||
| this.theGraph = createTheGraphClient<any>( | ||
| { | ||
| instances: this.options.theGraph.instances, | ||
| accessToken: this.options.theGraph.accessToken, | ||
| subgraphName: this.options.theGraph.subgraphName, | ||
| cache: this.options.theGraph.cache, | ||
| }, | ||
| undefined, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -538,9 +553,38 @@ export class EASClient { | |
| * Consider using getSchema() for individual schema lookups. | ||
| */ | ||
| public async getSchemas(_options?: GetSchemasOptions): Promise<SchemaData[]> { | ||
| throw new Error( | ||
| "Schema listing not implemented yet. Portal's direct contract queries don't support listing all schemas. Use getSchema() for individual schema lookups or implement The Graph subgraph integration for bulk queries.", | ||
| ); | ||
| if (!this.theGraph) { | ||
| throw new Error( | ||
| "Schema listing requires The Graph configuration. Provide 'theGraph' options when creating EAS client.", | ||
| ); | ||
| } | ||
|
|
||
| // Basic listing without filters. Pagination can be added via @fetchAll directive. | ||
| const query = this.theGraph.graphql(` | ||
| query ListSchemas($first: Int = 100, $skip: Int = 0) { | ||
| schemas(first: $first, skip: $skip) @fetchAll { | ||
| id | ||
| resolver | ||
| revocable | ||
| schema | ||
| } | ||
| } | ||
| `); | ||
|
|
||
| const result = (await this.theGraph.client.request(query)) as ResultOf<typeof query>; | ||
| const list = (result as any).schemas as Array<{ | ||
| id: string; | ||
| resolver: string; | ||
| revocable: boolean; | ||
| schema: string | null; | ||
| }>; | ||
|
|
||
| return list.map((s) => ({ | ||
| uid: s.id as Hex, | ||
| resolver: s.resolver as Address, | ||
| revocable: Boolean(s.revocable), | ||
| schema: s.schema ?? "", | ||
| })); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The public async getSchemas(options?: GetSchemasOptions): Promise<SchemaData[]> {
if (!this.theGraph) {
throw new Error(
"Schema listing requires The Graph configuration. Provide 'theGraph' options when creating EAS client.",
);
}
// Basic listing without filters. Pagination can be added via @fetchAll directive.
const query = this.theGraph.graphql(`
query ListSchemas($first: Int = 100, $skip: Int = 0) {
schemas(first: $first, skip: $skip) @fetchAll {
id
resolver
revocable
schema
}
}
`);
const variables = {
first: options?.limit ?? 100,
skip: options?.offset ?? 0,
};
const result = (await this.theGraph.client.request(query, variables)) as ResultOf<typeof query>;
const list = (result as any).schemas as Array<{
id: string;
resolver: string;
revocable: boolean;
schema: string | null;
}>;
return list.map((s) => ({
uid: s.id as Hex,
resolver: s.resolver as Address,
revocable: Boolean(s.revocable),
schema: s.schema ?? "",
}));
} |
||
|
|
||
| /** | ||
|
|
@@ -586,10 +630,72 @@ export class EASClient { | |
| * as Portal's direct contract queries don't support listing all attestations. | ||
| * Consider using getAttestation() for individual attestation lookups. | ||
| */ | ||
| public async getAttestations(_options?: GetAttestationsOptions): Promise<AttestationInfo[]> { | ||
| throw new Error( | ||
| "Attestation listing not implemented yet. Portal's direct contract queries don't support listing all attestations. Use getAttestation() for individual attestation lookups or implement The Graph subgraph integration for bulk queries.", | ||
| ); | ||
| public async getAttestations(options?: GetAttestationsOptions): Promise<AttestationInfo[]> { | ||
| if (!this.theGraph) { | ||
| throw new Error( | ||
| "Attestation listing requires The Graph configuration. Provide 'theGraph' options when creating EAS client.", | ||
| ); | ||
| } | ||
|
|
||
| const query = this.theGraph.graphql(` | ||
| query ListAttestations($first: Int = 100, $skip: Int = 0, $schema: Bytes, $attester: Bytes, $recipient: Bytes) { | ||
| attestations( | ||
| first: $first | ||
| skip: $skip | ||
| where: { | ||
| ${options?.schema ? "schema: $schema" : ""} | ||
| ${options?.attester ? "attester: $attester" : ""} | ||
| ${options?.recipient ? "recipient: $recipient" : ""} | ||
| } | ||
| ) @fetchAll { | ||
| id | ||
| schema { id } | ||
| attester | ||
| recipient | ||
| time | ||
| expirationTime | ||
| revocable | ||
| refUID | ||
| data | ||
| revokedAt | ||
| } | ||
| } | ||
| `); | ||
|
|
||
| const variables: Record<string, unknown> = { | ||
| first: options?.limit ?? 100, | ||
| skip: options?.offset ?? 0, | ||
| }; | ||
| if (options?.schema) variables.schema = options.schema; | ||
| if (options?.attester) variables.attester = options.attester; | ||
| if (options?.recipient) variables.recipient = options.recipient; | ||
|
|
||
| const result = (await this.theGraph.client.request(query, variables)) as ResultOf<typeof query>; | ||
| const list = (result as any).attestations as Array<{ | ||
| id: string; | ||
| schema: { id: string } | string; | ||
| attester: string; | ||
| recipient: string; | ||
| time: string | number | null; | ||
| expirationTime: string | number | null; | ||
| revocable: boolean; | ||
| refUID: string | null; | ||
| data: string | null; | ||
| revokedAt?: string | null; | ||
| }>; | ||
|
|
||
| return list.map((a) => ({ | ||
| uid: (a.id ?? (a as any).uid) as Hex, | ||
| schema: (typeof a.schema === "string" ? a.schema : a.schema.id) as Hex, | ||
| attester: a.attester as Address, | ||
| recipient: a.recipient as Address, | ||
| time: a.time ? BigInt(a.time) : BigInt(0), | ||
| expirationTime: a.expirationTime ? BigInt(a.expirationTime) : BigInt(0), | ||
| revocable: Boolean(a.revocable), | ||
| refUID: (a.refUID ?? ("0x" + "0".repeat(64))) as Hex, | ||
| data: (a.data ?? "0x") as Hex, | ||
| value: BigInt(0), | ||
| })); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The mapping logic for attestations includes defensive code that seems to contradict the GraphQL query and the subgraph schema. For instance:
This defensive coding makes the logic harder to follow and maintain. It should be simplified to align with the expected response shape defined by the GraphQL query. return list.map((a) => ({
uid: a.id as Hex,
schema: (a.schema as { id: string }).id as Hex,
attester: a.attester as Address,
recipient: a.recipient as Address,
time: a.time ? BigInt(a.time) : BigInt(0),
expirationTime: a.expirationTime ? BigInt(a.expirationTime) : BigInt(0),
revocable: Boolean(a.revocable),
refUID: (a.refUID ?? ("0x" + "0".repeat(64))) as Hex,
data: (a.data ?? "0x") as Hex,
value: BigInt(0),
})); |
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| /** | ||
| * EAS The Graph Listing Example | ||
| * | ||
| * This example demonstrates how to configure the EAS SDK with The Graph | ||
| * and perform bulk reads for schemas and attestations using the same style | ||
| * as the other examples. | ||
| */ | ||
|
|
||
| import { createEASClient } from "../eas.js"; | ||
| import { createLogger, requestLogger } from "@settlemint/sdk-utils/logging"; | ||
| import { loadEnv } from "@settlemint/sdk-utils/environment"; | ||
| import type { Address, Hex } from "viem"; | ||
| import { decodeAbiParameters, parseAbiParameters } from "viem"; | ||
|
|
||
| async function theGraphWorkflow() { | ||
| const logger = createLogger(); | ||
| const env = await loadEnv(true, false); | ||
|
|
||
| if (!env.SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT) { | ||
| console.error("❌ Missing SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT"); | ||
| process.exit(1); | ||
| } | ||
| if (!env.SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS || env.SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS.length === 0) { | ||
| console.error("❌ Missing SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS (JSON array of subgraph URLs ending with /<name>)"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log("🚀 EAS The Graph Listing Example"); | ||
| console.log("================================\n"); | ||
|
|
||
| // Build client with The Graph config | ||
| const eas = createEASClient( | ||
| { | ||
| instance: env.SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT, | ||
| accessToken: env.SETTLEMINT_ACCESS_TOKEN, | ||
| theGraph: { | ||
| instances: env.SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS, | ||
| subgraphName: env.SETTLEMINT_THEGRAPH_DEFAULT_SUBGRAPH ?? "eas", | ||
| accessToken: env.SETTLEMINT_ACCESS_TOKEN, | ||
| cache: "force-cache", | ||
| }, | ||
| debug: true, | ||
| }, | ||
| ); | ||
|
|
||
| // Replace global fetch logging for visibility (no-op in Node unless used) | ||
| // This pattern mirrors other examples and ensures consistent logging. | ||
| void requestLogger(logger, "eas-thegraph", fetch); | ||
|
|
||
| // Step 1: List Schemas | ||
| console.log("📚 Step 1: List Schemas (via The Graph)"); | ||
| try { | ||
| const schemas = await eas.getSchemas({ limit: 10, offset: 0 }); | ||
| if (schemas.length === 0) { | ||
| console.log("⚠️ No schemas found in the subgraph\n"); | ||
| } else { | ||
| console.log(`✅ Found ${schemas.length} schema(s)`); | ||
| for (const s of schemas) { | ||
| console.log(` • ${s.uid} | revocable=${s.revocable} | resolver=${s.resolver}`); | ||
| } | ||
| console.log(); | ||
| } | ||
| } catch (error) { | ||
| console.log("❌ Failed to list schemas:", error); | ||
| console.log(); | ||
| } | ||
|
|
||
| // Step 2: List Attestations (optionally filter by schema/attester/recipient) | ||
| console.log("🧾 Step 2: List Attestations (via The Graph)"); | ||
| try { | ||
| // Optional filters | ||
| const schemaUID = process.env.EAS_SCHEMA_UID as Hex | undefined; | ||
| const attester = process.env.EAS_ATTESTER as Address | undefined; | ||
| const recipient = (env.SETTLEMINT_DEPLOYER_ADDRESS as Address | undefined) ?? (process.env.EAS_RECIPIENT as Address | undefined); | ||
|
|
||
| const attestations = await eas.getAttestations({ | ||
| limit: 10, | ||
| offset: 0, | ||
| schema: schemaUID, | ||
| attester, | ||
| recipient, | ||
| }); | ||
|
|
||
| if (attestations.length === 0) { | ||
| console.log("⚠️ No attestations found with current filters\n"); | ||
| } else { | ||
| console.log(`✅ Found ${attestations.length} attestation(s)`); | ||
| for (const a of attestations) { | ||
| console.log(` • uid=${a.uid} schema=${a.schema} attester=${a.attester} recipient=${a.recipient}`); | ||
| } | ||
| console.log(); | ||
| } | ||
| } catch (error) { | ||
| console.log("❌ Failed to list attestations:", error); | ||
| console.log(); | ||
| } | ||
|
|
||
| // Step 3: (Optional) Decode attestation data for the first attestation of the first schema | ||
| console.log("🔍 Step 3: Optional Data Decode Example"); | ||
| try { | ||
| const schemas = await eas.getSchemas({ limit: 1, offset: 0 }); | ||
| if (schemas.length === 0) { | ||
| console.log("ℹ️ Skipping decode: no schemas available\n"); | ||
| return; | ||
| } | ||
|
|
||
| const schema = schemas[0]; | ||
| const [example] = await eas.getAttestations({ limit: 1, offset: 0, schema: schema.uid }); | ||
| if (!example || !example.data || example.data === ("0x" as Hex)) { | ||
| console.log("ℹ️ Skipping decode: no example attestation with data found\n"); | ||
| return; | ||
| } | ||
|
|
||
| // Convert the EAS schema string (e.g., "uint256 score, address user") to ABI parameter format | ||
| const abiParams = parseAbiParameters(schema.schema); | ||
| const decoded = decodeAbiParameters(abiParams, example.data); | ||
|
|
||
| console.log("✅ Decoded example attestation data:"); | ||
| console.log(decoded); | ||
| console.log(); | ||
| } catch (error) { | ||
| console.log("⚠️ Decode step failed:", error); | ||
| console.log(); | ||
| } | ||
|
|
||
| console.log("🎉 The Graph listing example complete\n"); | ||
| } | ||
|
|
||
| if (typeof require !== "undefined" && require.main === module) { | ||
| theGraphWorkflow().catch(console.error); | ||
| } | ||
|
|
||
| export { theGraphWorkflow }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "abi": [ | ||
| {"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"uid","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"schema","type":"bytes32"},{"indexed":true,"internalType":"address","name":"attester","type":"address"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint64","name":"time","type":"uint64"},{"indexed":false,"internalType":"uint64","name":"expirationTime","type":"uint64"},{"indexed":false,"internalType":"bool","name":"revocable","type":"bool"},{"indexed":false,"internalType":"bytes32","name":"refUID","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"Attested","type":"event"}, | ||
| {"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"revoker","type":"address"},{"indexed":true,"internalType":"bytes32","name":"schema","type":"bytes32"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"bytes32","name":"uid","type":"bytes32"}],"name":"Revoked","type":"event"} | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "abi": [ | ||
| {"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"uid","type":"bytes32"},{"indexed":true,"internalType":"address","name":"resolver","type":"address"},{"indexed":false,"internalType":"bool","name":"revocable","type":"bool"},{"indexed":false,"internalType":"string","name":"schema","type":"string"}],"name":"Registered","type":"event"} | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # EAS Subgraph Schema (MVP) | ||
|
|
||
| type Schema @entity { | ||
| id: Bytes! # uid | ||
| resolver: Bytes! | ||
| revocable: Boolean! | ||
| schema: String! | ||
| createdAt: BigInt! | ||
| txHash: Bytes! | ||
| } | ||
|
|
||
| type Attestation @entity { | ||
| id: Bytes! # uid | ||
| schema: Schema! | ||
| attester: Bytes! | ||
| recipient: Bytes! | ||
| time: BigInt! | ||
| expirationTime: BigInt! | ||
| revocable: Boolean! | ||
| refUID: Bytes! | ||
| data: Bytes! | ||
| revokedAt: BigInt | ||
| txHash: Bytes! | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
anywhen creating the The Graph client (createTheGraphClient<any>) disables the type-safety features ofgql.tada. This leads toanytypes for query results downstream, forcing the use of unsafe type casts like(result as any).schemasto access the data.While this might be a pragmatic choice to avoid making the
EASClientgeneric over the subgraph schema, it's a significant trade-off that reduces maintainability and increases the risk of runtime errors if the GraphQL schema changes. It would be beneficial to explore ways to provide stronger types here.