Skip to content

feat: adding stamp for proof of clean hands #3435

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

Merged
merged 3 commits into from
Apr 29, 2025
Merged
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
1 change: 1 addition & 0 deletions app/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ NEXT_PUBLIC_FF_NEW_TWITTER_STAMPS=on
NEXT_PUBLIC_FF_CYBERCONNECT_STAMPS=on
NEXT_PUBLIC_FF_TRUSTALABS_STAMPS=on
NEXT_PUBLIC_FF_OUTDID_STAMP=on
NEXT_PUBLIC_FF_CLEAN_HANDS_STAMP=on
NEXT_PUBLIC_FF_ONCHAIN_ZKSYNC=off
NEXT_PUBLIC_FF_ONCHAIN_SCROLL=off
NEXT_PUBLIC_FF_ONCHAIN_SHAPE=off
Expand Down
8 changes: 8 additions & 0 deletions app/config/platformMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
Outdid,
Binance,
CustomGithub,
CleanHands,
} = platforms;

type CustomPlatformTypeInfo = {
Expand Down Expand Up @@ -208,3 +209,10 @@ export const CUSTOM_PLATFORM_TYPE_INFO: { [id: string]: CustomPlatformTypeInfo }
},
},
};

if (process.env.NEXT_PUBLIC_FF_CLEAN_HANDS_STAMP === "on") {
defaultPlatformMap.set("CleanHands", {
platform: new CleanHands.CleanHandsPlatform(),
platFormGroupSpec: CleanHands.ProviderConfig,
});
}
2 changes: 1 addition & 1 deletion app/hooks/usePlatforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const BASE_PLATFORM_CATAGORIES: PLATFORM_CATEGORY[] = [
{
name: "Government IDs",
description: "Use your government-issued IDs or complete a KYC process with our partners to verify your identity.",
platforms: ["Coinbase", "Holonym", "Outdid", "Binance"],
platforms: ["Coinbase", "Holonym", "Outdid", "Binance", "CleanHands"],
},
{
name: "Social & Professional Platforms",
Expand Down
5 changes: 5 additions & 0 deletions app/public/assets/proofOfCleanHandsBlack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions app/public/assets/proofOfCleanHandsWhite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions platforms/src/CleanHands/App-Bindings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable */
import React from "react";
import { AppContext, ProviderPayload } from "../types.js";
import { Platform } from "../utils/platform.js";
import { Hyperlink } from "../utils/Hyperlink.js";

export class CleanHandsPlatform extends Platform {
platformId = "CleanHands";
path = "clean_hands";
isEVM = true;
banner = {
heading: "Privately prove you are not sanctioned using Proof of Clean Hands",
content: (
<>
To add the Clean Hands Stamp to your Passport...{" "}
<Hyperlink href="">Learn more - TODO (link to new knowledge base article)</Hyperlink>
<ul style={{ listStyleType: "disc", paddingLeft: "20px" }}>
<li>
Have a smartphone and an Ethereum wallet with $5 in ETH (mainnet, OP, or Aurora), AVAX, or FTM for Clean
Hands
</li>
<li>
Go to{" "}
<Hyperlink href="https://silksecure.net/holonym/diff-wallet/clean-hands/issuance/prereqs">
Proof of Clean Hands
</Hyperlink>
, verify your Gov ID by connecting your wallet, and follow prompts to obtain the Clean Hands verification.
</li>
<li>After verification, mint the SBT to your wallet, then link it to your Passport by verifying it.</li>
</ul>
<br></br>
Check the Sign attestation protocol to validate:
<ul style={{ listStyleType: "disc", paddingLeft: "20px" }}>
<li>Default to checking all 3 chains</li>
<li>End users will only be able to mint Passport on OP (out of Sui, Near & OP)</li>
</ul>
</>
),
cta: {
label: "Learn more",
url: "https://support.passport.xyz/passport-knowledge-base/stamps/how-do-i-add-passport-stamps/connecting-snapshot-to-passport",
},
};

async getProviderPayload(appContext: AppContext): Promise<ProviderPayload> {
const result = await Promise.resolve({});
return result;
}
}
21 changes: 21 additions & 0 deletions platforms/src/CleanHands/Providers-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PlatformSpec, PlatformGroupSpec, Provider } from "../types.js";
import { ClanHandsProvider } from "./Providers/index.js";

export const PlatformDetails: PlatformSpec = {
icon: "./assets/proofOfCleanHandsWhite.svg",
platform: "CleanHands",
name: "Clean Hands",
description: "Privately prove you are not sanctioned using Proof of Clean Hands",
connectMessage: "Verify Account",
isEVM: true,
website: "https://silksecure.net/holonym/diff-wallet/clean-hands/issuance/prereqs",
};

export const ProviderConfig: PlatformGroupSpec[] = [
{
platformGroup: "Clean Hands",
providers: [{ title: "Prove clean hands ...", name: "CleanHands" }],
},
];

export const providers: Provider[] = [new ClanHandsProvider()];
115 changes: 115 additions & 0 deletions platforms/src/CleanHands/Providers/__tests__/cleanHands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { RequestPayload, VerifiedPayload, ProviderContext } from "@gitcoin/passport-types";
import axios from "axios";
import { ClanHandsProvider } from "../index.js";

jest.mock("axios");

const mockedAxios = axios as jest.Mocked<typeof axios>;

describe("ClanHandsProvider", function () {
beforeEach(() => {
jest.clearAllMocks();
});
const mockContext: ProviderContext = {
github: {
id: "123",
},
};
const mockPayload: RequestPayload = {
address: "0x0",
proofs: {
code: "ABC123_ACCESSCODE",
},
type: "",
version: "",
};
const mockIndexingValue = "0xnullifier";
it("handles valid verification attempt", async () => {
mockedAxios.get.mockImplementation(async () => {
return {
data: {
data: {
rows: [
{
fullSchemaId: "onchain_evm_10_0x8",
attester: "0xB1f50c6C34C72346b1229e5C80587D0D659556Fd",
isReceiver: true,
revoked: false,
validUntil: new Date().getTime() / 1000 + 3600,
indexingValue: mockIndexingValue,
},
],
},
},
};
});

const provider = new ClanHandsProvider();
const result: VerifiedPayload = await provider.verify(mockPayload, mockContext);

expect(mockedAxios.get).toHaveBeenCalledTimes(1);
expect(mockedAxios.get).toHaveBeenCalledWith(
`https://mainnet-rpc.sign.global/api/scan/addresses/${mockPayload.address}/attestations`
);
expect(result).toEqual({
valid: true,
errors: undefined,
record: { id: mockIndexingValue },
});
});

it.each([
[
{
fullSchemaId: "bad_schema",
},
],
[
{
attester: "bad_attester",
},
],
[
{
isReceiver: false,
},
],
[{ revoked: true }],
[{ validUntil: new Date().getTime() / 1000 - 3600 }],
[{ indexingValue: undefined }],
[{ indexingValue: null }],
])("handles invalid verification attempt with: `%s`", async (attestationAttributes) => {
mockedAxios.get.mockImplementation(async () => {
return {
data: {
data: {
rows: [
{
fullSchemaId: "onchain_evm_10_0x8",
attester: "0xB1f50c6C34C72346b1229e5C80587D0D659556Fd",
isReceiver: true,
revoked: false,
validUntil: new Date().getTime() / 1000 + 3600,
indexingValue: mockIndexingValue,
...attestationAttributes,
},
],
},
},
};
});

const provider = new ClanHandsProvider();
const result: VerifiedPayload = await provider.verify(mockPayload, mockContext);

expect(mockedAxios.get).toHaveBeenCalledTimes(1);
expect(mockedAxios.get).toHaveBeenCalledWith(
`https://mainnet-rpc.sign.global/api/scan/addresses/${mockPayload.address}/attestations`
);
expect(result).toEqual({
valid: false,
errors: [`Unable to find any valid attestation for ${mockPayload.address}`],
record: undefined,
});
});
});
77 changes: 77 additions & 0 deletions platforms/src/CleanHands/Providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { type Provider } from "../../types.js";
import { RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";
import axios from "axios";
import { handleProviderAxiosError } from "../../utils/handleProviderAxiosError.js";

type Attestation = {
fullSchemaId: string;
attester: "0xB1f50c6C34C72346b1229e5C80587D0D659556Fd";
isReceiver: boolean;
revoked: boolean;
validUntil: number;
indexingValue: string;
};

type CleanHandsResponseData = {
success: boolean;
statusCode: number;
data: {
total: number;
rows: Attestation[];
page: number;
size: number;
};
message: string;
};

type CleanHandsResponse = {
data: CleanHandsResponseData;
};

export class ClanHandsProvider implements Provider {
// The type will be determined dynamically, from the options passed in to the constructor
type = "CleanHands";

constructor() {}

async verify(payload: RequestPayload, context: any): Promise<VerifiedPayload> {
let valid = false;
let errors: string[] | undefined = undefined;
let record:
| {
[k: string]: string;
}
| undefined = undefined;

try {
// Set user address
const address = payload.address.toLowerCase();

const resp: CleanHandsResponse = await axios.get(
`https://mainnet-rpc.sign.global/api/scan/addresses/${address}/attestations`
);
const data = resp.data;

const cleanHandsAttestations = data.data.rows.find(
(att) =>
att.fullSchemaId == "onchain_evm_10_0x8" &&
att.attester == "0xB1f50c6C34C72346b1229e5C80587D0D659556Fd" &&
att.isReceiver == true &&
!att.revoked &&
att.validUntil > new Date().getTime() / 1000
);
// Make sure cleanHandsAttestations and cleanHandsAttestations.indexingValue are not undefined or null
valid = !!cleanHandsAttestations?.indexingValue;
errors = valid ? undefined : [`Unable to find any valid attestation for ${address}`];
record = !valid ? undefined : { id: cleanHandsAttestations?.indexingValue };
} catch (error) {
handleProviderAxiosError(error, "CleanHands");
}

return {
valid,
errors,
record,
};
}
}
3 changes: 3 additions & 0 deletions platforms/src/CleanHands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { CleanHandsPlatform } from "./App-Bindings.js";
export { PlatformDetails, ProviderConfig, providers } from "./Providers-config.js";
export { ClanHandsProvider } from "./Providers/index.js";
2 changes: 2 additions & 0 deletions platforms/src/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as Outdid from "./Outdid/index.js";
import * as AllowList from "./AllowList/index.js";
import * as Binance from "./Binance/index.js";
import * as CustomGithub from "./CustomGithub/index.js";
import * as CleanHands from "./CleanHands/index.js";
import { PlatformSpec, PlatformGroupSpec, Provider } from "./types.js";

export type PlatformConfig = {
Expand Down Expand Up @@ -60,6 +61,7 @@ const platforms: Record<string, PlatformConfig> = {
AllowList,
Binance,
CustomGithub,
CleanHands,
};

if (process.env.NEXT_PUBLIC_FF_NEW_POAP_STAMPS === "on") {
Expand Down
6 changes: 4 additions & 2 deletions types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,8 @@ export type PLATFORM_ID =
| "AllowList"
| "Binance"
| "DeveloperList"
| `Custom#${string}`;
| `Custom#${string}`
| "CleanHands";

export type PLATFORM_CATEGORY = {
name: string;
Expand Down Expand Up @@ -447,7 +448,8 @@ export type PROVIDER_ID =
| `AllowList#${string}`
| "BinanceBABT"
| "BinanceBABT2"
| `DeveloperList#${string}#${string}`;
| `DeveloperList#${string}#${string}`
| "CleanHands";

export type StampBit = {
bit: number;
Expand Down
Loading