From 2c48d035e9b3a44dea30772a0d1ff8504e9612d4 Mon Sep 17 00:00:00 2001 From: Jeeiii Date: Thu, 15 Feb 2024 18:35:16 +0100 Subject: [PATCH] feat: add eas integration with minAttestation validator --- libs/credentials/src/index.test.ts | 8 +- libs/credentials/src/providers.ts | 4 +- libs/credentials/src/providers/eas/index.ts | 22 +++ libs/credentials/src/providers/index.ts | 3 +- libs/credentials/src/queryGraph.ts | 18 +++ libs/credentials/src/types/index.ts | 29 +++- libs/credentials/src/validateCredentials.ts | 2 +- libs/credentials/src/validators.ts | 6 +- .../validators/easAttestations/index.test.ts | 139 ++++++++++++++++++ .../src/validators/easAttestations/index.ts | 46 ++++++ libs/credentials/src/validators/index.ts | 4 +- 11 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 libs/credentials/src/providers/eas/index.ts create mode 100644 libs/credentials/src/queryGraph.ts create mode 100644 libs/credentials/src/validators/easAttestations/index.test.ts create mode 100644 libs/credentials/src/validators/easAttestations/index.ts diff --git a/libs/credentials/src/index.test.ts b/libs/credentials/src/index.test.ts index 5362bd7d..ad876b2a 100644 --- a/libs/credentials/src/index.test.ts +++ b/libs/credentials/src/index.test.ts @@ -12,7 +12,7 @@ describe("Credentials library", () => { it("Should add a provider to the list of supported providers", () => { addProvider({} as any) - expect(providers).toHaveLength(4) + expect(providers).toHaveLength(5) }) }) @@ -20,7 +20,7 @@ describe("Credentials library", () => { it("Should add 2 providers to the list of supported providers", () => { addProviders([{} as any, {} as any]) - expect(providers).toHaveLength(6) + expect(providers).toHaveLength(7) }) }) @@ -28,7 +28,7 @@ describe("Credentials library", () => { it("Should add a validator to the list of supported validators", () => { addValidator({} as any) - expect(validators).toHaveLength(8) + expect(validators).toHaveLength(9) }) }) @@ -36,7 +36,7 @@ describe("Credentials library", () => { it("Should add 2 validators to the list of supported validators", () => { addValidators([{} as any, {} as any]) - expect(validators).toHaveLength(10) + expect(validators).toHaveLength(11) }) }) diff --git a/libs/credentials/src/providers.ts b/libs/credentials/src/providers.ts index 4a82d263..272c8719 100644 --- a/libs/credentials/src/providers.ts +++ b/libs/credentials/src/providers.ts @@ -1,6 +1,6 @@ import { Provider } from "./types" -import { github, twitter, blockchain } from "./providers/index" +import { github, twitter, blockchain, eas } from "./providers/index" -const providers: Provider[] = [github, twitter, blockchain] +const providers: Provider[] = [github, twitter, blockchain, eas] export default providers diff --git a/libs/credentials/src/providers/eas/index.ts b/libs/credentials/src/providers/eas/index.ts new file mode 100644 index 00000000..910d6fef --- /dev/null +++ b/libs/credentials/src/providers/eas/index.ts @@ -0,0 +1,22 @@ +import { EASProvider, EASNetworks } from "../.." +import queryGraph from "../../queryGraph" + +// Graph endpoint for ethereum mainnet. +const easDefaultEndpoint = `https://easscan.org/graphql` +const easNetworkEndpoint = (network: EASNetworks) => + `https://${network}.easscan.org/graphql` + +const provider: EASProvider = { + name: "eas", + + async queryGraph(network: EASNetworks, query: string) { + return queryGraph( + network === EASNetworks.ETHEREUM + ? easDefaultEndpoint + : easNetworkEndpoint(network), + query + ) + } +} + +export default provider diff --git a/libs/credentials/src/providers/index.ts b/libs/credentials/src/providers/index.ts index 541a44d7..98cda640 100644 --- a/libs/credentials/src/providers/index.ts +++ b/libs/credentials/src/providers/index.ts @@ -1,5 +1,6 @@ import github from "./github" import twitter from "./twitter" import blockchain from "./blockchain" +import eas from "./eas" -export { github, twitter, blockchain } +export { github, twitter, blockchain, eas } diff --git a/libs/credentials/src/queryGraph.ts b/libs/credentials/src/queryGraph.ts new file mode 100644 index 00000000..70b79b9d --- /dev/null +++ b/libs/credentials/src/queryGraph.ts @@ -0,0 +1,18 @@ +import { request } from "@bandada/utils" + +/** + * It returns a function that can be used to query graphs + * data using GraphQL style queries for the EAS provider supported by Bandada. + * @param endpoint The endpoint of the graph. + * @param query The query to execute to fetch the data. + * @returns The function to query the graph. + */ +export default function queryGraph(endpoint: string, query: string) { + request(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + data: JSON.stringify({ + query + }) + }) +} diff --git a/libs/credentials/src/types/index.ts b/libs/credentials/src/types/index.ts index 55930ae3..b6943e65 100644 --- a/libs/credentials/src/types/index.ts +++ b/libs/credentials/src/types/index.ts @@ -1,5 +1,16 @@ import { BigNumberish } from "ethers" +export enum EASNetworks { + ETHEREUM = "ethereum", + ETHEREUM_SEPOLIA = "sepolia", + ARBITRUM = "arbitrum", + BASE = "base", + BASE_GOERLI = "base-goerli", + LINEA = "linea", + OPTIMISM = "optimism", + OPTIMISM_GOERLI = "optimism-goerli" +} + export type Web2Context = { utils?: { api: (endpoint: string) => Promise @@ -16,9 +27,19 @@ export type BlockchainContext = { blockNumber?: number } -export type Context = Web2Context | BlockchainContext +export type EASContext = { + recipient: BigNumberish + queryGraph: (query: string) => Promise + attester?: BigNumberish + schemaId?: BigNumberish +} + +export type Context = Web2Context | BlockchainContext | EASContext -export type Handler = (criteria: any, context: Context) => Promise +export type Handler = ( + criteria: any, + context: Context +) => Promise | boolean export interface Provider { name: string @@ -42,6 +63,10 @@ export interface BlockchainProvider extends Provider { getJsonRpcProvider: (url: string) => Promise } +export interface EASProvider extends Provider { + queryGraph: (network: EASNetworks, query: string) => Promise +} + export interface Validator { id: string criteriaABI: any diff --git a/libs/credentials/src/validateCredentials.ts b/libs/credentials/src/validateCredentials.ts index bdc75cdc..752b2c2a 100644 --- a/libs/credentials/src/validateCredentials.ts +++ b/libs/credentials/src/validateCredentials.ts @@ -32,7 +32,7 @@ export default async function validateCredentials( }) } - if ("getAddress" in provider) { + if ("getAddress" in provider || "queryGraph" in provider) { return validator.validate(criteria, { ...context }) diff --git a/libs/credentials/src/validators.ts b/libs/credentials/src/validators.ts index c0b45280..8102590a 100644 --- a/libs/credentials/src/validators.ts +++ b/libs/credentials/src/validators.ts @@ -6,7 +6,8 @@ import { twitterFollowers, twitterFollowingUser, blockchainTransactions, - blockchainBalance + blockchainBalance, + easAttestations } from "./validators/index" const validators: Validator[] = [ @@ -16,7 +17,8 @@ const validators: Validator[] = [ twitterFollowers, twitterFollowingUser, blockchainTransactions, - blockchainBalance + blockchainBalance, + easAttestations ] export default validators diff --git a/libs/credentials/src/validators/easAttestations/index.test.ts b/libs/credentials/src/validators/easAttestations/index.test.ts new file mode 100644 index 00000000..8e409df0 --- /dev/null +++ b/libs/credentials/src/validators/easAttestations/index.test.ts @@ -0,0 +1,139 @@ +import { validateCredentials } from "../.." +import easAttestations from "./index" + +describe("EASAttestations", () => { + const queryGraphMocked = { + queryGraph: jest.fn() + } + + it("Should return true if an account has greater than or equal to 3 attestations", async () => { + queryGraphMocked.queryGraph.mockReturnValue([ + { + id: "0x52561c95029d9f2335839ddc96a69ee9737a18e2a781e64659b7bd645ccb8efc", + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8" + }, + { + id: "0xee06a022c7d55f67bac213d6b2cd384a899ef79a57f1f5f148e45c313b4fdebe", + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8" + }, + { + id: "0xfbc0f1aac4379c18fa9a5b6493825234a8ca82a2a296148465d150c2e64c6202", + recipient: "0x0000000000000000000000000000000000000000" + }, + { + id: "0x227510204bcfe7b543388b82c6e02aafe7b0d0a20e4f159794e8121611aa601b", + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8" + } + ]) + + const result = await validateCredentials( + { + id: easAttestations.id, + criteria: { + minAttestations: 3 + } + }, + { + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + queryGraph: queryGraphMocked.queryGraph + } + ) + + expect(result).toBeTruthy() + }) + + it("Should return false if an account has less than 3 attestations", async () => { + queryGraphMocked.queryGraph.mockReturnValue([ + { + id: "0x52561c95029d9f2335839ddc96a69ee9737a18e2a781e64659b7bd645ccb8efc", + recipient: "0x0000000000000000000000000000000000000000" + }, + { + id: "0xee06a022c7d55f67bac213d6b2cd384a899ef79a57f1f5f148e45c313b4fdebe", + recipient: "0x0000000000000000000000000000000000000000" + }, + { + id: "0xfbc0f1aac4379c18fa9a5b6493825234a8ca82a2a296148465d150c2e64c6202", + recipient: "0x0000000000000000000000000000000000000000" + }, + { + id: "0x227510204bcfe7b543388b82c6e02aafe7b0d0a20e4f159794e8121611aa601b", + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8" + } + ]) + + const result = await validateCredentials( + { + id: easAttestations.id, + criteria: { + minAttestations: 3 + } + }, + { + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + queryGraph: queryGraphMocked.queryGraph + } + ) + + expect(result).toBeFalsy() + }) + + it("Should throw an error if a criteria parameter is missing", async () => { + const fun = () => + validateCredentials( + { + id: easAttestations.id, + criteria: {} + }, + { + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae9", + queryGraph: queryGraphMocked.queryGraph + } + ) + + await expect(fun).rejects.toThrow( + "Parameter 'minAttestations' has not been defined" + ) + }) + + it("Should throw an error if a criteria parameter should not exist", async () => { + const fun = () => + validateCredentials( + { + id: easAttestations.id, + criteria: { + minAttestations: 1, + test: 123 + } + }, + { + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae9", + queryGraph: queryGraphMocked.queryGraph + } + ) + + await expect(fun).rejects.toThrow( + "Parameter 'test' should not be part of the criteria" + ) + }) + + it("Should throw a type error if a criteria parameter has the wrong type", async () => { + const fun = () => + validateCredentials( + { + id: easAttestations.id, + criteria: { + minAttestations: "1" + } + }, + { + recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae9", + queryGraph: queryGraphMocked.queryGraph + } + ) + + await expect(fun).rejects.toThrow( + "Parameter 'minAttestations' is not a number" + ) + }) +}) diff --git a/libs/credentials/src/validators/easAttestations/index.ts b/libs/credentials/src/validators/easAttestations/index.ts new file mode 100644 index 00000000..c13c9cb6 --- /dev/null +++ b/libs/credentials/src/validators/easAttestations/index.ts @@ -0,0 +1,46 @@ +import { Context, EASContext, Validator } from "../.." + +export type Criteria = { + minAttestations: number +} + +const validator: Validator = { + id: "EAS_ATTESTATIONS", + + criteriaABI: { + minAttestations: "number" + }, + + /** + * It checks if a user has greater than or equal to 'minAttestations' attestations. + * @param criteria The criteria used to check user's credentials. + * @param context Context variables. + * @returns True if the user meets the criteria. + */ + async validate(criteria: Criteria, context: Context) { + if ("recipient" in context) { + const { recipient } = context as EASContext + + const getAttestations = (context as EASContext).queryGraph + + const attestations = await getAttestations(` + query { + attestations { + id + recipient + } + } + `) + + const recipientAttestations = attestations.filter( + (attestation: any) => attestation.recipient === recipient + ) + + return recipientAttestations.length >= criteria.minAttestations + } + + throw new Error("No recipient value found") + } +} + +export default validator diff --git a/libs/credentials/src/validators/index.ts b/libs/credentials/src/validators/index.ts index 3d12a6de..3531781e 100644 --- a/libs/credentials/src/validators/index.ts +++ b/libs/credentials/src/validators/index.ts @@ -5,6 +5,7 @@ import twitterFollowers from "./twitterFollowers" import twitterFollowingUser from "./twitterFollowingUser" import blockchainTransactions from "./blockchainTransactions" import blockchainBalance from "./blockchainBalance" +import easAttestations from "./easAttestations" export { githubFollowers, @@ -13,5 +14,6 @@ export { twitterFollowingUser, githubPersonalStars, blockchainTransactions, - blockchainBalance + blockchainBalance, + easAttestations }