diff --git a/apps/api/src/app/credentials/credentials.service.ts b/apps/api/src/app/credentials/credentials.service.ts index 268cfd53..ac637e64 100644 --- a/apps/api/src/app/credentials/credentials.service.ts +++ b/apps/api/src/app/credentials/credentials.service.ts @@ -6,7 +6,9 @@ import { Web2Provider, providers, Web2Context, - BlockchainContext + BlockchainContext, + EASContext, + EASNetworks } from "@bandada/credentials" import { blockchainCredentialSupportedNetworks } from "@bandada/utils" import { id } from "@ethersproject/hash" @@ -139,7 +141,7 @@ export class CredentialsService { } = this.oAuthState.get(credentialOAuthState)) const provider = getProvider(providerName) - let context: Web2Context | BlockchainContext + let context: Web2Context | BlockchainContext | EASContext if (address && credentialProvider === "blockchain") { const { network } = credentials.credentials[i].criteria @@ -171,6 +173,14 @@ export class CredentialsService { jsonRpcProvider } + // Check if the same account has already joined the group. + accountHash = id(address + groupId) + } else if (address && credentialProvider === "eas") { + context = { + network: EASNetworks.ETHEREUM_SEPOLIA, + address: address[0] + } + // Check if the same account has already joined the group. accountHash = id(address + groupId) } else { @@ -259,30 +269,44 @@ export class CredentialsService { let accountHash: string - let context: Web2Context | BlockchainContext + let context: Web2Context | BlockchainContext | EASContext if (address) { - const { network } = JSON.parse(group.credentials).criteria + const { network, recipient } = JSON.parse( + group.credentials + ).criteria + + if (network) { + const supportedNetwork = + blockchainCredentialSupportedNetworks.find( + (n) => n.name.toLowerCase() === network.toLowerCase() + ) - const supportedNetwork = blockchainCredentialSupportedNetworks.find( - (n) => n.name.toLowerCase() === network.toLowerCase() - ) + if (supportedNetwork === undefined) + throw new BadRequestException( + `The network is not supported` + ) - if (supportedNetwork === undefined) - throw new BadRequestException(`The network is not supported`) + const networkEnvVariableName = supportedNetwork.id.toUpperCase() - const networkEnvVariableName = supportedNetwork.id.toUpperCase() + const web3providerRpcURL = + process.env[`${networkEnvVariableName}_RPC_URL`] - const web3providerRpcURL = - process.env[`${networkEnvVariableName}_RPC_URL`] + const jsonRpcProvider = await ( + provider as BlockchainProvider + ).getJsonRpcProvider(web3providerRpcURL) - const jsonRpcProvider = await ( - provider as BlockchainProvider - ).getJsonRpcProvider(web3providerRpcURL) + context = { + address: address[0], + jsonRpcProvider + } + } - context = { - address: address[0], - jsonRpcProvider + if (recipient) { + context = { + network: EASNetworks.ETHEREUM_SEPOLIA, + address: address[0] + } } // Check if the same account has already joined the group. diff --git a/apps/dashboard/src/pages/credentials.tsx b/apps/dashboard/src/pages/credentials.tsx index ad79ebfb..c9aa6099 100644 --- a/apps/dashboard/src/pages/credentials.tsx +++ b/apps/dashboard/src/pages/credentials.tsx @@ -3,7 +3,8 @@ import { blockchain, Web2Provider, twitter, - github + github, + eas } from "@bandada/credentials" import { Flex, Text, Button } from "@chakra-ui/react" import { useEffect, useState, useCallback, useContext } from "react" @@ -227,6 +228,57 @@ export default function CredentialsPage() { } } + // If the credential is EAS + if ( + providerName === eas.name && + isLoggedInAdmin() && + state && + admin + ) { + if (groupType) { + const bandadaCredentials = + localStorage.getItem(LOCAL_STORAGE) + if (!bandadaCredentials) return null + const providers = JSON.parse(bandadaCredentials) + providers[providerName] = [ + state, + undefined, + admin.address + ] + localStorage.setItem( + LOCAL_STORAGE, + JSON.stringify(providers) + ) + if (!groupId || !memberId) return + const url = getUrlNextEmpty(groupId, memberId) + + if (url === null) { + const clientRedirectUrl = + await validateCredentials() + if (clientRedirectUrl) { + window.location.replace(clientRedirectUrl) + } else { + setMessage("You have joined the group!") + } + return + } + + window.location.replace(url) + } + + const redirectUrl = await addMemberByCredentials( + [state], + undefined, + [admin.address] + ) + + if (redirectUrl) { + window.location.replace(redirectUrl) + } else { + setMessage("You have joined the group!") + } + } + // If it is a web2 credential. This is the first request for web2 credentials if ( (providerName === twitter.name || diff --git a/libs/credentials/README.md b/libs/credentials/README.md index fe46a4d2..f0ad9ba2 100644 --- a/libs/credentials/README.md +++ b/libs/credentials/README.md @@ -463,6 +463,42 @@ validateCredentials( } } ) + +// Validate blockchain credentials +validateCredentials( + { + id: blockchainBalance.id, + criteria: { + minBalance: "10", + network: "sepolia", + blockNumber: 4749638 + } + }, + { + address: "0x", + jsonRpcProvider + } +) + +// Validate EAS attestations credentials +validateCredentials( + { + id: easAttestations.id, + criteria: { + minAttestations: 1, + recipient: "0x0", + attester: "0x1", + schemaId: "0x2", + revocable: true, + revoked: false, + isOffchain: false + } + }, + { + network: EASNetworks.ETHEREUM, + address: "0x1" + } +) ``` ## Validate many credentials diff --git a/libs/credentials/src/index.test.ts b/libs/credentials/src/index.test.ts index a620bcd4..f0bf03ab 100644 --- a/libs/credentials/src/index.test.ts +++ b/libs/credentials/src/index.test.ts @@ -147,12 +147,16 @@ describe("Credentials library", () => { }) describe("# queryGraph", () => { - it("Should return a function that can be used to query graphs data using GraphQL", () => { - const query = queryGraph( + it("Should return a function that can be used to query graphs data using GraphQL", async () => { + const query = await queryGraph( "https://easscan.org/graphql", ` query { - attestations { + attestations(where: { + recipient: { + equals: "0x" + } + }) { recipient attester revocable @@ -164,7 +168,7 @@ describe("Credentials library", () => { ` ) - expect(query).toBeUndefined() + expect(query).toEqual({}) }) }) diff --git a/libs/credentials/src/queryGraph.ts b/libs/credentials/src/queryGraph.ts index 70b79b9d..4d7aa5e8 100644 --- a/libs/credentials/src/queryGraph.ts +++ b/libs/credentials/src/queryGraph.ts @@ -8,7 +8,7 @@ import { request } from "@bandada/utils" * @returns The function to query the graph. */ export default function queryGraph(endpoint: string, query: string) { - request(endpoint, { + return request(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, data: JSON.stringify({ diff --git a/libs/credentials/src/types/index.ts b/libs/credentials/src/types/index.ts index d138e3ee..45b03327 100644 --- a/libs/credentials/src/types/index.ts +++ b/libs/credentials/src/types/index.ts @@ -27,7 +27,8 @@ export type BlockchainContext = { } export type EASContext = { - queryGraph: (query: string) => Promise + network: EASNetworks + address: BigNumberish } export type Context = Web2Context | BlockchainContext | EASContext diff --git a/libs/credentials/src/validators/easAttestations/index.test.ts b/libs/credentials/src/validators/easAttestations/index.test.ts index 5cf98aa9..695043e3 100644 --- a/libs/credentials/src/validators/easAttestations/index.test.ts +++ b/libs/credentials/src/validators/easAttestations/index.test.ts @@ -1,65 +1,26 @@ -import { validateCredentials } from "../.." +import { EASNetworks, validateCredentials } from "../.." import easAttestations from "./index" -describe("EASAttestations", () => { - const queryGraphMocked = { - queryGraph: jest.fn() - } - - queryGraphMocked.queryGraph.mockReturnValue([ - { - id: "0x52561c95029d9f2335839ddc96a69ee9737a18e2a781e64659b7bd645ccb8efc", - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", - attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", - revocable: true, - revoked: false, - schemaId: - "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", - isOffchain: false - }, - { - id: "0xee06a022c7d55f67bac213d6b2cd384a899ef79a57f1f5f148e45c313b4fdebe", - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", - attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", - revocable: true, - revoked: false, - schemaId: - "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", - isOffchain: false - }, - { - id: "0xfbc0f1aac4379c18fa9a5b6493825234a8ca82a2a296148465d150c2e64c6202", - recipient: "0x0000000000000000000000000000000000000000", - attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", - revocable: true, - revoked: false, - schemaId: - "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", - isOffchain: false - }, - { - id: "0x227510204bcfe7b543388b82c6e02aafe7b0d0a20e4f159794e8121611aa601b", - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", - attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", - revocable: true, - revoked: false, - schemaId: - "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", - isOffchain: false - } - ]) +jest.mock("../..", () => ({ + EASNetworks: { ETHEREUM_SEPOLIA: "sepolia" }, + validateCredentials: jest.fn(() => true) +})) +describe("EASAttestations", () => { it("Should return true if an account has greater than or equal to 3 attestations", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => true) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 3, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8" + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d" } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -67,12 +28,14 @@ describe("EASAttestations", () => { }) it("Should return true if the given optional criterias are satisfied", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => true) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 1, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d", attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", schemaId: "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", @@ -82,7 +45,8 @@ describe("EASAttestations", () => { } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -90,12 +54,14 @@ describe("EASAttestations", () => { }) it("Should return false if the attester optional criteria doesn't match", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => false) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 1, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d", attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d4", schemaId: "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", @@ -105,7 +71,8 @@ describe("EASAttestations", () => { } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d4" } ) @@ -113,12 +80,14 @@ describe("EASAttestations", () => { }) it("Should return false if the schemaId optional criteria doesn't match", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => false) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 1, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d", attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", schemaId: "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5d", @@ -128,7 +97,8 @@ describe("EASAttestations", () => { } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -136,12 +106,14 @@ describe("EASAttestations", () => { }) it("Should return false if the revocable optional criteria doesn't match", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => false) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 1, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d", attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", schemaId: "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", @@ -151,7 +123,8 @@ describe("EASAttestations", () => { } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -159,12 +132,14 @@ describe("EASAttestations", () => { }) it("Should return false if the revoked optional criteria doesn't match", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => false) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 1, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d", attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", schemaId: "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", @@ -174,7 +149,8 @@ describe("EASAttestations", () => { } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -182,12 +158,14 @@ describe("EASAttestations", () => { }) it("Should return false if the isOffchain optional criteria doesn't match", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => false) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 1, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae8", + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d", attester: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3", schemaId: "0xe2636f31239f7948afdd9a9c477048b7fc2a089c347af60e3aa1251e5bf63e5c", @@ -197,7 +175,8 @@ describe("EASAttestations", () => { } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -205,16 +184,19 @@ describe("EASAttestations", () => { }) it("Should return false if an account has less than 3 attestations", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => false) + const result = await validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 3, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae9" + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d" } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d4" } ) @@ -222,6 +204,10 @@ describe("EASAttestations", () => { }) it("Should throw an error if a mandatory criteria parameter is missing", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => { + throw new Error("Parameter 'minAttestations' has not been defined") + }) + const fun = () => validateCredentials( { @@ -229,7 +215,8 @@ describe("EASAttestations", () => { criteria: {} }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -239,18 +226,25 @@ describe("EASAttestations", () => { }) it("Should throw an error if a criteria parameter should not exist", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => { + throw new Error( + "Parameter 'test' should not be part of the criteria" + ) + }) + const fun = () => validateCredentials( { id: easAttestations.id, criteria: { minAttestations: 1, - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae9", + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d", test: 123 } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) @@ -260,17 +254,22 @@ describe("EASAttestations", () => { }) it("Should throw a type error if a criteria parameter has the wrong type", async () => { + ;(validateCredentials as any).mockImplementationOnce(async () => { + throw new Error("Parameter 'minAttestations' is not a number") + }) + const fun = () => validateCredentials( { id: easAttestations.id, criteria: { minAttestations: "1", - recipient: "0x9aB3971e1b065701C72C5f3cAFbF33118dC51ae9" + recipient: "0xBB00B71E34Df590847060A4c597821Bad585ED6d" } }, { - queryGraph: queryGraphMocked.queryGraph + network: EASNetworks.ETHEREUM_SEPOLIA, + address: "0x63A35A52c0ac206108EBbf559E4C7109dAd281d3" } ) diff --git a/libs/credentials/src/validators/easAttestations/index.ts b/libs/credentials/src/validators/easAttestations/index.ts index f428bb33..b52dbbe0 100644 --- a/libs/credentials/src/validators/easAttestations/index.ts +++ b/libs/credentials/src/validators/easAttestations/index.ts @@ -1,4 +1,5 @@ -import { Context, EASContext, Validator } from "../.." +import { Context, Validator } from "../.." +import provider from "../../providers/eas" export type Criteria = { minAttestations: number @@ -51,9 +52,7 @@ const validator: Validator = { * @returns True if the user meets the criteria. */ async validate(criteria: Criteria, context: Context) { - if ("queryGraph" in context) { - const getAttestations = (context as EASContext).queryGraph - + if ("network" in context) { const { recipient, attester, @@ -63,21 +62,31 @@ const validator: Validator = { isOffchain } = criteria - const attestations = await getAttestations(` - query { - attestations { - recipient - attester - revocable - revoked - schemaId - isOffchain + const query = `query { + attestations(where: { + recipient: { + equals: "${criteria.recipient}" + }, + attester: { + equals: "${context.address}" } + }) { + recipient + attester + revocable + revoked + schemaId + isOffchain } - `) + }` + + const getAttestations = await provider.queryGraph( + context.network, + query + ) - const filteredAttestations = attestations.filter( - (attestation: any) => { + const filteredAttestations = + getAttestations.data.attestations.filter((attestation: any) => { // Criteria checks. if (attestation.recipient !== recipient) return false @@ -108,8 +117,7 @@ const validator: Validator = { return false return true - } - ) + }) return filteredAttestations.length >= criteria.minAttestations }