Skip to content

Commit

Permalink
feat(platforms): added zksync model providers (passportxyz#2389)
Browse files Browse the repository at this point in the history
* feat(platforms): added zksync model providers

* feat(platforms): parameterized zk model tests

* fix: small type change
  • Loading branch information
lucianHymer authored Apr 18, 2024
1 parent 07ed83e commit be2ed2c
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 45 deletions.
82 changes: 43 additions & 39 deletions platforms/src/ETH/Providers/accountAnalysis.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
// ----- Types
import { ProviderExternalVerificationError, type Provider } from "../../types";
import { type Provider } from "../../types";
import type { RequestPayload, VerifiedPayload, ProviderContext, PROVIDER_ID } from "@gitcoin/passport-types";
import axios from "axios";
import { handleProviderAxiosError } from "../../utils/handleProviderAxiosError";

type HumanProbability = {
human_probability: number;
};
type HumanProbability = number;

export type ModelResponse = {
data: HumanProbability;
data: {
human_probability: HumanProbability;
};
};

type ETHAnalysis = {
humanProbability: HumanProbability;
};

export type ETHAnalysisContext = ProviderContext & {
ethAnalysis?: {
humanProbability?: HumanProbability;
};
ethAnalysis?: ETHAnalysis;
};

const dataScienceEndpoint = process.env.DATA_SCIENCE_API_URL;

export async function getETHAnalysis(address: string, context: ETHAnalysisContext): Promise<HumanProbability> {
if (context?.ethAnalysis?.humanProbability) {
return context.ethAnalysis.humanProbability;
}

const response = (await axios.post(`http://${dataScienceEndpoint}/eth-stamp-predict`, {
address,
})) as unknown as { data: ModelResponse };
export async function getETHAnalysis(address: string, context: ETHAnalysisContext): Promise<ETHAnalysis> {
if (!context?.ethAnalysis) {
const response = await fetchModelData<ModelResponse>(address, "eth-stamp-predict");

const humanProbability = response.data.data;
context.ethAnalysis = {
humanProbability: response.data.human_probability,
};
}
return context.ethAnalysis;
}

context.ethAnalysis = {
humanProbability,
};
return humanProbability;
export async function fetchModelData<T>(address: string, url_subpath: string): Promise<T> {
try {
const response = await axios.post(`http://${dataScienceEndpoint}/${url_subpath}`, {
address,
});
return response.data as T;
} catch (e) {
handleProviderAxiosError(e, "model data (" + url_subpath + ")", [dataScienceEndpoint]);
}
}

export type EthOptions = {
Expand All @@ -52,28 +60,24 @@ export class AccountAnalysis implements Provider {
}

async verify(payload: RequestPayload, context: ETHAnalysisContext): Promise<VerifiedPayload> {
try {
const { address } = payload;
const ethAnalysis = await getETHAnalysis(address, context);

if (ethAnalysis.human_probability < this.minimum) {
return {
valid: false,
errors: [
`You received a score of ${ethAnalysis.human_probability} from our analysis. You must have a score of ${this.minimum} or higher to obtain this stamp.`,
],
};
}
const { address } = payload;
const ethAnalysis = await getETHAnalysis(address, context);

if (ethAnalysis.humanProbability < this.minimum) {
return {
valid: true,
record: {
address,
},
valid: false,
errors: [
`You received a score of ${ethAnalysis.humanProbability} from our analysis. You must have a score of ${this.minimum} or higher to obtain this stamp.`,
],
};
} catch (e: unknown) {
throw new ProviderExternalVerificationError(`Error validating ETH amounts: ${String(e)}`);
}

return {
valid: true,
record: {
address,
},
};
}
}

Expand Down
4 changes: 2 additions & 2 deletions platforms/src/ETH/__tests__/accountAnalysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ describe("AccountAnalysis Providers", () => {
mockContext = {};
const response1 = await getETHAnalysis(mockAddress, mockContext);
const response2 = await getETHAnalysis(mockAddress, mockContext);
expect(response1.human_probability).toEqual(80);
expect(response2.human_probability).toEqual(80);
expect(response1.humanProbability).toEqual(80);
expect(response2.humanProbability).toEqual(80);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(axios.post).toHaveBeenCalledTimes(1);
});
Expand Down
39 changes: 36 additions & 3 deletions platforms/src/ZkSync/Providers-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PlatformSpec, PlatformGroupSpec, Provider } from "../types";
import { ZkSyncEraProvider } from "./Providers/zkSyncEra";
import { ZkSyncScore5Provider, ZkSyncScore20Provider, ZkSyncScore50Provider } from "./Providers/accountAnalysis";

export const PlatformDetails: PlatformSpec = {
icon: "./assets/zksyncStampIcon.svg",
Expand All @@ -13,9 +14,41 @@ export const PlatformDetails: PlatformSpec = {

export const ProviderConfig: PlatformGroupSpec[] = [
{
platformGroup: "zkSync Era",
providers: [{ title: "Transacted on zkSync Era", name: "ZkSyncEra" }],
platformGroup: "Transactional Verification",
providers: [
{
title: "Verified Transactor",
name: "ZkSyncEra",
description:
"Recognizes users whose transactions on zkSync Era have achieved verified status, confirming their active participation and trust in the platform.",
},
],
},
{
platformGroup: "Engagement Levels in zkSync Era",
providers: [
{
title: "Engagement Explorer",
name: "zkSyncScore#5",
description: "For users who are actively exploring various features of zkSync Era.",
},
{
title: "L2 Believer",
name: "zkSyncScore#20",
description: "For users who regularly transact and demonstrate reliance on the zkSync Era platform.",
},
{
title: "zkSync Champion",
name: "zkSyncScore#50",
description: "For leading users who significantly contribute to and influence the zkSync community.",
},
],
},
];

export const providers: Provider[] = [new ZkSyncEraProvider()];
export const providers: Provider[] = [
new ZkSyncEraProvider(),
new ZkSyncScore5Provider(),
new ZkSyncScore20Provider(),
new ZkSyncScore50Provider(),
];
93 changes: 93 additions & 0 deletions platforms/src/ZkSync/Providers/accountAnalysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// ----- Types
import { type Provider } from "../../types";
import type { RequestPayload, VerifiedPayload, ProviderContext, PROVIDER_ID } from "@gitcoin/passport-types";
import { fetchModelData } from "../../ETH/Providers/accountAnalysis";

export type ModelResponse = {
data: {
human_probability: number;
};
};

type ZkSyncAnalysis = {
humanProbability: number;
};

export type ZkSyncAnalysisContext = ProviderContext & {
zkSyncAnalysis?: ZkSyncAnalysis;
};

export async function getZkSyncAnalysis(address: string, context: ZkSyncAnalysisContext): Promise<ZkSyncAnalysis> {
if (!context?.zkSyncAnalysis) {
const response = await fetchModelData<ModelResponse>(address, "zksync-model-predict");

context.zkSyncAnalysis = {
humanProbability: response.data.human_probability,
};
}
return context.zkSyncAnalysis;
}

export type EthOptions = {
type: PROVIDER_ID;
minimum: number;
};

export class ZkSyncAccountAnalysis implements Provider {
type: PROVIDER_ID;
minimum: number;

// construct the provider instance with supplied options
constructor(options: EthOptions) {
this.type = options.type;
this.minimum = options.minimum;
}

async verify(payload: RequestPayload, context: ZkSyncAnalysisContext): Promise<VerifiedPayload> {
const { address } = payload;
const zkSyncAnalysis = await getZkSyncAnalysis(address, context);

if (zkSyncAnalysis.humanProbability < this.minimum) {
return {
valid: false,
errors: [
`You received a score of ${zkSyncAnalysis.humanProbability} from our analysis. You must have a score of ${this.minimum} or higher to obtain this stamp.`,
],
};
}

return {
valid: true,
record: {
address,
},
};
}
}

export class ZkSyncScore5Provider extends ZkSyncAccountAnalysis {
constructor() {
super({
type: "zkSyncScore#5",
minimum: 5,
});
}
}

export class ZkSyncScore20Provider extends ZkSyncAccountAnalysis {
constructor() {
super({
type: "zkSyncScore#20",
minimum: 20,
});
}
}

export class ZkSyncScore50Provider extends ZkSyncAccountAnalysis {
constructor() {
super({
type: "zkSyncScore#50",
minimum: 50,
});
}
}
86 changes: 86 additions & 0 deletions platforms/src/ZkSync/__tests__/accountAnalysis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { RequestPayload } from "@gitcoin/passport-types";
import axios from "axios";
import {
ZkSyncScore20Provider,
ZkSyncScore50Provider,
ModelResponse,
getZkSyncAnalysis,
ZkSyncScore5Provider,
} from "../Providers/accountAnalysis";

const mockAddress = "0x0";
let mockContext = {};
const mockResponse = (score: number): { data: ModelResponse } => ({
data: {
data: {
human_probability: score,
},
},
});

jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;

const scoreTestCases = [
//[score, [result for ZkSyncScore5Provider, result for ZkSyncScore20Provider, result for ZkSyncScore50Provider]]
[0, [false, false, false]],
[1, [false, false, false]],
[5, [true, false, false]],
[20, [true, true, false]],
[50, [true, true, true]],
[100, [true, true, true]],
]
.map(([score, expected]: [number, boolean[]]) => {
return [
[score, expected[0], ZkSyncScore5Provider],
[score, expected[1], ZkSyncScore20Provider],
[score, expected[2], ZkSyncScore50Provider],
];
})
.flat() as [
number,
boolean,
typeof ZkSyncScore5Provider | typeof ZkSyncScore20Provider | typeof ZkSyncScore50Provider,
][];

describe("AccountAnalysis Providers", () => {
beforeEach(() => {
jest.clearAllMocks();
mockContext = {};
});

describe("should return valid/invalid based on score", () => {
it.each(scoreTestCases)("score %i should return %s for %p", async (score, expected, provider) => {
const mockedResponse = mockResponse(score);
mockedAxios.post.mockResolvedValueOnce(mockedResponse);
const ethAdvocateProvider = new provider();
const payload = await ethAdvocateProvider.verify({ address: mockAddress } as RequestPayload, mockContext);

expect(payload.valid).toBe(expected);
if (expected) {
// eslint-disable-next-line jest/no-conditional-expect
expect(payload.record).toEqual({ address: mockAddress });
}
});
});

it("should handle errors gracefully", async () => {
jest.clearAllMocks();
mockedAxios.post.mockRejectedValueOnce(new Error("Test Error"));
const ethAdvocateProvider = new ZkSyncScore50Provider();
await expect(ethAdvocateProvider.verify({ address: mockAddress } as RequestPayload, mockContext)).rejects.toThrow();
});
describe("getZkSyncAnalysis", () => {
it("should use value from context if present", async () => {
const mockedResponse = mockResponse(80);
mockedAxios.post.mockResolvedValueOnce(mockedResponse);
mockContext = {};
const response1 = await getZkSyncAnalysis(mockAddress, mockContext);
const response2 = await getZkSyncAnalysis(mockAddress, mockContext);
expect(response1.humanProbability).toEqual(80);
expect(response2.humanProbability).toEqual(80);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(axios.post).toHaveBeenCalledTimes(1);
});
});
});
5 changes: 4 additions & 1 deletion types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@ export type PLATFORM_ID =
| "ETH"
| "GtcStaking"
| "NFT"
| "ZkSync"
| "Lens"
| "GnosisSafe"
| "Coinbase"
Expand All @@ -357,6 +356,7 @@ export type PLATFORM_ID =
| "Idena"
| "Civic"
| "GrantsStack"
| "ZkSync"
| "TrustaLabs";

export type PROVIDER_ID =
Expand Down Expand Up @@ -396,6 +396,9 @@ export type PROVIDER_ID =
| "SelfStakingGold"
| "NFT"
| "ZkSyncEra"
| "zkSyncScore#20"
| "zkSyncScore#50"
| "zkSyncScore#5"
| "Lens"
| "GnosisSafe"
| "CoinbaseDualVerification"
Expand Down

0 comments on commit be2ed2c

Please sign in to comment.