-
Notifications
You must be signed in to change notification settings - Fork 353
feat: add hasVerifiedAttestations and getVerifiedAttestations #95
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
Changes from all commits
eba43e3
ba1a847
d2abdea
0f1794f
aaf4eb5
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 |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import type { Address } from 'viem'; | ||
import type { Chain } from 'viem'; | ||
import { gql, request } from 'graphql-request'; | ||
import { Attestation } from './types'; | ||
import { | ||
getChainSchemasUids, | ||
getChainEASGraphQLAPI, | ||
getAttesterAddresses, | ||
AttestationSchema, | ||
} from '../utils/attestation'; | ||
|
||
const attestationsQuery = gql` | ||
alvaroraminelli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
query AttestationsForUsers( | ||
$where: AttestationWhereInput | ||
$orderBy: [AttestationOrderByWithRelationInput!] | ||
$distinct: [AttestationScalarFieldEnum!] | ||
$take: Int | ||
) { | ||
attestations(where: $where, orderBy: $orderBy, distinct: $distinct, take: $take) { | ||
attester | ||
expirationTime | ||
id | ||
recipient | ||
revocationTime | ||
schemaId | ||
timeCreated | ||
txid | ||
} | ||
alvaroraminelli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
`; | ||
|
||
/** | ||
* Retrieves attestations for a given address and chain, optionally filtered by schemas. | ||
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. This is in the context of Coinbase Verifications. Do we want to be more specific here in that case? e.g. Same with below, should we name it 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. Bringing this up in case we extend Onchain Kit to support all attestations in general -- which IMO, we should! 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. what is the path to support all attestations in general? 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. I don't think we ever settled on a clear path for this. But absent any decisions, I think we should be more opinionated with the naming to leave room for more general EAS support in the near future. Committing to something abstract like cc: @Zizzamia |
||
* | ||
* @param chain - The blockchain of interest. | ||
* @param address - The address for which attestations are being queried. | ||
* @param filters - Optional filters including schemas to further refine the query. | ||
* @returns A promise that resolves to an array of Attestations. | ||
* @throws Will throw an error if the request to the GraphQL API fails. | ||
*/ | ||
export async function getAttestation<TChain extends Chain>( | ||
address: Address, | ||
chain: TChain, | ||
filters?: { schemas?: AttestationSchema[] }, | ||
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. @cbfyi curios, what other kind of filters we imagine to have later on? 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. I guess GraphQL filters maybe
mmmm 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. Yeah perhaps we can drive this with use-cases:
And for both (1), and (2), we would want to filter by only active by default. (3) doesn't even have to hit GraphQL, but if we want the payload / contents to be unpacked, it's more convenient if we do. That should cover 70% of apps leveraging attestations IMO. |
||
): Promise<Attestation[]> { | ||
try { | ||
const easScanGraphQLAPI = getChainEASGraphQLAPI(chain); | ||
const conditions: Record<string, any> = { | ||
attester: { in: getAttesterAddresses(chain) }, | ||
recipient: { equals: address }, | ||
revoked: { equals: false }, // Not revoked | ||
OR: [ | ||
{ expirationTime: { equals: 0 } }, | ||
{ expirationTime: { gt: Math.round(Date.now() / 1000) } }, | ||
], // Not expired | ||
}; | ||
|
||
if (filters?.schemas && filters?.schemas?.length > 0) { | ||
conditions.schemaId = { in: getChainSchemasUids(filters.schemas, chain.id) }; | ||
} | ||
|
||
const variables = { | ||
where: { AND: [conditions] }, | ||
orderBy: [{ timeCreated: 'desc' }], | ||
distinct: ['schemaId', 'attester'], | ||
take: 10, | ||
}; | ||
|
||
const data: { attestations: Attestation[] } = await request( | ||
easScanGraphQLAPI, | ||
attestationsQuery, | ||
variables, | ||
); | ||
return data?.attestations || []; | ||
} catch (error) { | ||
console.error(`Error in getAttestation: ${(error as Error).message}`); | ||
throw error; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { Address } from 'viem'; | ||
import type { Chain } from 'viem'; | ||
import { getAttestation } from './getAttestation'; | ||
import { isChainSupported, getChainSchemasUids, AttestationSchema } from '../utils/attestation'; | ||
|
||
/** | ||
* Checks if the specified address has verified attestations for the given chain and expected schemas. | ||
* | ||
* @param chain - The blockchain to check for attestations. | ||
* @param address - The address to check for attestations. | ||
* @param expectedSchemas - An array of attestation schemas that are expected. | ||
* @returns A promise that resolves to a boolean indicating whether the address has the expected attestations. | ||
* @throws Will throw an error if the chain is not supported. | ||
*/ | ||
export async function hasVerifiedAttestations<TChain extends Chain>( | ||
address: Address, | ||
chain: TChain, | ||
expectedSchemas: AttestationSchema[] = [], | ||
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.
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. Oh I see we have those Step 1, tell me the chain 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. do we want to be more opinionated on naming address? what if it is not a customer, it is something else to me. Might name like 'walletAddress'. But I assume that address in onchain app is clear. 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.
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. I think |
||
): Promise<boolean> { | ||
if (!chain || !address || expectedSchemas.length === 0) { | ||
return false; | ||
} | ||
|
||
if (!isChainSupported(chain)) { | ||
throw new Error(`Chain ${chain.id} is not supported`); | ||
} | ||
|
||
const schemaUids = getChainSchemasUids(expectedSchemas, chain.id); | ||
const attestations = await getAttestation(address, chain, { schemas: expectedSchemas }); | ||
const schemasFound = attestations.map((attestation) => attestation.schemaId); | ||
|
||
return schemaUids.every((schemaUid) => schemasFound.includes(schemaUid)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,3 +102,19 @@ export type FrameMetadataType = { | |
* Note: exported as public Type | ||
*/ | ||
export type FrameMetadataResponse = Record<string, string>; | ||
|
||
/** | ||
* Attestation | ||
* | ||
* Note: exported as public Type | ||
*/ | ||
export type Attestation = { | ||
attester: string; | ||
expirationTime: number; | ||
id: string; | ||
recipient: string; | ||
revocationTime: number; | ||
schemaId: string; | ||
timeCreated: number; | ||
txid: string; | ||
}; | ||
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. Can we add a line of comment for each property, so we know what they are used for. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// Import necessary modules and types from 'viem/chains' and '../core/types'. | ||
import { base, optimism } from 'viem/chains'; | ||
import type { Chain } from 'viem'; | ||
|
||
// Define an interface for the structure of each chain's data. | ||
interface ChainData { | ||
easGraphqlAPI: string; | ||
schemaUids: Record<string, `0x${string}`>; | ||
attesterAddresses: `0x${string}`[]; | ||
} | ||
|
||
// More details in https://github.com/coinbase/verifications | ||
const baseChain: ChainData = { | ||
easGraphqlAPI: 'https://base.easscan.org/graphql', | ||
schemaUids: { | ||
VERIFIED_COUNTRY: '0x1801901fabd0e6189356b4fb52bb0ab855276d84f7ec140839fbd1f6801ca065', | ||
VERIFIED_ACCOUNT: '0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9', | ||
}, | ||
attesterAddresses: ['0x357458739F90461b99789350868CD7CF330Dd7EE'], | ||
}; | ||
|
||
// More details in https://docs.optimism.io/chain/identity/schemas | ||
const optimismChain: ChainData = { | ||
easGraphqlAPI: 'https://optimism.easscan.org/graphql', | ||
schemaUids: { | ||
N_A: '0xac4c92fc5c7babed88f78a917cdbcdc1c496a8f4ab2d5b2ec29402736b2cf929', | ||
GITCOIN_PASSPORT_SCORES_V1: | ||
'0x6ab5d34260fca0cfcf0e76e96d439cace6aa7c3c019d7c4580ed52c6845e9c89', | ||
OPTIMISM_GOVERNANCE_SEASON_4_CO_GRANT_PARTICIPANT: | ||
'0x401a80196f3805c57b00482ae2b575a9f270562b6b6de7711af9837f08fa0faf', | ||
}, | ||
attesterAddresses: [ | ||
'0x38e9ef91f1a96aca71e2c5f7abfea45536b995a2', | ||
'0x2a0eb7cae52b68e94ff6ab0bfcf0df8eeeb624be', | ||
'0x2d93c2f74b2c4697f9ea85d0450148aa45d4d5a2', | ||
'0x843829986e895facd330486a61Ebee9E1f1adB1a', | ||
'0x3C7820f2874b665AC7471f84f5cbd6E12871F4cC', | ||
], | ||
}; | ||
|
||
const supportedChains: Record<number, ChainData> = { | ||
[base.id]: baseChain, | ||
[optimism.id]: optimismChain, | ||
}; | ||
|
||
export const attestationSchemas = { | ||
[base.id]: baseChain.schemaUids, | ||
[optimism.id]: optimismChain.schemaUids, | ||
}; | ||
|
||
export type AttestationSchema = keyof typeof baseChain.schemaUids & | ||
keyof typeof optimismChain.schemaUids; | ||
|
||
// Function to check if a chain is supported in the application. | ||
export function isChainSupported(chain: Chain): boolean { | ||
// Check if the chain's ID exists in the supportedChains object. | ||
return chain.id in supportedChains; | ||
} | ||
|
||
// Function to retrieve schema UIDs for a given chain and list of schemas. | ||
export function getChainSchemasUids( | ||
schemas: AttestationSchema[], | ||
clientChainId?: number, | ||
): `0x${string}`[] { | ||
// Return an empty array if the clientChainId is not provided or the chain is not supported. | ||
if (!clientChainId || !supportedChains[clientChainId]) { | ||
return []; | ||
} | ||
// Map each schema to its UID, filtering out any undefined values. | ||
return schemas.map((schema) => supportedChains[clientChainId].schemaUids[schema]).filter(Boolean); | ||
} | ||
|
||
// Function to get the list of attester addresses for a given chain. | ||
export function getAttesterAddresses(chain: Chain): `0x${string}`[] { | ||
return supportedChains[chain.id]?.attesterAddresses ?? []; | ||
} | ||
|
||
// Function to get the EAS GraphQL API endpoint for a given chain. | ||
export function getChainEASGraphQLAPI(chain: Chain): string { | ||
return supportedChains[chain.id]?.easGraphqlAPI ?? ''; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.