Skip to content

Commit

Permalink
1889 coinbase verification (passportxyz#1937)
Browse files Browse the repository at this point in the history
* feat(platforms): revamped coinbase stamp

* chore(platforms): configure display and schema criteris for coinbase stamp

* fix(app): horizontally wrap json to ensure content is always visible

* chore(iam, types, app, platforms): new provider id for updated coinbase verification

* feat(app): UI changes for coinbase stamp

* fix(platforms): fix linter errors

* fix(app,platforms): change test for Coinbase stamp

---------

Co-authored-by: Gerald Iakobinyi-Pich <[email protected]>
  • Loading branch information
tim-schultz and nutrina authored Nov 27, 2023
1 parent 74189b6 commit cacb1f7
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 74 deletions.
4 changes: 2 additions & 2 deletions app/__test-fixtures__/contextTestHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ export const makeTestCeramicContext = (initialState?: Partial<CeramicContextStat
providerSpec: getProviderSpec("Gitcoin", "GitcoinContributorStatistics#numGr14ContributionsGte#1"),
stamp: undefined,
},
Coinbase: {
providerSpec: getProviderSpec("Coinbase", "Coinbase"),
CoinbaseDualVerification: {
providerSpec: getProviderSpec("Coinbase", "CoinbaseDualVerification"),
stamp: undefined,
},
},
Expand Down
4 changes: 3 additions & 1 deletion app/components/JsonOutputModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ export const JsonOutputModal = ({
</ModalHeader>
<ModalCloseButton color="rgb(var(--color-text-1))" />
<ModalBody className="overflow-auto text-color-1">
<pre data-testid="passport-json">{JSON.stringify(jsonOutput, null, "\t")}</pre>
<pre data-testid="passport-json" className="whitespace-pre-wrap">
{JSON.stringify(jsonOutput, null, "\t")}
</pre>
</ModalBody>

<ModalFooter borderTopWidth={2} borderTopColor="rgb(var(--color-foreground-6))">
Expand Down
2 changes: 1 addition & 1 deletion iam/src/static/providerBitMapInfo.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"bit":0,"index":0,"name":"SelfStakingBronze"},{"bit":1,"index":0,"name":"SelfStakingSilver"},{"bit":2,"index":0,"name":"SelfStakingGold"},{"bit":3,"index":0,"name":"CommunityStakingBronze"},{"bit":4,"index":0,"name":"CommunityStakingSilver"},{"bit":5,"index":0,"name":"CommunityStakingGold"},{"bit":6,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#1"},{"bit":7,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#10"},{"bit":8,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#25"},{"bit":9,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#100"},{"bit":10,"index":0,"name":"GitcoinContributorStatistics#totalContributionAmountGte#10"},{"bit":11,"index":0,"name":"GitcoinContributorStatistics#totalContributionAmountGte#100"},{"bit":12,"index":0,"name":"GitcoinContributorStatistics#totalContributionAmountGte#1000"},{"bit":13,"index":0,"name":"GitcoinContributorStatistics#numGr14ContributionsGte#1"},{"bit":14,"index":0,"name":"GitcoinContributorStatistics#numRoundsContributedToGte#1"},{"bit":15,"index":0,"name":"twitterAccountAgeGte#180"},{"bit":16,"index":0,"name":"twitterAccountAgeGte#365"},{"bit":17,"index":0,"name":"twitterAccountAgeGte#730"},{"bit":18,"index":0,"name":"Discord"},{"bit":19,"index":0,"name":"Google"},{"bit":20,"index":0,"name":"githubAccountCreationGte#90"},{"bit":21,"index":0,"name":"githubAccountCreationGte#180"},{"bit":22,"index":0,"name":"githubAccountCreationGte#365"},{"bit":23,"index":0,"name":"githubContributionActivityGte#30"},{"bit":24,"index":0,"name":"githubContributionActivityGte#60"},{"bit":25,"index":0,"name":"githubContributionActivityGte#120"},{"bit":26,"index":0,"name":"Facebook"},{"bit":27,"index":0,"name":"FacebookProfilePicture"},{"bit":28,"index":0,"name":"Linkedin"},{"bit":29,"index":0,"name":"Ens"},{"bit":30,"index":0,"name":"Brightid"},{"bit":31,"index":0,"name":"Poh"},{"bit":32,"index":0,"name":"ethPossessionsGte#1"},{"bit":33,"index":0,"name":"ethPossessionsGte#10"},{"bit":34,"index":0,"name":"ethPossessionsGte#32"},{"bit":35,"index":0,"name":"FirstEthTxnProvider"},{"bit":36,"index":0,"name":"EthGTEOneTxnProvider"},{"bit":37,"index":0,"name":"EthGasProvider"},{"bit":38,"index":0,"name":"SnapshotVotesProvider"},{"bit":39,"index":0,"name":"SnapshotProposalsProvider"},{"bit":40,"index":0,"name":"NFT"},{"bit":41,"index":0,"name":"ZkSync"},{"bit":42,"index":0,"name":"ZkSyncEra"},{"bit":43,"index":0,"name":"Lens"},{"bit":44,"index":0,"name":"GnosisSafe"},{"bit":45,"index":0,"name":"Coinbase"},{"bit":46,"index":0,"name":"GuildMember"},{"bit":47,"index":0,"name":"GuildAdmin"},{"bit":48,"index":0,"name":"GuildPassportMember"},{"bit":49,"index":0,"name":"Hypercerts"},{"bit":50,"index":0,"name":"PHIActivitySilver"},{"bit":51,"index":0,"name":"PHIActivityGold"},{"bit":52,"index":0,"name":"HolonymGovIdProvider"},{"bit":53,"index":0,"name":"IdenaState#Newbie"},{"bit":54,"index":0,"name":"IdenaState#Verified"},{"bit":55,"index":0,"name":"IdenaState#Human"},{"bit":56,"index":0,"name":"IdenaStake#1k"},{"bit":57,"index":0,"name":"IdenaStake#10k"},{"bit":58,"index":0,"name":"IdenaStake#100k"},{"bit":59,"index":0,"name":"IdenaAge#5"},{"bit":60,"index":0,"name":"IdenaAge#10"},{"bit":61,"index":0,"name":"CivicCaptchaPass"},{"bit":62,"index":0,"name":"CivicUniquenessPass"},{"bit":63,"index":0,"name":"CivicLivenessPass"},{"bit":64,"index":0,"name":"CyberProfilePremium"},{"bit":65,"index":0,"name":"CyberProfilePaid"},{"bit":66,"index":0,"name":"CyberProfileOrgMember"},{"bit":67,"index":0,"name":"GrantsStack3Projects"},{"bit":68,"index":0,"name":"GrantsStack5Projects"},{"bit":69,"index":0,"name":"GrantsStack7Projects"},{"bit":70,"index":0,"name":"GrantsStack2Programs"},{"bit":71,"index":0,"name":"GrantsStack4Programs"},{"bit":72,"index":0,"name":"GrantsStack6Programs"},{"bit":73,"index":0,"name":"TrustaLabs"},{"bit":74,"index":0,"name":"BeginnerCommunityStaker"},{"bit":75,"index":0,"name":"ExperiencedCommunityStaker"},{"bit":76,"index":0,"name":"TrustedCitizen"}]
[{"bit":0,"index":0,"name":"SelfStakingBronze"},{"bit":1,"index":0,"name":"SelfStakingSilver"},{"bit":2,"index":0,"name":"SelfStakingGold"},{"bit":3,"index":0,"name":"CommunityStakingBronze"},{"bit":4,"index":0,"name":"CommunityStakingSilver"},{"bit":5,"index":0,"name":"CommunityStakingGold"},{"bit":6,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#1"},{"bit":7,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#10"},{"bit":8,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#25"},{"bit":9,"index":0,"name":"GitcoinContributorStatistics#numGrantsContributeToGte#100"},{"bit":10,"index":0,"name":"GitcoinContributorStatistics#totalContributionAmountGte#10"},{"bit":11,"index":0,"name":"GitcoinContributorStatistics#totalContributionAmountGte#100"},{"bit":12,"index":0,"name":"GitcoinContributorStatistics#totalContributionAmountGte#1000"},{"bit":13,"index":0,"name":"GitcoinContributorStatistics#numGr14ContributionsGte#1"},{"bit":14,"index":0,"name":"GitcoinContributorStatistics#numRoundsContributedToGte#1"},{"bit":15,"index":0,"name":"twitterAccountAgeGte#180"},{"bit":16,"index":0,"name":"twitterAccountAgeGte#365"},{"bit":17,"index":0,"name":"twitterAccountAgeGte#730"},{"bit":18,"index":0,"name":"Discord"},{"bit":19,"index":0,"name":"Google"},{"bit":20,"index":0,"name":"githubAccountCreationGte#90"},{"bit":21,"index":0,"name":"githubAccountCreationGte#180"},{"bit":22,"index":0,"name":"githubAccountCreationGte#365"},{"bit":23,"index":0,"name":"githubContributionActivityGte#30"},{"bit":24,"index":0,"name":"githubContributionActivityGte#60"},{"bit":25,"index":0,"name":"githubContributionActivityGte#120"},{"bit":26,"index":0,"name":"Facebook"},{"bit":27,"index":0,"name":"FacebookProfilePicture"},{"bit":28,"index":0,"name":"Linkedin"},{"bit":29,"index":0,"name":"Ens"},{"bit":30,"index":0,"name":"Brightid"},{"bit":31,"index":0,"name":"Poh"},{"bit":32,"index":0,"name":"ethPossessionsGte#1"},{"bit":33,"index":0,"name":"ethPossessionsGte#10"},{"bit":34,"index":0,"name":"ethPossessionsGte#32"},{"bit":35,"index":0,"name":"FirstEthTxnProvider"},{"bit":36,"index":0,"name":"EthGTEOneTxnProvider"},{"bit":37,"index":0,"name":"EthGasProvider"},{"bit":38,"index":0,"name":"SnapshotVotesProvider"},{"bit":39,"index":0,"name":"SnapshotProposalsProvider"},{"bit":40,"index":0,"name":"NFT"},{"bit":41,"index":0,"name":"ZkSync"},{"bit":42,"index":0,"name":"ZkSyncEra"},{"bit":43,"index":0,"name":"Lens"},{"bit":44,"index":0,"name":"GnosisSafe"},{"bit":45,"index":0,"name":"Coinbase"},{"bit":46,"index":0,"name":"GuildMember"},{"bit":47,"index":0,"name":"GuildAdmin"},{"bit":48,"index":0,"name":"GuildPassportMember"},{"bit":49,"index":0,"name":"Hypercerts"},{"bit":50,"index":0,"name":"PHIActivitySilver"},{"bit":51,"index":0,"name":"PHIActivityGold"},{"bit":52,"index":0,"name":"HolonymGovIdProvider"},{"bit":53,"index":0,"name":"IdenaState#Newbie"},{"bit":54,"index":0,"name":"IdenaState#Verified"},{"bit":55,"index":0,"name":"IdenaState#Human"},{"bit":56,"index":0,"name":"IdenaStake#1k"},{"bit":57,"index":0,"name":"IdenaStake#10k"},{"bit":58,"index":0,"name":"IdenaStake#100k"},{"bit":59,"index":0,"name":"IdenaAge#5"},{"bit":60,"index":0,"name":"IdenaAge#10"},{"bit":61,"index":0,"name":"CivicCaptchaPass"},{"bit":62,"index":0,"name":"CivicUniquenessPass"},{"bit":63,"index":0,"name":"CivicLivenessPass"},{"bit":64,"index":0,"name":"CyberProfilePremium"},{"bit":65,"index":0,"name":"CyberProfilePaid"},{"bit":66,"index":0,"name":"CyberProfileOrgMember"},{"bit":67,"index":0,"name":"GrantsStack3Projects"},{"bit":68,"index":0,"name":"GrantsStack5Projects"},{"bit":69,"index":0,"name":"GrantsStack7Projects"},{"bit":70,"index":0,"name":"GrantsStack2Programs"},{"bit":71,"index":0,"name":"GrantsStack4Programs"},{"bit":72,"index":0,"name":"GrantsStack6Programs"},{"bit":73,"index":0,"name":"TrustaLabs"},{"bit":74,"index":0,"name":"BeginnerCommunityStaker"},{"bit":75,"index":0,"name":"ExperiencedCommunityStaker"},{"bit":76,"index":0,"name":"TrustedCitizen"},{"bit":77,"index":0,"name":"CoinbaseDualVerification"}]
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { PlatformOptions } from "../types";
import { Platform } from "../utils/platform";
import React from "react";

const Link = ({ href, children }: { href: string; children: React.ReactNode }) => (
<a href={href} target="_blank" className="text-color-1 cursor-pointer underline" rel="noreferrer">
{children}
</a>
);

export class CoinbasePlatform extends Platform {
platformId = "Coinbase";
path = "coinbase";
Expand Down
14 changes: 11 additions & 3 deletions platforms/src/Coinbase/Providers-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ export const PlatformDetails: PlatformSpec = {
icon: "./assets/coinbaseStampIcon.svg",
platform: "Coinbase",
name: "Coinbase",
description: "Connect your existing account to verify with Coinbase.",
description: "Confirm Your Coinbase Account & Onchain Identity",
connectMessage: "Connect Account",
website: "https://www.coinbase.com/onchain-verify",
};

export const ProviderConfig: PlatformGroupSpec[] = [
{
platformGroup: "Account Name",
providers: [{ title: "Encrypted", name: "Coinbase" }],
platformGroup: "Account & Onchain Identity",
providers: [
{
title: "Privacy-First Verification",
description:
"Your privacy is paramount. We only retain a unique hash to acknowledge your account's verification.",
name: "CoinbaseDualVerification",
},
],
},
];

Expand Down
109 changes: 84 additions & 25 deletions platforms/src/Coinbase/Providers/coinbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ export type CoinbaseUserData = {
};

export type CoinbaseFindMyUserResponse = {
data?: CoinbaseUserData;
data?: {
data: CoinbaseUserData;
};
status: number;
};

export class CoinbaseProvider implements Provider {
// Give the provider a type so that we can select it with a payload
type = "Coinbase";
type = "CoinbaseDualVerification";

// Options can be set here and/or via the constructor
_options = {};
Expand All @@ -31,28 +34,19 @@ export class CoinbaseProvider implements Provider {
// verify that the proof object contains valid === "true"
async verify(payload: RequestPayload): Promise<VerifiedPayload> {
try {
const errors = [];
let valid = false,
verifiedPayload: CoinbaseFindMyUserResponse = {},
record = undefined;
const coinbaseAccountId = await verifyCoinbaseLogin(payload.proofs.code);

verifiedPayload = await verifyCoinbase(payload.proofs.code);
const verifiedCoinbaseAttestation = await verifyCoinbaseAttestation(payload.address);

valid = verifiedPayload && verifiedPayload.data && verifiedPayload.data.id ? true : false;

if (valid) {
record = {
id: verifiedPayload.data.id,
if (verifiedCoinbaseAttestation) {
return {
valid: true,
errors: [],
record: { id: coinbaseAccountId },
};
} else {
errors.push(`We could not verify your Coinbase account: ${verifiedPayload.data.id}.`);
throw `We could not find a Coinbase-verified onchain attestation for your account: ${coinbaseAccountId}.`;
}

return {
valid,
errors,
record,
};
} catch (e: unknown) {
return {
valid: false,
Expand Down Expand Up @@ -86,19 +80,19 @@ export const requestAccessToken = async (code: string): Promise<string> => {
return tokenResponse.access_token;
};

export const verifyCoinbase = async (code: string): Promise<CoinbaseFindMyUserResponse> => {
let userRequest;
export const verifyCoinbaseLogin = async (code: string): Promise<string> => {
let userResponse: CoinbaseFindMyUserResponse;
try {
// retrieve user's auth bearer token to authenticate client
const accessToken = await requestAccessToken(code);

// Now that we have an access token fetch the user details
userRequest = await axios.get("https://api.coinbase.com/v2/user", {
userResponse = await axios.get("https://api.coinbase.com/v2/user", {
headers: { Authorization: `Bearer ${accessToken}` },
});

if (userRequest.status != 200) {
throw `Get user request returned status code ${userRequest.status} instead of the expected 200`;
if (userResponse.status != 200) {
throw `Get user request returned status code ${userResponse.status} instead of the expected 200`;
}
} catch (e) {
const error = e as {
Expand All @@ -112,5 +106,70 @@ export const verifyCoinbase = async (code: string): Promise<CoinbaseFindMyUserRe
};
handleProviderAxiosError(error, "Coinbase access token request error", [code]);
}
return userRequest.data as CoinbaseFindMyUserResponse;

const userData = userResponse.data;

if (!userData.data || !userData.data.id) {
throw "Coinbase user id was not found.";
}
return userData.data.id;
};

const COINBASE_ATTESTER = "0x357458739F90461b99789350868CD7CF330Dd7EE";
export const BASE_EAS_SCAN_URL = "https://base.easscan.org/graphql";
export const VERIFIED_ACCOUNT_SCHEMA = "0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9";

export type Attestation = {
recipient: string;
revocationTime: number;
revoked: boolean;
expirationTime: number;
schema: {
id: string;
};
};

export type EASQueryResponse = {
data?: {
data?: {
attestations: Attestation[];
};
};
};

export const verifyCoinbaseAttestation = async (address: string): Promise<boolean> => {
const query = `
query {
attestations (where: {
attester: { equals: "${COINBASE_ATTESTER}" },
recipient: { equals: "${address}" }
}) {
recipient
revocationTime
revoked
expirationTime
schema {
id
}
}
}
`;

const result: EASQueryResponse = await axios.post(BASE_EAS_SCAN_URL, {
query,
});

if (!result.data.data.attestations) {
throw "No attestations found for this address.";
}

return (
result.data.data.attestations.filter(
(attestation) =>
attestation.revoked === false &&
attestation.revocationTime === 0 &&
attestation.expirationTime === 0 &&
attestation.schema.id === VERIFIED_ACCOUNT_SCHEMA
).length > 0
);
};
Loading

0 comments on commit cacb1f7

Please sign in to comment.