Skip to content

Commit

Permalink
1366 indicate onchain stamp status (passportxyz#1497)
Browse files Browse the repository at this point in the history
* refactor(app): use resolver contract to fetch user's attestations

* chore(app): add base goerli network

* feat(iam): expose bitmap json

* feat(app): fetch bitmap info

* feat(app): convert BN representation of providers to binary

* feat(app): build onchain context from provider bitmap
Co-authored-by: lucian [email protected]

* feat(app): display on chain status using passport schema

* refactor(app): move bitmap logic to utils and test implementation

* fix(app): clean up test

* fix(app): remove data mock

* chore(app): add sample env variables

* fix(app): build error

* chore(app): yarn.lock update
  • Loading branch information
schultztimothy authored Jul 21, 2023
1 parent c6fdbd0 commit 6698d3f
Show file tree
Hide file tree
Showing 15 changed files with 1,011 additions and 261 deletions.
9 changes: 6 additions & 3 deletions app/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ NEXT_PUBLIC_PASSPORT_SIGNER_URL=http://localhost:8000/
NEXT_PUBLIC_PASSPORT_IAM_URL=http://localhost:8003/api/
NEXT_PUBLIC_PASSPORT_IAM_ISSUER_DID=did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC
NEXT_PUBLIC_PASSPORT_PROCEDURE_URL=http://localhost:8003/procedure/
NEXT_PUBLIC_PASSPORT_IAM_STATIC_URL=http://localhost:8003/static/

NEXT_PUBLIC_PASSPORT_MAINNET_RPC_URL=YOUR_RPC_URL
NEXT_PUBLIC_PASSPORT_SEPOLIA_RPC_URL=YOUR_RPC_URL
Expand Down Expand Up @@ -55,9 +56,10 @@ NEXT_PUBLIC_ALLO_SCORER_API_KEY=SCORER_API_KEY
NEXT_PUBLIC_SCORER_ENDPOINT=http://localhost:8002/ceramic-cache

NEXT_PUBLIC_GITCOIN_VERIFIER_CONTRACT_ADDRESS=0xc000000000000000000000000000000000000004
NEXT_PUBLIC_GITCOIN_VC_SCHEMA_UUID=0xc000000000000000000000000000000000000004
NEXT_PUBLIC_GITCOIN_RESOLVER_CONTRACT_ADDRESS=0xc0fF118369894100b652b5Bb8dF5A2C3d7b2E343
NEXT_PUBLIC_PASSPORT_BASE_GOERLI_RPC_URL=https://goerli.base.org
NEXT_PUBLIC_EAS_ADDRESS=0xAcfE09Fd03f7812F022FBf636700AdEA18Fd2A7A
NEXT_PUBLIC_FF_CHAIN_SYNC=on
NEXT_PUBLIC_EAS_INDEXER_URL=https://sepolia.easscan.org/graphql
NEXT_PUBLIC_EAS_EXPLORER=https://sepolia.easscan.org

NEXT_PUBLIC_ENABLE_TESTNET=on
Expand All @@ -72,4 +74,5 @@ NEXT_PUBLIC_ENABLE_TESTNET=on
NEXT_PUBLIC_MAINTENANCE_MODE_ON=["2023-06-07T21:00:00.000Z", "2023-06-08T22:15:00.000Z"]

NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=YOUR_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_WEB3_ONBOARD_EXPLORE_URL=http://localhost:3000/
NEXT_PUBLIC_WEB3_ONBOARD_EXPLORE_URL=http://localhost:3000/

97 changes: 97 additions & 0 deletions app/__tests__/utils/onChainStamps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { decodeProviderInformation } from "../../utils/onChainStamps";
import { SchemaEncoder } from "@ethereum-attestation-service/eas-sdk";
import axios from "axios";
import { Attestation } from "@ethereum-attestation-service/eas-sdk";
import { BigNumber } from "@ethersproject/bignumber";
import * as easModule from "@ethereum-attestation-service/eas-sdk";

jest.mock("axios");
jest.mock("@ethereum-attestation-service/eas-sdk", () => ({
SchemaEncoder: jest.fn(),
Attestation: jest.fn(),
}));

describe("decodeProviderInformation", () => {
beforeEach(() => {
jest.restoreAllMocks();
});
it("decodes and sorts provider information with multiple provider maps", async () => {
(SchemaEncoder as unknown as jest.Mock).mockImplementation(() => ({
decodeData: jest.fn().mockReturnValue([
{ name: "providers", value: { value: [BigNumber.from(1), BigNumber.from(2)] } },
{ name: "issuanceDates", value: { value: ["issuanceDate1", "issuanceDate2"] } },
{ name: "expirationDates", value: { value: ["expirationDate1", "expirationDate2"] } },
{ name: "hashes", value: { value: ["hash1", "hash2"] } },
]),
}));
process.env.NEXT_PUBLIC_PASSPORT_IAM_STATIC_URL = "mockStaticUrl";

const mockStampBits = [
{ bit: 0, index: 0, name: "provider1" },
{ bit: 1, index: 1, name: "provider2" },
];
(axios.get as jest.Mock).mockResolvedValueOnce({ data: mockStampBits });

const mockAttestation = {
data: "0x0000000",
} as Attestation;

const result = await decodeProviderInformation(mockAttestation);

expect(axios.get).toHaveBeenCalledWith("mockStaticUrl/providerBitMapInfo.json");
expect(SchemaEncoder).toHaveBeenCalledWith(
"uint256[] providers,bytes32[] hashes,uint64[] issuanceDates,uint64[] expirationDates,uint16 providerMapVersion"
);
expect(result).toEqual({
onChainProviderInfo: [
{ providerName: "provider1", providerNumber: 0 },
{ providerName: "provider2", providerNumber: 257 },
],
hashes: ["hash1", "hash2"],
issuanceDates: ["issuanceDate1", "issuanceDate2"],
expirationDates: ["expirationDate1", "expirationDate2"],
});
});

it("decodes and sorts provider information with a single providermap", async () => {
const providerMap = BigNumber.from(11);
(SchemaEncoder as unknown as jest.Mock).mockImplementation(() => ({
decodeData: jest.fn().mockReturnValue([
{ name: "providers", value: { value: [providerMap] } },
{ name: "issuanceDates", value: { value: ["issuanceDate1", "issuanceDate2", "issuanceDate4"] } },
{ name: "expirationDates", value: { value: ["expirationDate1", "expirationDate2", "expirationDate4"] } },
{ name: "hashes", value: { value: ["hash1", "hash2", "hash3"] } },
]),
}));
process.env.NEXT_PUBLIC_PASSPORT_IAM_STATIC_URL = "mockStaticUrl";

const mockStampBits = [
{ bit: 0, index: 0, name: "provider1" },
{ bit: 1, index: 0, name: "provider2" },
{ bit: 2, index: 0, name: "provider3" },
{ bit: 3, index: 0, name: "provider4" },
];
(axios.get as jest.Mock).mockResolvedValueOnce({ data: mockStampBits });

const mockAttestation = {
data: "0x0000000",
} as Attestation;

const result = await decodeProviderInformation(mockAttestation);

expect(axios.get).toHaveBeenCalledWith("mockStaticUrl/providerBitMapInfo.json");
expect(SchemaEncoder).toHaveBeenCalledWith(
"uint256[] providers,bytes32[] hashes,uint64[] issuanceDates,uint64[] expirationDates,uint16 providerMapVersion"
);
expect(result).toEqual({
onChainProviderInfo: [
{ providerName: "provider1", providerNumber: 0 },
{ providerName: "provider2", providerNumber: 1 },
{ providerName: "provider4", providerNumber: 3 },
],
hashes: ["hash1", "hash2", "hash3"],
issuanceDates: ["issuanceDate1", "issuanceDate2", "issuanceDate4"],
expirationDates: ["expirationDate1", "expirationDate2", "expirationDate4"],
});
});
});
11 changes: 3 additions & 8 deletions app/components/PlatformCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,9 @@ export const PlatformCard = ({
if (!providers.length) return false;

return providers.some((provider: PROVIDER_ID) => {
const providerSpec = getProviderSpec(platform.platform, provider);
const providerObjs = onChainProviders.filter((p) => p.providerHash === providerSpec.hash);

if (providerObjs.length > 0) {
return providerObjs.some((providerObj) => {
const credentialHash = allProvidersState[provider]?.stamp?.credential.credentialSubject.hash;
return providerSpec.hash === providerObj.providerHash && credentialHash === providerObj.credentialHash;
});
const providerObj = onChainProviders.find((p) => p.providerName === provider);
if (providerObj) {
return providerObj.credentialHash === allProvidersState[provider]?.stamp?.credential.credentialSubject.hash;
}

return false;
Expand Down
11 changes: 3 additions & 8 deletions app/components/StampSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,9 @@ export function StampSelector({
// check if provider is on-chain
const isProviderOnChain = (provider: PROVIDER_ID) => {
if (currentPlatform) {
const providerSpec = getProviderSpec(currentPlatform.platform, provider);
const providerObjs = onChainProviders.filter((p) => p.providerHash === providerSpec.hash);

if (providerObjs.length > 0) {
return providerObjs.some((providerObj) => {
const credentialHash = allProvidersState[provider]?.stamp?.credential.credentialSubject.hash;
return providerSpec.hash === providerObj.providerHash && credentialHash === providerObj.credentialHash;
});
const providerObj = onChainProviders.find((p) => p.providerName === provider);
if (providerObj) {
return providerObj.credentialHash === allProvidersState[provider]?.stamp?.credential.credentialSubject.hash;
}
}

Expand Down
95 changes: 43 additions & 52 deletions app/context/onChainContext.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
// --- React Methods
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";

import { graphql_fetch } from "../utils/helpers";
import { ethers } from "ethers";
import { datadogLogs } from "@datadog/browser-logs";
import { datadogRum } from "@datadog/browser-rum";
import { UserContext } from "./userContext";

import { SchemaEncoder, EAS } from "@ethereum-attestation-service/eas-sdk";

import { PROVIDER_ID, StampBit } from "@gitcoin/passport-types";

import { BigNumber } from "@ethersproject/bignumber";

import axios from "axios";
import { decodeProviderInformation, getAttestationData } from "../utils/onChainStamps";

type OnChainProviderType = {
providerHash: string;
providerName: PROVIDER_ID;
credentialHash: string;
expirationDate: Date;
issuanceDate: Date;
};

export interface OnChainContextState {
Expand All @@ -25,64 +35,45 @@ const startingState: OnChainContextState = {
// create our app context
export const OnChainContext = createContext(startingState);

export type DecodedProviderInfo = {
providerName: PROVIDER_ID;
providerNumber: number;
};

export const OnChainContextProvider = ({ children }: { children: any }) => {
const { address } = useContext(UserContext);
const { address, wallet } = useContext(UserContext);
const [onChainProviders, setOnChainProviders] = useState<OnChainProviderType[]>([]);

const fetchOnChainStatus = useCallback(async () => {
try {
if (!process.env.NEXT_PUBLIC_EAS_INDEXER_URL) {
throw new Error("NEXT_PUBLIC_EAS_INDEXER_URL is not defined");
}
if (wallet && address) {
try {
const passportAttestationData = await getAttestationData(wallet, address);

// Get the attestations for the given user
const res = await graphql_fetch(
new URL(process.env.NEXT_PUBLIC_EAS_INDEXER_URL),
`
query GetAttestations($where: AttestationWhereInput) {
attestations(where: $where) {
decodedDataJson
}
}
`,
{
where: {
recipient: { equals: ethers.getAddress(address!) },
attester: { equals: process.env.NEXT_PUBLIC_GITCOIN_ATTESTER_CONTRACT_ADDRESS },
schemaId: { equals: process.env.NEXT_PUBLIC_GITCOIN_VC_SCHEMA_UUID },
},
if (!passportAttestationData) {
return;
}
);

const attestations = res.data.attestations;

let providers: OnChainProviderType[] = [];

// Extract all providers
attestations.forEach((attestation: any) => {
const { decodedDataJson } = attestation;
const decodedData = JSON.parse(decodedDataJson);
const providerData = decodedData.find((data: any) => data.name === "provider");
const hashData = decodedData.find((data: any) => data.name === "hash");

const hexValue = hashData.value.value.slice(2); // Remove the "0x" prefix
const base64EncodedBytes = Buffer.from(hexValue, "hex").toString("base64");

if (providerData) {
providers.push({
providerHash: providerData.value.value,
credentialHash: `v0.0.0:${base64EncodedBytes}`,
});
}
});

// Set the on-chain status
setOnChainProviders(providers);
} catch (e: any) {
datadogLogs.logger.error("Failed to check on-chain status", e);
datadogRum.addError(e);
const { onChainProviderInfo, hashes, issuanceDates, expirationDates } = await decodeProviderInformation(
passportAttestationData
);

const onChainProviders: OnChainProviderType[] = onChainProviderInfo
.sort((a, b) => a.providerNumber - b.providerNumber)
.map((providerInfo, index) => ({
providerName: providerInfo.providerName,
credentialHash: `v0.0.0:${Buffer.from(hashes[index].slice(2), "hex").toString("base64")}`,
expirationDate: new Date(expirationDates[index].toNumber() * 1000),
issuanceDate: new Date(issuanceDates[index].toNumber() * 1000),
}));

// Set the on-chain status
setOnChainProviders(onChainProviders);
} catch (e: any) {
datadogLogs.logger.error("Failed to check on-chain status", e);
datadogRum.addError(e);
}
}
}, [address]);
}, [wallet, address]);

const refreshOnChainProviders = () => {
return fetchOnChainStatus();
Expand Down
Loading

0 comments on commit 6698d3f

Please sign in to comment.