Skip to content

Commit

Permalink
feat(platforms): updated GTC stake provider to use new API response (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
lucianHymer authored Nov 13, 2023
1 parent a31214b commit 5c285eb
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 224 deletions.
184 changes: 39 additions & 145 deletions platforms/src/GtcStaking/Providers/GtcStaking.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
// ----- Types
import { ProviderExternalVerificationError, type Provider } from "../../types";
import { ProviderExternalVerificationError, ProviderInternalVerificationError, type Provider } from "../../types";
import type { ProviderContext, PROVIDER_ID, RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";

// ----- Libs
import axios from "axios";
import { handleProviderAxiosError } from "../../utils/handleProviderAxiosError";
import BigNumber from "bignumber.js";

// ----- Utils
import { buildCID } from "../../utils/createCid";

const gtcStakingEndpoint = `${process.env.PASSPORT_SCORER_BACKEND}registry/gtc-stake`;
const apiKey = process.env.SCORER_API_KEY;

Expand All @@ -31,11 +28,6 @@ export type Stake = {
tx_hash: string;
};

type CommunityStakingCounts = {
bcs1gte5: number;
ecs2gte10: number;
tc5gte20: number;
};
export interface StakeResponse {
data: {
results: Stake[];
Expand All @@ -50,164 +42,66 @@ export type GtcStakingContext = ProviderContext & {

export type GtcStakingProviderOptions = {
type: PROVIDER_ID;
threshold?: BigNumber | number;
dataKey: keyof UserStake;
communityTypeCount?: number | undefined;
// Only needed for historic hashes, can be left
// off of any new providers
identifier?: string;
thresholdAmount: BigNumber;
};

export class GtcStakingProvider implements Provider {
type: PROVIDER_ID;
threshold?: BigNumber | number;
dataKey: keyof UserStake;
identifier: string;
communityTypeCount?: number | undefined;
thresholdAmount: BigNumber;

// construct the provider instance with supplied options
constructor(options: GtcStakingProviderOptions) {
this.type = options.type;
this.threshold = options.threshold;
this.dataKey = options.dataKey;
this.identifier = options.identifier;
this.communityTypeCount = options.communityTypeCount;
this.thresholdAmount = options.thresholdAmount;
}

// verify that the proof object contains valid === "true"
async verify(payload: RequestPayload, context: GtcStakingContext): Promise<VerifiedPayload> {
try {
const address = payload.address.toLowerCase();
const errors: string[] = [];
let record = undefined,
valid = false,
stakeData;

if (!address || address.substring(0, 2) !== "0x" || address.length !== 42) {
valid = false;
throw Error("Not a proper ethereum address");
}

try {
stakeData = await verifyStake(payload, context);
} catch (error: unknown) {
errors.push(String(error));
}

const selfStakeAmount = stakeData.selfStake;
const communityStakes = stakeData.communityStakes;
const commStakeCounts = await checkCommunityStakes(communityStakes, address);

if (selfStakeAmount >= this.threshold) valid = true;

for (const [key, val] of Object.entries(commStakeCounts)) {
if (val >= this.communityTypeCount && this.identifier === key) {
valid = true;
}
}

if (valid) {
record = {
address: payload.address,
stakeAmount: this.identifier,
};
} else if (!valid && selfStakeAmount < this.threshold) {
errors.push(
`Your current GTC self staking amount is ${selfStakeAmount.toString()} GTC, which is below the required ${this.threshold.toString()} GTC for this stamp.`
);
} else {
errors.push(
"You are not staking enough on community members and/or community members are not staking enough on you 🥲"
);
}

return {
valid,
record,
errors,
};
} catch (e: unknown) {
throw new ProviderExternalVerificationError(`${this.type} verifyStake: ${String(e)}.`);
}
verify(_payload: RequestPayload, _context: GtcStakingContext): Promise<VerifiedPayload> {
throw new Error("Method not implemented, this base class should not be used directly");
}
}

async function checkCommunityStakes(communityStakes: Stake[], address: string): Promise<CommunityStakingCounts> {
const bcsMap = new Map<string, number>();
const ecsMap = new Map<string, number>();
const tcMap = new Map<string, number>();
const tcSet = new Set();

for (let i = 0; i < communityStakes.length; i++) {
const stake = communityStakes[i];
const cid = await buildCID({ address: stake.address, staker: stake.staker, amount: stake.amount });
const currentAmount = new BigNumber(stake.amount);

if (stake.address === address || stake.staker === address) {
if (currentAmount.gte(5) && currentAmount.lt(10)) {
bcsMap.set(cid, (bcsMap.get(cid) || 0) + 1);
}
if (currentAmount.gte(10) && currentAmount.lt(20)) {
bcsMap.set(cid, (bcsMap.get(cid) || 0) + 1);
ecsMap.set(cid, (ecsMap.get(cid) || 0) + 1);
}
}

if (stake.address === address && currentAmount.gte(20)) {
if (!tcSet.has(stake.staker)) {
tcSet.add(stake.staker);
bcsMap.set(cid, (bcsMap.get(cid) || 0) + 1);
ecsMap.set(cid, (ecsMap.get(cid) || 0) + 1);
tcMap.set(cid, (tcMap.get(cid) || 0) + 1);
}
getAddress(payload: RequestPayload): string {
const address = payload.address.toLowerCase();
if (!address || address.substring(0, 2) !== "0x" || address.length !== 42) {
throw new ProviderInternalVerificationError("Not a proper ethereum address");
}
return address;
}

// Use the maps to find unpaired CIDs.
const bcs1gte5 = [...bcsMap].filter(([_, count]) => count === 1).map(([cid, _]) => cid).length;
const ecs2gte10 = [...ecsMap].filter(([_, count]) => count === 1).map(([cid, _]) => cid).length;
const tc5gte20 = [...tcMap].filter(([_, count]) => count === 1).map(([cid, _]) => cid).length;

return {
bcs1gte5,
ecs2gte10,
tc5gte20,
};
}

async function verifyStake(payload: RequestPayload, context: GtcStakingContext): Promise<UserStake> {
try {
if (!context.gtcStaking?.userStake) {
const round = process.env.GTC_STAKING_ROUND || "1";
const address = payload.address.toLowerCase();
async getStakes(payload: RequestPayload, context: GtcStakingContext): Promise<UserStake> {
try {
if (!context.gtcStaking?.userStake) {
const round = process.env.GTC_STAKING_ROUND || "1";
const address = payload.address.toLowerCase();

const selfStakes: Stake[] = [];
const communityStakes: Stake[] = [];
const selfStakes: Stake[] = [];
const communityStakes: Stake[] = [];

const response: StakeResponse = await axios.get(`${gtcStakingEndpoint}/${address}/${round}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const response: StakeResponse = await axios.get(`${gtcStakingEndpoint}/${address}/${round}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});

const results: Stake[] = response.data.results;
const results: Stake[] = response?.data?.results;
if (!results) throw new ProviderExternalVerificationError("No results returned from the GTC Staking API");

results.forEach((stake: Stake) => {
stake.event_type === "SelfStake" ? selfStakes.push(stake) : communityStakes.push(stake);
});
results.forEach((stake: Stake) => {
stake.event_type === "SelfStake" ? selfStakes.push(stake) : communityStakes.push(stake);
});

const selfStake: BigNumber = selfStakes.reduce((acc, curr) => {
if (curr.staked === true) {
return acc.plus(new BigNumber(curr.amount));
} else {
return acc.minus(new BigNumber(curr.amount));
}
}, new BigNumber(0));
const selfStake: BigNumber = selfStakes.reduce((totalStake, currentStake) => {
if (currentStake.staked === true) {
return totalStake.plus(new BigNumber(currentStake.amount));
} else {
return totalStake.minus(new BigNumber(currentStake.amount));
}
}, new BigNumber(0));

if (!context.gtcStaking) context.gtcStaking = {};
if (!context.gtcStaking) context.gtcStaking = {};

context.gtcStaking.userStake = { selfStake, communityStakes };
context.gtcStaking.userStake = { selfStake, communityStakes };
}
} catch (error) {
handleProviderAxiosError(error, "Verify GTC stake", [payload.address]);
}
} catch (error) {
handleProviderAxiosError(error, "Verify GTC stake", [payload.address]);
return context.gtcStaking.userStake;
}
return context.gtcStaking.userStake;
}
86 changes: 74 additions & 12 deletions platforms/src/GtcStaking/Providers/communityStaking.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,81 @@
import { GtcStakingProvider, GtcStakingProviderOptions } from "./GtcStaking";
import { RequestPayload, VerifiedPayload } from "@gitcoin/passport-types";
import BigNumber from "bignumber.js";
import { GtcStakingContext, GtcStakingProvider, GtcStakingProviderOptions, Stake } from "./GtcStaking";

class CommunityStakingBaseProvider extends GtcStakingProvider {
constructor(options: Omit<GtcStakingProviderOptions, "dataKey">) {
super({
...options,
dataKey: "communityStakes",
});
minimumCountCommunityStakes: number;

constructor(options: GtcStakingProviderOptions & { minimumCountCommunityStakes: number }) {
super(options);
this.minimumCountCommunityStakes = options.minimumCountCommunityStakes;
}

async verify(payload: RequestPayload, context: GtcStakingContext): Promise<VerifiedPayload> {
const address = this.getAddress(payload);
const stakeData = await this.getStakes(payload, context);
const communityStakes = stakeData.communityStakes;

const countRelevantStakes = this.getCountRelevantStakes(communityStakes, address);

if (countRelevantStakes >= this.minimumCountCommunityStakes) {
return {
valid: true,
record: { address },
};
} else {
return {
valid: false,
errors: [
`There are currently ${countRelevantStakes} community stakes of at least ${this.thresholdAmount.toString()} GTC on/by your address, ` +
`you need a minimum of ${this.minimumCountCommunityStakes} relevant community stakes to claim this stamp`,
],
};
}
}

getCountRelevantStakes(communityStakes: Stake[], address: string): number {
const stakesOnAddressByOthers: Record<string, BigNumber> = {};
const stakesByAddressOnOthers: Record<string, BigNumber> = {};

for (let i = 0; i < communityStakes.length; i++) {
const stake = communityStakes[i];
const stakeAmount = new BigNumber(stake.amount);

if (stake.staker === address && stake.address !== address) {
stakesByAddressOnOthers[stake.address] ||= new BigNumber(0);
if (stake.staked) {
stakesByAddressOnOthers[stake.address] = stakesByAddressOnOthers[stake.address].plus(stakeAmount);
} else {
stakesByAddressOnOthers[stake.address] = stakesByAddressOnOthers[stake.address].sub(stakeAmount);
}
} else if (stake.address === address && stake.staker !== address) {
stakesOnAddressByOthers[stake.staker] ||= new BigNumber(0);
if (stake.staked) {
stakesOnAddressByOthers[stake.staker] = stakesOnAddressByOthers[stake.staker].plus(stakeAmount);
} else {
stakesOnAddressByOthers[stake.staker] = stakesOnAddressByOthers[stake.staker].sub(stakeAmount);
}
}
}

return [...Object.entries(stakesByAddressOnOthers), ...Object.entries(stakesOnAddressByOthers)].reduce(
(count, [_address, amount]) => {
if (amount.gte(this.thresholdAmount)) {
return count + 1;
}
return count;
},
0
);
}
}

export class BeginnerCommunityStakerProvider extends CommunityStakingBaseProvider {
constructor() {
super({
type: "BeginnerCommunityStaker",
identifier: "bcs1gte5",
communityTypeCount: 1,
thresholdAmount: new BigNumber(5),
minimumCountCommunityStakes: 1,
});
}
}
Expand All @@ -22,8 +84,8 @@ export class ExperiencedCommunityStakerProvider extends CommunityStakingBaseProv
constructor() {
super({
type: "ExperiencedCommunityStaker",
identifier: "ecs2gte10",
communityTypeCount: 2,
thresholdAmount: new BigNumber(10),
minimumCountCommunityStakes: 2,
});
}
}
Expand All @@ -32,8 +94,8 @@ export class TrustedCitizenProvider extends CommunityStakingBaseProvider {
constructor() {
super({
type: "TrustedCitizen",
identifier: "tc5gte20",
communityTypeCount: 5,
thresholdAmount: new BigNumber(20),
minimumCountCommunityStakes: 5,
});
}
}
Loading

1 comment on commit 5c285eb

@zidane75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

H

Please sign in to comment.