diff --git a/.github/workflows/cd-iam-review.yml b/.github/workflows/cd-iam-review.yml
index e34d2fd810..105d10f106 100644
--- a/.github/workflows/cd-iam-review.yml
+++ b/.github/workflows/cd-iam-review.yml
@@ -21,8 +21,6 @@ jobs:
- name: Lerna Bootstrap
run: lerna bootstrap
- name: Run Tests
- env:
- RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
run: |
yarn test:iam
yarn test:identity
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9e6f402ba9..df80a90875 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,5 @@ jobs:
run: lerna bootstrap
- name: Run Tests
run: yarn test
- env:
- RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
- name: Run Linter
run: yarn lint
diff --git a/.github/workflows/manual-iam-staging.yml b/.github/workflows/manual-iam-staging.yml
index 631930f372..943aa09fba 100644
--- a/.github/workflows/manual-iam-staging.yml
+++ b/.github/workflows/manual-iam-staging.yml
@@ -25,8 +25,6 @@ jobs:
- name: Lerna Bootstrap
run: lerna bootstrap
- name: Run Tests
- env:
- RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
run: |
yarn test:iam
yarn test:identity
diff --git a/app/__test-fixtures__/databaseStorageFixtures.ts b/app/__test-fixtures__/databaseStorageFixtures.ts
index 8f72541b90..841946614e 100644
--- a/app/__test-fixtures__/databaseStorageFixtures.ts
+++ b/app/__test-fixtures__/databaseStorageFixtures.ts
@@ -39,6 +39,11 @@ export const ensStampFixture: Stamp = {
credential,
};
+export const pohStampFixture: Stamp = {
+ provider: "Poh",
+ credential,
+};
+
export const twitterStampFixture: Stamp = {
provider: "Twitter",
credential,
diff --git a/app/__test-fixtures__/verifiableCredentialResults.ts b/app/__test-fixtures__/verifiableCredentialResults.ts
index 75ee776e08..9d85035e75 100644
--- a/app/__test-fixtures__/verifiableCredentialResults.ts
+++ b/app/__test-fixtures__/verifiableCredentialResults.ts
@@ -36,3 +36,16 @@ export const SUCCESFUL_ENS_RESULT: VerifiableCredentialRecord = {
challenge: credential,
credential: credential,
};
+
+export const SUCCESFUL_POH_RESULT: VerifiableCredentialRecord = {
+ record: {
+ type: "Poh",
+ address: "0xcF323CE817E25b4F784bC1e14c9A99A525fDC50f",
+ version: "0.0.0",
+ poh: "Is registered",
+ },
+ signature:
+ "0xbdbac10fdb0921e73df7575e47cbda484be550c36570bc146bed90c5dcb7435e64178cb263864f48af1ad6eeee1ee94c9a0794a3812ae861f8898a973233abea1c",
+ challenge: credential,
+ credential: credential,
+};
diff --git a/app/__tests__/components/ProviderCards/PohCard.test.tsx b/app/__tests__/components/ProviderCards/PohCard.test.tsx
new file mode 100644
index 0000000000..a833262a43
--- /dev/null
+++ b/app/__tests__/components/ProviderCards/PohCard.test.tsx
@@ -0,0 +1,199 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor, waitForElementToBeRemoved } from "@testing-library/react";
+import { PohCard } from "../../../components/ProviderCards";
+
+import { UserContext, UserContextState } from "../../../context/userContext";
+import { mockAddress, mockWallet } from "../../../__test-fixtures__/onboardHookValues";
+import { STAMP_PROVIDERS } from "../../../config/providers";
+import { pohStampFixture } from "../../../__test-fixtures__/databaseStorageFixtures";
+import { SUCCESFUL_POH_RESULT } from "../../../__test-fixtures__/verifiableCredentialResults";
+import { fetchVerifiableCredential } from "@dpopp/identity/dist/commonjs";
+
+jest.mock("@dpopp/identity/dist/commonjs", () => ({
+ fetchVerifiableCredential: jest.fn(),
+}));
+jest.mock("../../../utils/onboard.ts");
+
+const mockHandleConnection = jest.fn();
+const mockCreatePassport = jest.fn();
+const handleAddStamp = jest.fn();
+const mockUserContext: UserContextState = {
+ loggedIn: true,
+ passport: undefined,
+ isLoadingPassport: false,
+ allProvidersState: {
+ Poh: {
+ providerSpec: STAMP_PROVIDERS.Poh,
+ stamp: undefined,
+ },
+ },
+ handleAddStamp: handleAddStamp,
+ handleCreatePassport: mockCreatePassport,
+ handleConnection: mockHandleConnection,
+ address: mockAddress,
+ wallet: mockWallet,
+ signer: undefined,
+ walletLabel: mockWallet.label,
+};
+
+describe("when user has not verfied with PohProvider", () => {
+ it("should display a verification button", () => {
+ render(
+
+
+
+ );
+
+ const verifyButton = screen.queryByRole("button", {
+ name: /Verify/,
+ });
+
+ expect(verifyButton).toBeInTheDocument();
+ });
+});
+
+describe("when user has verified with PohProvider", () => {
+ it("should display is verified", () => {
+ render(
+
+
+
+ );
+
+ const verified = screen.queryByText(/Verified/);
+
+ expect(verified).toBeInTheDocument();
+ });
+});
+
+describe("when the verify button is clicked", () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("and when a successful POH result is returned", () => {
+ beforeEach(() => {
+ (fetchVerifiableCredential as jest.Mock).mockResolvedValue(SUCCESFUL_POH_RESULT);
+ });
+
+ it("the modal displays the verify button", async () => {
+ render(
+
+
+
+ );
+
+ const initialVerifyButton = screen.queryByRole("button", {
+ name: /Verify/,
+ });
+
+ fireEvent.click(initialVerifyButton!);
+
+ const verifyModal = await screen.findByRole("dialog");
+ const verifyModalButton = screen.getByTestId("modal-verify");
+
+ expect(verifyModal).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(verifyModalButton).toBeInTheDocument();
+ });
+ });
+
+ it("clicking verify adds the stamp", async () => {
+ render(
+
+
+
+ );
+
+ const initialVerifyButton = screen.queryByRole("button", {
+ name: /Verify/,
+ });
+
+ // Click verify button on ens card
+ fireEvent.click(initialVerifyButton!);
+
+ // Wait to see the verify button on the modal
+ await waitFor(() => {
+ const verifyModalButton = screen.getByTestId("modal-verify");
+ expect(verifyModalButton).toBeInTheDocument();
+ });
+
+ const finalVerifyButton = screen.queryByRole("button", {
+ name: /Verify/,
+ });
+
+ // Click the verify button on modal
+ fireEvent.click(finalVerifyButton!);
+
+ expect(handleAddStamp).toBeCalled();
+ });
+
+ it("clicking cancel closes the modal and a stamp should not be added", async () => {
+ (fetchVerifiableCredential as jest.Mock).mockResolvedValue(SUCCESFUL_POH_RESULT);
+ render(
+
+
+
+ );
+
+ const initialVerifyButton = screen.queryByRole("button", {
+ name: /Verify/,
+ });
+
+ fireEvent.click(initialVerifyButton!);
+
+ // Wait to see the cancel button on the modal
+ let modalCancelButton: HTMLElement | null = null;
+ await waitFor(() => {
+ modalCancelButton = screen.queryByRole("button", {
+ name: /Cancel/,
+ });
+ expect(modalCancelButton).toBeInTheDocument();
+ });
+
+ fireEvent.click(modalCancelButton!);
+
+ expect(handleAddStamp).not.toBeCalled();
+
+ await waitForElementToBeRemoved(modalCancelButton);
+ expect(modalCancelButton).not.toBeInTheDocument();
+ });
+ });
+
+ describe("and when a failed POH result is returned", () => {
+ it("modal displays a failed message", async () => {
+ (fetchVerifiableCredential as jest.Mock).mockRejectedValue("ERROR");
+ render(
+
+
+
+ );
+
+ const initialVerifyButton = screen.queryByRole("button", {
+ name: /Verify/,
+ });
+
+ fireEvent.click(initialVerifyButton!);
+
+ const verifyModal = await screen.findByRole("dialog");
+ const verifyModalText = screen.getByText("The Proof of Humanity Status for this address Is not Registered");
+
+ expect(verifyModal).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(verifyModalText).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/app/__tests__/pages/Dashboard.test.tsx b/app/__tests__/pages/Dashboard.test.tsx
index ddd234df55..d4ab510bb1 100644
--- a/app/__tests__/pages/Dashboard.test.tsx
+++ b/app/__tests__/pages/Dashboard.test.tsx
@@ -29,6 +29,10 @@ const mockUserContext: UserContextState = {
providerSpec: STAMP_PROVIDERS.Ens,
stamp: undefined,
},
+ Poh: {
+ providerSpec: STAMP_PROVIDERS.Poh,
+ stamp: undefined,
+ },
Twitter: {
providerSpec: STAMP_PROVIDERS.Twitter,
stamp: undefined,
diff --git a/app/components/CardList.tsx b/app/components/CardList.tsx
index 9fb411082f..40f3d38f00 100644
--- a/app/components/CardList.tsx
+++ b/app/components/CardList.tsx
@@ -2,7 +2,7 @@
import React from "react";
// --- Identity Providers
-import { GoogleCard, SimpleCard, EnsCard, TwitterCard } from "./ProviderCards";
+import { GoogleCard, SimpleCard, EnsCard, PohCard, TwitterCard } from "./ProviderCards";
export const CardList = (): JSX.Element => {
return (
@@ -13,6 +13,7 @@ export const CardList = (): JSX.Element => {
+
diff --git a/app/components/ProviderCards/PohCard.tsx b/app/components/ProviderCards/PohCard.tsx
new file mode 100644
index 0000000000..e5d7ddcf06
--- /dev/null
+++ b/app/components/ProviderCards/PohCard.tsx
@@ -0,0 +1,93 @@
+// --- React Methods
+import React, { useContext, useState } from "react";
+
+// --- Identity tools
+import { fetchVerifiableCredential } from "@dpopp/identity/dist/commonjs";
+
+// pull context
+import { UserContext } from "../../context/userContext";
+
+import { PROVIDER_ID, Stamp } from "@dpopp/types";
+
+const iamUrl = process.env.NEXT_PUBLIC_DPOPP_IAM_URL || "";
+
+// --- import components
+import { Card } from "../Card";
+import { VerifyModal } from "../VerifyModal";
+import { useDisclosure } from "@chakra-ui/react";
+
+const providerId: PROVIDER_ID = "Poh";
+
+export default function PohCard(): JSX.Element {
+ const { address, signer, handleAddStamp, allProvidersState } = useContext(UserContext);
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [credentialResponse, SetCredentialResponse] = useState(undefined);
+ const [credentialResponseIsLoading, setCredentialResponseIsLoading] = useState(false);
+ const [pohVerified, SetPohVerified] = useState(undefined);
+
+ const handleFetchCredential = (): void => {
+ setCredentialResponseIsLoading(true);
+ fetchVerifiableCredential(
+ iamUrl,
+ {
+ type: "Poh",
+ version: "0.0.0",
+ address: address || "",
+ proofs: {
+ valid: address ? "true" : "false",
+ },
+ },
+ signer as { signMessage: (message: string) => Promise }
+ )
+ .then((verified: { record: any; credential: any }): void => {
+ SetPohVerified(verified.record?.poh);
+ SetCredentialResponse({
+ provider: "Poh",
+ credential: verified.credential,
+ });
+ })
+ .catch((e: any): void => {})
+ .finally((): void => {
+ setCredentialResponseIsLoading(false);
+ });
+ };
+
+ const handleUserVerify = (): void => {
+ if (credentialResponse) {
+ handleAddStamp(credentialResponse);
+ }
+ onClose();
+ };
+
+ const issueCredentialWidget = (
+ <>
+
+ {`The Proof of Humanity Status for this address ${pohVerified || "Is not Registered"}`}>}
+ isLoading={credentialResponseIsLoading}
+ />
+ >
+ );
+
+ return (
+
+ );
+}
diff --git a/app/components/ProviderCards/index.tsx b/app/components/ProviderCards/index.tsx
index fbda710dbd..d44701625d 100644
--- a/app/components/ProviderCards/index.tsx
+++ b/app/components/ProviderCards/index.tsx
@@ -1,4 +1,5 @@
export { default as GoogleCard } from "./GoogleCard";
export { default as SimpleCard } from "./SimpleCard";
export { default as EnsCard } from "./EnsCard";
+export { default as PohCard } from "./PohCard";
export { default as TwitterCard } from "./TwitterCard";
diff --git a/app/config/providers.ts b/app/config/providers.ts
index 4749e0e834..7fb8cd7fef 100644
--- a/app/config/providers.ts
+++ b/app/config/providers.ts
@@ -23,6 +23,10 @@ export const STAMP_PROVIDERS: Readonly = {
name: "Ens",
description: "Ens name",
},
+ Poh: {
+ name: "POH",
+ description: "Proof of Humanity",
+ },
Twitter: {
name: "Twitter",
description: "Twitter name",
diff --git a/app/context/userContext.tsx b/app/context/userContext.tsx
index 68bee8cef3..9198fb0f4c 100644
--- a/app/context/userContext.tsx
+++ b/app/context/userContext.tsx
@@ -38,6 +38,10 @@ const startingAllProvidersState: AllProvidersState = {
providerSpec: STAMP_PROVIDERS.Ens,
stamp: undefined,
},
+ Poh: {
+ providerSpec: STAMP_PROVIDERS.Poh,
+ stamp: undefined,
+ },
Twitter: {
providerSpec: STAMP_PROVIDERS.Twitter,
stamp: undefined,
diff --git a/iam/__tests__/poh.test.ts b/iam/__tests__/poh.test.ts
new file mode 100644
index 0000000000..25fdf50b2e
--- /dev/null
+++ b/iam/__tests__/poh.test.ts
@@ -0,0 +1,70 @@
+// ---- Test subject
+import { RequestPayload } from "@dpopp/types";
+import { PohProvider } from "../src/providers/poh";
+
+const mockIsRegistered = jest.fn();
+
+jest.mock("ethers", () => {
+ return {
+ Contract: jest.fn().mockImplementation(() => {
+ return {
+ isRegistered: mockIsRegistered,
+ };
+ }),
+ };
+});
+
+const MOCK_ADDRESS = "0x738488886dd94725864ae38252a90be1ab7609c7";
+
+describe("Attempt verification", function () {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should return true for an address registered with proof of humanity", async () => {
+ mockIsRegistered.mockResolvedValueOnce(true);
+ const poh = new PohProvider();
+ const verifiedPayload = await poh.verify({
+ address: MOCK_ADDRESS,
+ } as RequestPayload);
+
+ expect(mockIsRegistered).toBeCalledWith(MOCK_ADDRESS);
+ expect(verifiedPayload).toEqual({
+ valid: true,
+ record: {
+ poh: "Is registered",
+ },
+ });
+ });
+
+ it("should return false for an address not registered with proof of humanity", async () => {
+ mockIsRegistered.mockResolvedValueOnce(false);
+ const UNREGISTERED_ADDRESS = "0xUNREGISTERED";
+
+ const poh = new PohProvider();
+ const verifiedPayload = await poh.verify({
+ address: UNREGISTERED_ADDRESS,
+ } as RequestPayload);
+
+ expect(mockIsRegistered).toBeCalledWith(UNREGISTERED_ADDRESS);
+ expect(verifiedPayload).toEqual({
+ valid: false,
+ });
+ });
+
+ it("should return error response when isRegistered call errors", async () => {
+ mockIsRegistered.mockRejectedValueOnce("some error");
+ const UNREGISTERED_ADDRESS = "0xUNREGISTERED";
+
+ const poh = new PohProvider();
+ const verifiedPayload = await poh.verify({
+ address: UNREGISTERED_ADDRESS,
+ } as RequestPayload);
+
+ expect(mockIsRegistered).toBeCalledWith(UNREGISTERED_ADDRESS);
+ expect(verifiedPayload).toEqual({
+ valid: false,
+ error: [JSON.stringify("some error")],
+ });
+ });
+});
diff --git a/iam/src/index.ts b/iam/src/index.ts
index 78f5a76941..24b869fdb2 100644
--- a/iam/src/index.ts
+++ b/iam/src/index.ts
@@ -34,6 +34,7 @@ import { SimpleProvider } from "./providers/simple";
import { GoogleProvider } from "./providers/google";
import { TwitterProvider } from "./providers/twitter";
import { EnsProvider } from "./providers/ens";
+import { PohProvider } from "./providers/poh";
// Initiate providers - new Providers should be registered in this array...
const providers = new Providers([
@@ -42,6 +43,7 @@ const providers = new Providers([
new GoogleProvider(),
new TwitterProvider(),
new EnsProvider(),
+ new PohProvider(),
]);
// create the app and run on port
diff --git a/iam/src/providers/poh.ts b/iam/src/providers/poh.ts
new file mode 100644
index 0000000000..0ca8f0fd94
--- /dev/null
+++ b/iam/src/providers/poh.ts
@@ -0,0 +1,69 @@
+// ----- Types
+import type { Provider, ProviderOptions } from "../types";
+import type { RequestPayload, VerifiedPayload } from "@dpopp/types";
+
+// ----- Ethers library
+import { Contract } from "ethers";
+import { StaticJsonRpcProvider } from "@ethersproject/providers";
+
+// Proof of humanity contract address
+const POH_CONTRACT_ADDRESS = "0xC5E9dDebb09Cd64DfaCab4011A0D5cEDaf7c9BDb";
+
+// Proof of humanity Contract ABI
+const POH_ABI = [
+ {
+ constant: true,
+ inputs: [{ internalType: "address", name: "_submissionID", type: "address" }],
+ name: "isRegistered",
+ outputs: [{ internalType: "bool", name: "", type: "bool" }],
+ payable: false,
+ stateMutability: "view",
+ type: "function",
+ },
+];
+
+// set the network rpc url based on env
+export const RPC_URL = process.env.RPC_URL;
+
+// Export a Poh Provider to carry out Proof of Humanity account is registered and active check and return a record object
+export class PohProvider implements Provider {
+ // Give the provider a type so that we can select it with a payload
+ type = "Poh";
+ // Options can be set here and/or via the constructor
+ _options = {};
+
+ // construct the provider instance with supplied options
+ constructor(options: ProviderOptions = {}) {
+ this._options = { ...this._options, ...options };
+ }
+
+ // verify that the proof object contains valid === "true"
+ async verify(payload: RequestPayload): Promise {
+ const { address } = payload;
+ try {
+ // define a provider using the rpc url
+ const provider: StaticJsonRpcProvider = new StaticJsonRpcProvider(RPC_URL);
+
+ // load Proof of humanity contract
+ const readContract = new Contract(POH_CONTRACT_ADDRESS, POH_ABI, provider);
+
+ // Checks to see if the address is registered with proof of humanity
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ const valid: boolean = await readContract.isRegistered(address);
+
+ return {
+ valid,
+ record: valid
+ ? {
+ poh: "Is registered",
+ }
+ : undefined,
+ };
+ } catch (e) {
+ return {
+ valid: false,
+ error: [JSON.stringify(e)],
+ };
+ }
+ }
+}
diff --git a/types/src/index.d.ts b/types/src/index.d.ts
index 498d5046af..349b25f7b6 100644
--- a/types/src/index.d.ts
+++ b/types/src/index.d.ts
@@ -119,4 +119,4 @@ export type Passport = {
// Passport DID
export type DID = string;
-export type PROVIDER_ID = "Google" | "Simple" | "Ens" | "Twitter";
+export type PROVIDER_ID = "Google" | "Simple" | "Ens" | "Poh" | "Twitter";