Skip to content

Commit

Permalink
Add at least N operator and checkExpired flag to GC Passport validati…
Browse files Browse the repository at this point in the history
…on strategy
  • Loading branch information
0xAurelius committed Mar 14, 2024
1 parent 7e4ed10 commit 1e5cba9
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 9 deletions.
4 changes: 2 additions & 2 deletions src/validations/passport-gated/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Before using this code, you need to create an API Key and Scorer ID to interact

## Stamps Metadata

The Stamps currently supported by Gitcoin Passport are stored in [stampsMetadata.json](./stampsMetadata.json). The Passport API has an [endpoint](https://docs.passport.gitcoin.co/building-with-passport/scorer-api/endpoint-definition#get-stamps-metadata-beta) where you can fetch all this information, but we don't do this programmatically in order to minimize the number of requests made by the validation strategy and meet the requirements listed in the main [README](../../../README.md).
The Stamps currently supported by Gitcoin Passport are stored in [stampsMetadata.json](./stampsMetadata.json). The Passport API has an [endpoint](https://docs.passport.gitcoin.co/building-with-passport/scorer-api/endpoint-definition#get-stamps-metadata-beta) where you can fetch all this information, but we don't do this programmatically in order to minimize the number of requests made by the validation strategy and meet the requirements listed in the main [README](../../../README.md).

**NOTICE**: this file might need to be updated from time to time when Passport updates their supported Stamps and VCs.

Expand All @@ -38,7 +38,7 @@ The main function (validate()) first fetches the following parameters:

Then, it calls the following validation methods:

* `validateStamps`: it uses the API to fetch the current user's Passport stamps and verifies that each has valid issuance and isn't expired. Then, depending on the `operator`, it will iterate through the required `stamps` and check that the user holds at least one verifiable credential that makes the passport eligible for that stamp. Finally, a Passport will be set as valid if it meets the criteria.
* `validateStamps`: it uses the API to fetch the current user's Passport stamps and verifies that each has valid issuance and isn't expired (if checkExpired param is set to true). Then, depending on the `operator`, it will iterate through the required `stamps` and check that the user holds at least one verifiable credential that makes the passport eligible for that stamp. Finally, a Passport will be set as valid if it meets the criteria.
* `validatePassportScore`: if `scoreThreshold` is set to zero this function will be omitted. Otherwise when called, it uses the Scorer API to submit the passport for scoring and get the latest score. If the API response returns a payload with `status === 'DONE'` it will return the result of evaluating the scoring threshold criteria, otherwise the implementation will make periodic requests (up to `PASSPORT_SCORER_MAX_ATTEMPTS`) to the Scorer API until getting a `DONE` status.

Finally, it checks the results of both eval functions and returns a boolean value indicating whether the user has a valid Passport.
Expand Down
15 changes: 15 additions & 0 deletions src/validations/passport-gated/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,20 @@
"operator": "OR"
},
"valid": false
},
{
"name": "Example of a passport gated validation",
"author": "0x24F15402C6Bb870554489b2fd2049A85d75B982f",
"space": "fabien.eth",
"network": "1",
"snapshot": "latest",
"params": {
"scoreThreshold": 20,
"stamps": ["Ens", "Github", "Snapshot"],
"operator": "MIN",
"minStamps": 1,
"checkExpired": false
},
"valid": true
}
]
47 changes: 40 additions & 7 deletions src/validations/passport-gated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,32 @@ const stampCredentials = STAMPS.map((stamp) => {
// Useful to get stamp metadata and update `stampsMetata.json`
// console.log('stampCredentials', JSON.stringify(stampCredentials.map((s) => ({"const": s.id, title: s.name}))));

function hasValidIssuanceAndExpiration(credential: any, proposalTs: string) {
function hasValidIssuanceAndExpiration(
credential: any,
proposalTs: string,
checkExpired: boolean
) {
const issuanceDate = Number(
new Date(credential.issuanceDate).getTime() / 1000
).toFixed(0);
const expirationDate = Number(
new Date(credential.expirationDate).getTime() / 1000
).toFixed(0);
if (issuanceDate <= proposalTs && expirationDate >= proposalTs) {
return true;

// handle undefined checkExpired since it's a new parameter
if (checkExpired === undefined) {
checkExpired = true; // default value
}
return false;

let isValid = false;
if (issuanceDate <= proposalTs) {
if (checkExpired && expirationDate >= proposalTs) {
isValid = true;
} else if (!checkExpired) {
isValid = true;
}
}
return isValid;
}

function hasStampCredential(stampId: string, credentials: Array<string>) {
Expand All @@ -64,7 +79,9 @@ async function validateStamps(
currentAddress: string,
operator: string,
proposalTs: string,
requiredStamps: Array<string> = []
requiredStamps: Array<string> = [],
minStamps: number,
checkExpired: boolean
): Promise<boolean> {
if (requiredStamps.length === 0) return true;

Expand All @@ -82,7 +99,7 @@ async function validateStamps(
// check expiration for all stamps
const validStamps = stampsData.items
.filter((stamp: any) =>
hasValidIssuanceAndExpiration(stamp.credential, proposalTs)
hasValidIssuanceAndExpiration(stamp.credential, proposalTs, checkExpired)
)
.map((stamp: any) => stamp.credential.credentialSubject.provider);

Expand All @@ -94,7 +111,19 @@ async function validateStamps(
return requiredStamps.some((stampId) =>
hasStampCredential(stampId, validStamps)
);
} else if (operator === 'MIN') {
if (minStamps === null) {
throw new Error(
'When using MIN operator, minStamps parameter must be specified'
);
}
return (
requiredStamps.filter((stampId) =>
hasStampCredential(stampId, validStamps)
).length >= minStamps
);
}

return false;
}

Expand Down Expand Up @@ -168,6 +197,8 @@ export default class extends Validation {
const requiredStamps = this.params.stamps || [];
const operator = this.params.operator;
const scoreThreshold = this.params.scoreThreshold;
const minStamps = this.params.minStamps;
const checkExpired = this.params.checkExpired;

if (scoreThreshold === undefined)
throw new Error('Score threshold is required');
Expand All @@ -180,7 +211,9 @@ export default class extends Validation {
currentAddress,
operator,
proposalTs,
requiredStamps
requiredStamps,
minStamps,
checkExpired
);

if (scoreThreshold === 0) {
Expand Down
18 changes: 18 additions & 0 deletions src/validations/passport-gated/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,27 @@
{
"const": "OR",
"title": "Require at least one stamp"
},
{
"const": "MIN",
"title": "Require at least N stamps (specified via minStamps param)"
}
]
},
"minStamps": {
"type": "number",
"title": "Minimum matching stamps for MIN operator",
"description": "Minimum number of matching stamps for MIN operator - ignored for other operators",
"minimum": 1,
"maximum": 1000000,
"default": null
},
"checkExpired": {
"type": "boolean",
"title": "Check stamp expiration",
"description": "Whether or not to check for expired stamps - if false, expired stamps count as valid",
"default": true
},
"stamps": {
"type": "array",
"title": "Stamps",
Expand Down

0 comments on commit 1e5cba9

Please sign in to comment.