Skip to content
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

feat: add hasVerifiedAttestations and getVerifiedAttestations #95

Closed
wants to merge 5 commits into from
Closed
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,34 @@ type UseAvatar = {
};
```

### hasVerifiedAttestations

alvaroraminelli marked this conversation as resolved.
Show resolved Hide resolved
Checks if the specified address has verified attestations for the given chain and expected schemas.

```tsx
import { base } from 'viem/chains';
import { hasVerifiedAttestations, attestationSchemas } from '@coinbase/onchainkit';

const hasVerifiedAttestation = await hasVerifiedAttestations(
alvaroraminelli marked this conversation as resolved.
Show resolved Hide resolved
'0x1234567890abcdef1234567890abcdef12345678',
base,
[attestationSchemas[base.id].VERIFIED_ACCOUNT],
);
```

### getVerifiedAttestations

Retrieves attestations for a given address and chain, optionally filtered by schemas.

```tsx
import { getVerifiedAttestations } from '@coinbase/onchainkit';

const attestations = await getVerifiedAttestation(
'0x1234567890abcdef1234567890abcdef12345678',
base,
);
```

<br />
<br />

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"release:version": "changeset version && yarn install --immutable"
},
"peerDependencies": {
"graphql": "^16",
"graphql-request": "^6",
"react": "^18",
"react-dom": "^18",
"viem": "^2.7.0"
Expand All @@ -30,6 +32,7 @@
"@types/jest": "^29.5.11",
"@types/react": "^18",
"@types/react-dom": "^18",
"graphql-request": "^6.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-extended": "^4.0.2",
Expand Down
79 changes: 79 additions & 0 deletions src/core/getAttestation.ts
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.
Copy link

Choose a reason for hiding this comment

The 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. getCoinbaseVerifications

Same with below, should we name it hasCoinbaseVerifications ?

Copy link

@cbfyi cbfyi Feb 7, 2024

Choose a reason for hiding this comment

The 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!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the path to support all attestations in general?

Copy link

Choose a reason for hiding this comment

The 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 getAttestation which really only targets CB Verifications would preclude us from that option.

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[] },
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess GraphQL filters maybe

      where: { AND: [conditions] },
      orderBy: [{ timeCreated: 'desc' }],
      distinct: ['schemaId', 'attester'],
      take: 10,

mmmm

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah perhaps we can drive this with use-cases:

  1. Gating: I want to know if someone has X, Y, Z attestation
  2. Display / Visual: I want to get all latest attestations to display, by attester and/or schema
  3. Additional Logic: I want to get a specific attestation to check its payload / contents

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;
}
}
33 changes: 33 additions & 0 deletions src/core/hasVerifiedAttestations.ts
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[] = [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expectedSchemas sounds odd. What exactly are we passing here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see we have those 'VERIFIED ACCOUNT' | 'VERIFIED COUNTRY', not sure I follow.

Step 1, tell me the chain
Step 2, tell me an address, ok but who address? Customer address? unclear.
Step 3, tell me a schema, ok what schema? how do I know where those schemas are?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

address is probably fine, but we need good example in the docs on how this works and how and when should be used.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think address is fine. Otherwise it would be target / recipient / subject, all of which may not convey the desired meaning.

): 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));
}
16 changes: 16 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export { version } from './version';
export { getFrameHtmlResponse } from './core/getFrameHtmlResponse';
export { getFrameMetadata } from './core/getFrameMetadata';
export { getFrameMessage } from './core/getFrameMessage';
export { getAttestation } from './core/getAttestation';
export { hasVerifiedAttestations } from './core/hasVerifiedAttestations';
export { attestationSchemas } from './utils/attestation';
export { FrameMetadata } from './components/FrameMetadata';
export { Avatar } from './components/Avatar';
export { Name } from './components/Name';
Expand Down
81 changes: 81 additions & 0 deletions src/utils/attestation.ts
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 ?? '';
}
35 changes: 34 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ __metadata:
"@types/jest": "npm:^29.5.11"
"@types/react": "npm:^18"
"@types/react-dom": "npm:^18"
graphql-request: "npm:^6.1.0"
jest: "npm:^29.7.0"
jest-environment-jsdom: "npm:^29.7.0"
jest-extended: "npm:^4.0.2"
Expand All @@ -718,12 +719,23 @@ __metadata:
viem: "npm:^2.7.0"
yarn: "npm:^1.22.21"
peerDependencies:
graphql: ^16
graphql-request: ^6
react: ^18
react-dom: ^18
viem: ^2.7.0
languageName: unknown
linkType: soft

"@graphql-typed-document-node/core@npm:^3.2.0":
version: 3.2.0
resolution: "@graphql-typed-document-node/core@npm:3.2.0"
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
checksum: 94e9d75c1f178bbae8d874f5a9361708a3350c8def7eaeb6920f2c820e82403b7d4f55b3735856d68e145e86c85cbfe2adc444fdc25519cd51f108697e99346c
languageName: node
linkType: hard

"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
Expand Down Expand Up @@ -2135,6 +2147,15 @@ __metadata:
languageName: node
linkType: hard

"cross-fetch@npm:^3.1.5":
version: 3.1.8
resolution: "cross-fetch@npm:3.1.8"
dependencies:
node-fetch: "npm:^2.6.12"
checksum: 4c5e022ffe6abdf380faa6e2373c0c4ed7ef75e105c95c972b6f627c3f083170b6886f19fb488a7fa93971f4f69dcc890f122b0d97f0bf5f41ca1d9a8f58c8af
languageName: node
linkType: hard

"cross-spawn@npm:^5.1.0":
version: 5.1.0
resolution: "cross-spawn@npm:5.1.0"
Expand Down Expand Up @@ -3075,6 +3096,18 @@ __metadata:
languageName: node
linkType: hard

"graphql-request@npm:^6.1.0":
version: 6.1.0
resolution: "graphql-request@npm:6.1.0"
dependencies:
"@graphql-typed-document-node/core": "npm:^3.2.0"
cross-fetch: "npm:^3.1.5"
peerDependencies:
graphql: 14 - 16
checksum: f8167925a110e8e1de93d56c14245e7e64391dc8dce5002dd01bf24a3059f345d4ca1bb6ce2040e2ec78264211b0704e75da3e63984f0f74d2042f697a4e8cc6
languageName: node
linkType: hard

"hard-rejection@npm:^2.1.0":
version: 2.1.0
resolution: "hard-rejection@npm:2.1.0"
Expand Down Expand Up @@ -4670,7 +4703,7 @@ __metadata:
languageName: node
linkType: hard

"node-fetch@npm:^2.5.0":
"node-fetch@npm:^2.5.0, node-fetch@npm:^2.6.12":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
Expand Down
Loading