Skip to content

Commit

Permalink
feat(platforms): Cyberconnect stamp (passportxyz#1557)
Browse files Browse the repository at this point in the history
* feat: adds cyberconnect profile stamp

* feat: remove cyberprofile free tier stamp, adds cyberconnect org member stamp

* feat(app, platforms): include cyberconnect in front end

* chore(platforms): use context for duplicate requests

* chore(platforms): mock requests instead of making external calls

* fix(platforms): return use handle for record

* feat(platforms): return unique org membership from nonevm

* chore(app): feature flag cyberconnect

* fix(platforms): remove eslint disable

* fix(types): remove duplicate type

---------

Co-authored-by: HaoPeiwen <[email protected]>
  • Loading branch information
schultztimothy and HaoPeiwen authored Aug 8, 2023
1 parent 901c868 commit 147d484
Show file tree
Hide file tree
Showing 12 changed files with 556 additions and 1 deletion.
1 change: 1 addition & 0 deletions app/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ NEXT_PUBLIC_FF_NEW_GITHUB_STAMPS=on
NEXT_PUBLIC_FF_ONE_CLICK_VERIFICATION=on
NEXT_PUBLIC_FF_LIVE_ALLO_SCORE=on
NEXT_PUBLIC_FF_NEW_TWITTER_STAMPS=on
NEXT_PUBLIC_FF_CYBERCONNECT_STAMPS=on

NEXT_PUBLIC_CERAMIC_CACHE_ENDPOINT=http://localhost:8002/

Expand Down
2 changes: 2 additions & 0 deletions app/components/PlatformCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export const PlatformCard = ({
// Feature Flag Holonym Stamp
if (process.env.NEXT_PUBLIC_FF_HOLONYM_STAMP !== "on" && platform.platform === "Holonym") return <></>;

if (process.env.NEXT_PUBLIC_FF_CYBERCONNECT_STAMPS !== "on" && platform.platform === "CyberConnect") return <></>;

// returns a single Platform card
return (
<div className={className} key={`${platform.name}${i}`}>
Expand Down
8 changes: 8 additions & 0 deletions app/context/ceramicContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const {
Holonym,
Idena,
Civic,
CyberConnect,
} = stampPlatforms;
import { PlatformProps } from "../components/GenericPlatform";

Expand Down Expand Up @@ -227,6 +228,13 @@ platforms.set("Civic", {
platFormGroupSpec: Civic.ProviderConfig,
});

if (process.env.NEXT_PUBLIC_FF_CYBERCONNECT_STAMPS === "on") {
platforms.set("CyberConnect", {
platform: new CyberConnect.CyberConnectPlatform(),
platFormGroupSpec: CyberConnect.ProviderConfig,
});
}

export enum IsLoadingPassportState {
Idle,
Loading,
Expand Down
4 changes: 4 additions & 0 deletions app/public/assets/cyberconnectLogoIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions platforms/src/CyberProfile/App-Bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AppContext, ProviderPayload } from "../types";
import { Platform } from "../utils/platform";

export class CyberConnectPlatform extends Platform {
platformId = "CyberConnect";
path = "CyberConnect";
clientId: string = null;
redirectUri: string = null;
isEVM = true;

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

export const PlatformDetails: PlatformSpec = {
icon: "./assets/cyberconnectLogoIcon.svg",
platform: "CyberConnect",
name: "CyberConnect",
description: "Connect your wallet to verify your CyberProfile Handle.",
connectMessage: "Verify Account",
isEVM: true,
};

export const ProviderConfig: PlatformGroupSpec[] = [
{
platformGroup: "CyberProfile Handle",
providers: [
{ title: "Premium CyberProfile Handle ( length is between 1 and 6 characters )", name: "CyberProfilePremium" },
{ title: "Paid CyberProfile Handle ( length is between 7 and 12 characters )", name: "CyberProfilePaid" },
{ title: "Organization Membership", name: "CyberProfileOrgMember" },
],
},
];

export const providers: Provider[] = [
new CyberProfilePremiumProvider(),
new CyberProfilePaidProvider(),
new CyberProfileOrgMemberProvider(),
];
160 changes: 160 additions & 0 deletions platforms/src/CyberProfile/Providers/cyberconnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// ----- Types
import type { Provider, ProviderOptions } from "../../types";
import type { ProviderContext, RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";

// ----- Ethers library
import { Contract, BigNumber } from "ethers";
import { StaticJsonRpcProvider } from "@ethersproject/providers";

// CyberProfile Proxy Contract Address
const CYBERPROFILE_PROXY_CONTRACT_ADDRESS = "0x2723522702093601e6360CAe665518C4f63e9dA6";

// CyberProfile Proxy ABI functions needed to get the length of primary profile handle
const CYBERPROFILE_PROXY_ABI = [
{
inputs: [{ internalType: "address", name: "user", type: "address" }],
name: "getPrimaryProfile",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "uint256", name: "profileId", type: "uint256" }],
name: "getHandleByProfileId",
outputs: [{ internalType: "string", name: "", type: "string" }],
stateMutability: "view",
type: "function",
},
];

export type GithubContext = ProviderContext & {
cyberConnect?: {
handle?: string;
};
};

export type CyberConnectHandleResponse = {
handle?: string;
errors?: string[];
};

interface CyberProfileContract extends Contract {
getPrimaryProfile?(address: string): Promise<BigNumber>;
getHandleByProfileId?(id: number): Promise<string>;
// add other methods as needed
}

// return 0 if no primary handle is found, otherwise return the length of the primary handle
export async function getPrimaryHandle(
userAddress: string,
context: GithubContext
): Promise<CyberConnectHandleResponse> {
if (!context.cyberConnect?.handle) {
const provider: StaticJsonRpcProvider = new StaticJsonRpcProvider(
process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org/"
);

const contract: CyberProfileContract = new Contract(
CYBERPROFILE_PROXY_CONTRACT_ADDRESS,
CYBERPROFILE_PROXY_ABI,
provider
);
if (!context.cyberConnect) context.cyberConnect = {};
// get primary profile id
const profileId: BigNumber = await contract.getPrimaryProfile(userAddress);
// if no primary profile id is found (profileId == 0), return 0
if (profileId.isZero()) {
context.cyberConnect.handle = "";
return context.cyberConnect;
}
// get primary profile handle
const handle: string = await contract.getHandleByProfileId(profileId.toNumber());

context.cyberConnect.handle = handle;
// return the length of the primary handle
return context.cyberConnect;
}
return context.cyberConnect;
}

// Export a CyberProfilePremiumProvider
export class CyberProfilePremiumProvider implements Provider {
// Give the provider a type so that we can select it with a payload
type = "CyberProfilePremium";

// 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 address defined in the payload has a handle length <= 6 and > 0
async verify(payload: RequestPayload, context: ProviderContext): Promise<VerifiedPayload> {
// if a signer is provider we will use that address to verify against
const address = payload.address.toString().toLowerCase();
let valid = false;
let userHandle: string;
try {
const { handle } = await getPrimaryHandle(address, context);
userHandle = handle;
} catch (e) {
return {
valid: false,
error: ["CyberProfile provider get user primary handle error"],
};
}
const lengthOfPrimaryHandle = userHandle.length;
valid = lengthOfPrimaryHandle <= 6 && lengthOfPrimaryHandle > 0;
return Promise.resolve({
valid: valid,
record: valid
? {
userHandle,
}
: {},
});
}
}

// Export a CyberProfilePaidProvider
export class CyberProfilePaidProvider implements Provider {
// Give the provider a type so that we can select it with a payload
type = "CyberProfilePaid";

// 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 address defined in the payload has a handle length <= 12 and > 6
async verify(payload: RequestPayload, context: ProviderContext): Promise<VerifiedPayload> {
// if a signer is provider we will use that address to verify against
const address = payload.address.toString().toLowerCase();
let valid = false;
let userHandle: string;
try {
const { handle } = await getPrimaryHandle(address, context);
userHandle = handle;
} catch (e) {
return {
valid: false,
error: ["CyberProfile provider get user primary handle error"],
};
}
const lengthOfPrimaryHandle = userHandle.length;
valid = lengthOfPrimaryHandle <= 12 && lengthOfPrimaryHandle > 6;
return Promise.resolve({
valid: valid,
record: valid
? {
userHandle,
}
: {},
});
}
}
98 changes: 98 additions & 0 deletions platforms/src/CyberProfile/Providers/cyberconnect_nonevm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// ----- Types
import type { Provider, ProviderOptions } from "../../types";
import type { RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";

// ----- Libs
import axios from "axios";

export const cyberconnectGraphQL = "https://api.cyberconnect.dev/";

// Defining interfaces for the data structure returned by the gql query
interface CheckOrgMemberResponse {
data: {
data?: {
checkVerifiedOrganizationMember?: {
isVerifiedOrganizationMember: boolean;
uniqueIdentifier: string;
};
};
errors?: {
message: string;
}[];
};
}

export const checkForOrgMember = async (
url: string,
address: string
): Promise<{ isMember: boolean; identifier: string }> => {
let isMember = false;
let identifier = "";
let result: CheckOrgMemberResponse;

// Query the CyberConnect graphQL
try {
result = await axios.post(url, {
query: `
query CheckOrgMember {
checkVerifiedOrganizationMember (
address: "${address}"
)
{
isVerifiedOrganizationMember
uniqueIdentifier
}
}`,
});
if (result.data.errors) {
throw result.data.errors[0].message;
}
} catch (e: unknown) {
throw `The following error is being thrown: ${JSON.stringify(e)}`;
}

isMember = result.data.data.checkVerifiedOrganizationMember.isVerifiedOrganizationMember;
identifier = result.data.data.checkVerifiedOrganizationMember.uniqueIdentifier;
return {
isMember,
identifier,
};
};

// Export a CyberProfileOrgMemberProvider
export class CyberProfileOrgMemberProvider implements Provider {
// Give the provider a type so that we can select it with a payload
type = "CyberProfileOrgMember";

// 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 address defined in the payload has a handle length > 12
async verify(payload: RequestPayload): Promise<VerifiedPayload> {
// if a signer is provider we will use that address to verify against
const address = payload.address.toString().toLowerCase();
let valid = false;
try {
const { isMember, identifier } = await checkForOrgMember(cyberconnectGraphQL, address);
valid = isMember ? true : false;
return Promise.resolve({
valid: valid,
record: valid
? {
orgMembership: identifier,
}
: {},
});
} catch (e) {
return {
valid: false,
error: ["CyberProfile provider check organization membership error"],
};
}
}
}
Loading

0 comments on commit 147d484

Please sign in to comment.