diff --git a/src/validations/passport-gated/README.md b/src/validations/passport-gated/README.md index fd7349598..8bd032759 100644 --- a/src/validations/passport-gated/README.md +++ b/src/validations/passport-gated/README.md @@ -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. @@ -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. diff --git a/src/validations/passport-gated/examples.json b/src/validations/passport-gated/examples.json index b89c9a402..1f82d8c3a 100644 --- a/src/validations/passport-gated/examples.json +++ b/src/validations/passport-gated/examples.json @@ -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 } ] diff --git a/src/validations/passport-gated/index.ts b/src/validations/passport-gated/index.ts index 6bad1f765..4095f8c9d 100644 --- a/src/validations/passport-gated/index.ts +++ b/src/validations/passport-gated/index.ts @@ -36,17 +36,27 @@ 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; + + let isValid = false; + if (issuanceDate <= proposalTs) { + if (checkExpired && expirationDate >= proposalTs) { + isValid = true; + } else if (~checkExpired) { + isValid = true; + } } - return false; + return isValid; } function hasStampCredential(stampId: string, credentials: Array) { @@ -64,7 +74,9 @@ async function validateStamps( currentAddress: string, operator: string, proposalTs: string, - requiredStamps: Array = [] + requiredStamps: Array = [], + minStamps: number, + checkExpired: boolean ): Promise { if (requiredStamps.length === 0) return true; @@ -82,7 +94,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); @@ -94,7 +106,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; } @@ -168,6 +192,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'); @@ -180,7 +206,9 @@ export default class extends Validation { currentAddress, operator, proposalTs, - requiredStamps + requiredStamps, + minStamps, + checkExpired ); if (scoreThreshold === 0) { diff --git a/src/validations/passport-gated/schema.json b/src/validations/passport-gated/schema.json index 9b430e8aa..0ac19c897 100644 --- a/src/validations/passport-gated/schema.json +++ b/src/validations/passport-gated/schema.json @@ -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",