From 3866decdf9a2b6fa386f24295625ac55871c05d2 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Mon, 2 Feb 2026 18:26:04 +0400 Subject: [PATCH 1/3] Added Human Passport identity integration --- .env.example | 3 + src/features/incentive/PointSystem.ts | 168 +++++++++++++- .../gcr/gcr_routines/GCRIdentityRoutines.ts | 109 +++++++++ .../gcr/gcr_routines/IncentiveManager.ts | 22 ++ .../gcr/gcr_routines/identityManager.ts | 123 +++++++++- src/libs/identity/tools/humanpassport.ts | 219 ++++++++++++++++++ src/libs/network/manageGCRRoutines.ts | 49 ++++ .../transactions/handleIdentityRequest.ts | 6 + src/model/entities/GCRv2/GCR_Main.ts | 1 + src/model/entities/types/IdentityTypes.ts | 29 +++ 10 files changed, 710 insertions(+), 19 deletions(-) create mode 100644 src/libs/identity/tools/humanpassport.ts diff --git a/.env.example b/.env.example index f8de622a..104d11df 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ DISCORD_BOT_TOKEN= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= +HUMAN_PASSPORT_API_KEY= +HUMAN_PASSPORT_SCORER_ID= + # OmniProtocol TCP Server (optional - disabled by default) OMNI_ENABLED=false OMNI_PORT=3001 diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 44f765ba..84fd70ad 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -21,12 +21,13 @@ const pointValues = { LINK_DISCORD: 1, LINK_UD_DOMAIN_DEMOS: 3, LINK_UD_DOMAIN: 1, + LINK_HUMAN_PASSPORT: 1, } export class PointSystem { private static instance: PointSystem - private constructor() {} + private constructor() { } public static getInstance(): PointSystem { if (!PointSystem.instance) { @@ -45,6 +46,7 @@ export class PointSystem { [network: string]: string[] } linkedNomis: NomisWalletIdentity[] + linkedHumanPassport: { address: string; score: number; passingScore: boolean }[] }> { const identities = await IdentityManager.getIdentities(userId) const twitterIdentities = await IdentityManager.getWeb2Identities( @@ -159,7 +161,19 @@ export class PointSystem { } } - return { linkedWallets, linkedSocials, linkedUDDomains, linkedNomis } + const linkedHumanPassport: { address: string; score: number; passingScore: boolean }[] = [] + const humanPassportIdentities = await IdentityManager.getIdentities(userId, "humanpassport") + if (Array.isArray(humanPassportIdentities)) { + for (const hp of humanPassportIdentities) { + linkedHumanPassport.push({ + address: hp.address, + score: hp.score || 0, + passingScore: hp.passingScore || false, + }) + } + } + + return { linkedWallets, linkedSocials, linkedUDDomains, linkedNomis, linkedHumanPassport } } /** @@ -175,7 +189,7 @@ export class PointSystem { const gcrMainRepository = db.getDataSource().getRepository(GCRMain) let account = await gcrMainRepository.findOneBy({ pubkey: userIdStr }) - const { linkedWallets, linkedSocials, linkedUDDomains, linkedNomis } = + const { linkedWallets, linkedSocials, linkedUDDomains, linkedNomis, linkedHumanPassport } = await this.getUserIdentitiesFromGCR(userIdStr) if (!account) { @@ -212,6 +226,7 @@ export class PointSystem { }, udDomains: account.points.breakdown?.udDomains || {}, nomisScores: account.points.breakdown?.nomisScores || {}, + humanPassport: account.points.breakdown?.humanPassport || 0, referrals: account.points.breakdown?.referrals || 0, demosFollow: account.points.breakdown?.demosFollow || 0, }, @@ -219,6 +234,7 @@ export class PointSystem { linkedSocials, linkedUDDomains, linkedNomisIdentities: linkedNomis, + linkedHumanPassport, lastUpdated: account.points.lastUpdated || new Date(), flagged: account.flagged || null, flaggedReason: account.flaggedReason || null, @@ -236,7 +252,8 @@ export class PointSystem { | "socialAccounts" | "udDomains" | "nomisScores" - | "demosFollow", + | "demosFollow" + | "humanPassport", platform: string, referralCode?: string, twitterUserId?: string, @@ -311,6 +328,15 @@ export class PointSystem { ) appliedDelta = newDemosFollowPoints - oldDemosFollowPoints account.points.breakdown.demosFollow = newDemosFollowPoints + } else if (type === "humanPassport") { + const oldHumanPassportPoints = + account.points.breakdown.humanPassport || 0 + const newHumanPassportPoints = Math.max( + 0, + oldHumanPassportPoints + points, + ) + appliedDelta = newHumanPassportPoints - oldHumanPassportPoints + account.points.breakdown.humanPassport = newHumanPassportPoints } if (appliedDelta !== 0) { @@ -466,8 +492,8 @@ export class PointSystem { message: walletIsAlreadyLinked ? walletIsAlreadyLinkedMessage : hasExistingWalletOnChain - ? hasExistingWalletOnChainMessage - : "Points awarded for linking wallet", + ? hasExistingWalletOnChainMessage + : "Points awarded for linking wallet", }, require_reply: false, extra: {}, @@ -1237,9 +1263,8 @@ export class PointSystem { response: { pointsAwarded: pointValue, totalPoints: updatedPoints.totalPoints, - message: `Points awarded for linking ${ - isDemosDomain ? ".demos" : "UD" - } domain`, + message: `Points awarded for linking ${isDemosDomain ? ".demos" : "UD" + } domain`, }, require_reply: false, extra: {}, @@ -1320,9 +1345,8 @@ export class PointSystem { response: { pointsDeducted: pointValue, totalPoints: updatedPoints.totalPoints, - message: `Points deducted for unlinking ${ - isDemosDomain ? ".demos" : "UD" - } domain`, + message: `Points deducted for unlinking ${isDemosDomain ? ".demos" : "UD" + } domain`, }, require_reply: false, extra: {}, @@ -1549,6 +1573,126 @@ export class PointSystem { } } + /** + * Award points for linking a Human Passport + * @param userId The user's Demos address + * @param referralCode Optional referral code + * @returns RPCResponse + */ + async awardHumanPassportPoints( + userId: string, + referralCode?: string, + ): Promise { + try { + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) + + // Check if user already has Human Passport points + if ((userPointsWithIdentities.breakdown.humanPassport || 0) > 0) { + return { + result: 200, + response: { + pointsAwarded: 0, + totalPoints: userPointsWithIdentities.totalPoints, + message: "Human Passport points already awarded", + }, + require_reply: false, + extra: {}, + } + } + + await this.addPointsToGCR( + userId, + pointValues.LINK_HUMAN_PASSPORT, + "humanPassport", + "humanPassport", + referralCode, + ) + + const updatedPoints = await this.getUserPointsInternal(userId) + + return { + result: 200, + response: { + pointsAwarded: pointValues.LINK_HUMAN_PASSPORT, + totalPoints: updatedPoints.totalPoints, + message: "Points awarded for linking Human Passport", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + return { + result: 500, + response: "Error awarding Human Passport points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + + /** + * Deduct points for unlinking a Human Passport + * @param userId The user's Demos address + * @returns RPCResponse + */ + async deductHumanPassportPoints(userId: string): Promise { + try { + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) + + const currentHumanPassport = + userPointsWithIdentities.breakdown.humanPassport || 0 + if (currentHumanPassport <= 0) { + return { + result: 200, + response: { + pointsDeducted: 0, + totalPoints: userPointsWithIdentities.totalPoints, + message: "No Human Passport points to deduct", + }, + require_reply: false, + extra: {}, + } + } + + await this.addPointsToGCR( + userId, + -pointValues.LINK_HUMAN_PASSPORT, + "humanPassport", + "humanPassport", + ) + + const updatedPoints = await this.getUserPointsInternal(userId) + + return { + result: 200, + response: { + pointsDeducted: pointValues.LINK_HUMAN_PASSPORT, + totalPoints: updatedPoints.totalPoints, + message: "Points deducted for unlinking Human Passport", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + return { + result: 500, + response: "Error deducting Human Passport points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + private getNomisPointsByScore(score: number): number { const formattedScore = Number((score * 100).toFixed(0)) if (formattedScore >= 80) return 5 diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index 1467d20a..91750e4e 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -14,12 +14,14 @@ import Hashing from "@/libs/crypto/hashing" import { NomisWalletIdentity, PqcIdentityEdit, + SavedHumanPassportIdentity, SavedNomisIdentity, SavedXmIdentity, SavedUdIdentity, } from "@/model/entities/types/IdentityTypes" import log from "@/utilities/logger" import { IncentiveManager } from "./IncentiveManager" +import HumanPassportProvider from "@/libs/identity/tools/humanpassport" export default class GCRIdentityRoutines { // SECTION XM Identity Routines @@ -869,6 +871,20 @@ export default class GCRIdentityRoutines { simulate, ) break + case "humanpassportadd": + result = await this.applyHumanPassportIdentityAdd( + identityEdit, + gcrMainRepository, + simulate, + ) + break + case "humanpassportremove": + result = await this.applyHumanPassportIdentityRemove( + identityEdit, + gcrMainRepository, + simulate, + ) + break default: result = { success: false, @@ -1122,4 +1138,97 @@ export default class GCRIdentityRoutines { return { success: true, message: "Nomis identity removed" } } + + // SECTION Human Passport Identity Routines + + private static async applyHumanPassportIdentityAdd( + editOperation: any, + gcrMainRepository: Repository, + simulate: boolean, + ): Promise { + const clientData = editOperation.data as { address: string; verificationMethod: "api" | "onchain" } + const normalizedAddress = clientData.address.toLowerCase() + + // Fetch verified score from Human Passport API (uses cache from earlier verification) + const provider = HumanPassportProvider.getInstance() + const verification = await provider.verifyAddress(normalizedAddress) + + const savedIdentity: SavedHumanPassportIdentity = { + address: verification.address, + score: verification.score, + passingScore: verification.passingScore, + threshold: verification.threshold, + stamps: verification.stamps, + verificationMethod: clientData.verificationMethod, + verifiedAt: verification.verifiedAt, + expiresAt: verification.expirationTimestamp + ? new Date(verification.expirationTimestamp).getTime() + : null, + } + + const accountGCR = await ensureGCRForUser(editOperation.account) + + // Initialize humanpassport array if needed + if (!accountGCR.identities.humanpassport) { + accountGCR.identities.humanpassport = [] + } + + // Check if already linked + const existing = accountGCR.identities.humanpassport.find( + (hp: SavedHumanPassportIdentity) => + hp.address.toLowerCase() === normalizedAddress, + ) + + const isFirst = !existing + + // Upsert: remove existing then add new + accountGCR.identities.humanpassport = + accountGCR.identities.humanpassport.filter( + (hp: SavedHumanPassportIdentity) => + hp.address.toLowerCase() !== normalizedAddress, + ) + accountGCR.identities.humanpassport.push(savedIdentity) + + if (!simulate) { + await gcrMainRepository.save(accountGCR) + + if (isFirst) { + await IncentiveManager.humanPassportLinked( + accountGCR.pubkey, + editOperation.referralCode, + ) + } + } + + return { success: true, message: "Human Passport identity added" } + } + + private static async applyHumanPassportIdentityRemove( + editOperation: any, + gcrMainRepository: Repository, + simulate: boolean, + ): Promise { + const data = editOperation.data as { address: string } + const normalizedAddress = data.address.toLowerCase() + + const accountGCR = await ensureGCRForUser(editOperation.account) + + if (!accountGCR.identities.humanpassport) { + return { success: true, message: "No Human Passport identities to remove" } + } + + accountGCR.identities.humanpassport = + accountGCR.identities.humanpassport.filter( + (hp: SavedHumanPassportIdentity) => + hp.address.toLowerCase() !== normalizedAddress, + ) + + if (!simulate) { + await gcrMainRepository.save(accountGCR) + + await IncentiveManager.humanPassportUnlinked(accountGCR.pubkey) + } + + return { success: true, message: "Human Passport identity removed" } + } } diff --git a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts index 3b48087b..8b2f42ea 100644 --- a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts @@ -191,4 +191,26 @@ export class IncentiveManager { chain, ) } + + /** + * Hook to be called after Human Passport linking + */ + static async humanPassportLinked( + userId: string, + referralCode?: string, + ): Promise { + return await this.pointSystem.awardHumanPassportPoints( + userId, + referralCode, + ) + } + + /** + * Hook to be called after Human Passport unlinking + */ + static async humanPassportUnlinked( + userId: string, + ): Promise { + return await this.pointSystem.deductHumanPassportPoints(userId) + } } diff --git a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts index ab1e8f92..4e258a00 100644 --- a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts @@ -22,7 +22,8 @@ import { PqcIdentityAssignPayload } from "node_modules/@kynesyslabs/demosdk/buil import { hexToUint8Array, ucrypto } from "@kynesyslabs/demosdk/encryption" import { CrossChainTools } from "@/libs/identity/tools/crosschain" import { chainIds } from "sdk/localsdk/multichain/configs/chainIds" -import { NomisWalletIdentity } from "@/model/entities/types/IdentityTypes" +import { NomisWalletIdentity, SavedHumanPassportIdentity } from "@/model/entities/types/IdentityTypes" +import { HumanPassportProvider } from "@/libs/identity/tools/humanpassport" /* * Example of a payload for the gcr_routine method @@ -279,11 +280,10 @@ export default class IdentityManager { return { success: true, - message: `Signature proof${ - payloads.length > 1 ? "s" : "" - } verified. ${JSON.stringify( - payloads.map(p => p.algorithm), - )} identities assigned`, + message: `Signature proof${payloads.length > 1 ? "s" : "" + } verified. ${JSON.stringify( + payloads.map(p => p.algorithm), + )} identities assigned`, } } @@ -355,7 +355,7 @@ export default class IdentityManager { */ static async getIdentities( address: string, - key?: "xm" | "web2" | "pqc" | "ud" | "nomis", + key?: "xm" | "web2" | "pqc" | "ud" | "nomis" | "humanpassport", ): Promise { const gcr = await ensureGCRForUser(address) if (key) { @@ -368,4 +368,113 @@ export default class IdentityManager { static async getUDIdentities(address: string) { return await this.getIdentities(address, "ud") } + + // SECTION: Human Passport Identity + + /** + * Verify the payload for a Human Passport identity assign + * + * Validates the address and fetches the score from Human Passport API. + * Requires score >= 20 (passing threshold) to succeed. + * + * @param payload - The payload containing the address and signature + * @returns {success: boolean, message: string, data?: SavedHumanPassportIdentity} + */ + static async verifyHumanPassportPayload( + payload: { + address: string + signature?: string + verificationMethod: "api" | "onchain" + chainId?: number + referralCode?: string + }, + ): Promise<{ success: boolean; message: string; data?: SavedHumanPassportIdentity }> { + const { address, verificationMethod } = payload + + if (!address) { + return { + success: false, + message: "Invalid Human Passport payload: missing address", + } + } + + try { + // Verify score via Human Passport API + const provider = HumanPassportProvider.getInstance() + const verification = await provider.verifyAddress(address) + + if (!verification.passingScore) { + return { + success: false, + message: `Human Passport score ${verification.score} below threshold (${verification.threshold}). ` + + `User needs to verify more stamps at passport.human.tech. Transaction not applied.`, + } + } + + // Build saved identity + const savedIdentity: SavedHumanPassportIdentity = { + address: verification.address, + score: verification.score, + passingScore: verification.passingScore, + threshold: verification.threshold, + stamps: verification.stamps, + verificationMethod: verificationMethod, + chainId: payload.chainId, + verifiedAt: verification.verifiedAt, + expiresAt: verification.expirationTimestamp + ? new Date(verification.expirationTimestamp).getTime() + : null, + } + + log.info( + `[IdentityManager] Human Passport verified: ${address} ` + + `(score: ${verification.score}, stamps: ${verification.stamps.length})`, + ) + + return { + success: true, + message: `Human Passport identity verified with score ${verification.score}`, + data: savedIdentity, + } + } catch (error: any) { + log.error(`[IdentityManager] Human Passport verification failed: ${error.message}`) + return { + success: false, + message: error.message || "Failed to verify Human Passport identity", + } + } + } + + /** + * Get Human Passport identities for a Demos address + */ + static async getHumanPassportIdentities( + address: string, + ): Promise { + const identities = await this.getIdentities(address, "humanpassport") + return identities || [] + } + + /** + * Get Human Passport score for an address (fetches from API) + */ + static async getHumanPassportScore(address: string): Promise<{ + address: string + score: number + passingScore: boolean + stamps: string[] + } | null> { + try { + const provider = HumanPassportProvider.getInstance() + const verification = await provider.verifyAddress(address) + return { + address: verification.address, + score: verification.score, + passingScore: verification.passingScore, + stamps: verification.stamps, + } + } catch { + return null + } + } } diff --git a/src/libs/identity/tools/humanpassport.ts b/src/libs/identity/tools/humanpassport.ts new file mode 100644 index 00000000..0f4631e6 --- /dev/null +++ b/src/libs/identity/tools/humanpassport.ts @@ -0,0 +1,219 @@ +import axios, { AxiosInstance } from "axios" +import log from "@/utilities/logger" + +/** + * Human Passport score verification result + */ +export interface HumanPassportVerification { + address: string + score: number + passingScore: boolean + threshold: number + stamps: string[] + lastScoreTimestamp: string + expirationTimestamp: string | null + verifiedAt: number +} + +/** + * Raw API response from Human Passport + */ +interface RawScoreResponse { + address: string + score: string + passing_score: boolean + last_score_timestamp: string + expiration_timestamp: string | null + threshold: string + error: string | null + stamps: Record +} + +/** + * Cache entry for passport scores + */ +interface CachedScore { + data: HumanPassportVerification + fetchedAt: number +} + +const DEFAULT_BASE_URL = process.env.HUMAN_PASSPORT_API_URL || "https://api.passport.xyz" +const DEFAULT_SCORER_ID = process.env.HUMAN_PASSPORT_SCORER_ID || "" +const DEFAULT_API_KEY = process.env.HUMAN_PASSPORT_API_KEY || "" +const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour + +/** + * Human Passport API Client for Node + * + * Provides methods to verify Human Passport scores via the Stamps API v2. + * Implements caching to reduce API calls. + */ +export class HumanPassportProvider { + private static instance: HumanPassportProvider + private readonly http: AxiosInstance + private readonly scorerId: string + private readonly cache: Map = new Map() + + private constructor() { + this.scorerId = DEFAULT_SCORER_ID + + if (!DEFAULT_API_KEY) { + log.warn("[HumanPassportProvider] HUMAN_PASSPORT_API_KEY not set") + } + + if (!DEFAULT_SCORER_ID) { + log.warn("[HumanPassportProvider] HUMAN_PASSPORT_SCORER_ID not set") + } + + this.http = axios.create({ + baseURL: DEFAULT_BASE_URL, + timeout: 30000, + headers: { + "X-API-KEY": DEFAULT_API_KEY, + "Content-Type": "application/json", + }, + }) + } + + /** + * Get singleton instance + */ + static getInstance(): HumanPassportProvider { + if (!HumanPassportProvider.instance) { + HumanPassportProvider.instance = new HumanPassportProvider() + } + return HumanPassportProvider.instance + } + + /** + * Verify an address's Human Passport score + * + * @param address EVM address to verify + * @param forceRefresh Skip cache and fetch fresh data + * @returns Verification result with score and stamps + */ + async verifyAddress( + address: string, + forceRefresh = false, + ): Promise { + const normalizedAddress = address.toLowerCase() + + // Check cache + if (!forceRefresh) { + const cached = this.getFromCache(normalizedAddress) + if (cached) { + log.debug(`[HumanPassportProvider] Cache hit for ${normalizedAddress}`) + return cached + } + } + + // Fetch from API + const apiUrl = `/v2/stamps/${this.scorerId}/score/${normalizedAddress}` + + try { + const response = await this.http.get(apiUrl) + const verification = this.transformResponse(response.data) + + // Cache the result + this.setInCache(normalizedAddress, verification) + + return verification + } catch (error: any) { + log.error(`[HumanPassportProvider] API error for ${normalizedAddress}: ${error.message}`) + + if (error.response?.status === 404) { + throw new Error( + "User has not created a Human Passport. Direct them to passport.human.tech", + ) + } + + if (error.response?.status === 429) { + throw new Error("Human Passport API rate limit exceeded. Try again later.") + } + + throw error + } + } + + /** + * Check if an address is considered human (score >= threshold) + */ + async isHuman(address: string, threshold = 20): Promise { + try { + const verification = await this.verifyAddress(address) + return verification.score >= threshold + } catch { + return false + } + } + + /** + * Get score for an address (returns 0 if no passport) + */ + async getScore(address: string): Promise { + try { + const verification = await this.verifyAddress(address) + return verification.score + } catch { + return 0 + } + } + + /** + * Invalidate cache for an address + */ + invalidateCache(address: string): void { + this.cache.delete(address.toLowerCase()) + } + + /** + * Clear entire cache + */ + clearCache(): void { + this.cache.clear() + } + + /** + * Get from cache if valid + */ + private getFromCache(address: string): HumanPassportVerification | null { + const cached = this.cache.get(address) + if (!cached) return null + + // Check if expired + if (Date.now() - cached.fetchedAt > CACHE_TTL_MS) { + this.cache.delete(address) + return null + } + + return cached.data + } + + /** + * Set in cache + */ + private setInCache(address: string, data: HumanPassportVerification): void { + this.cache.set(address, { + data, + fetchedAt: Date.now(), + }) + } + + /** + * Transform raw API response to verification result + */ + private transformResponse(data: RawScoreResponse): HumanPassportVerification { + return { + address: data.address, + score: parseFloat(data.score) || 0, + passingScore: data.passing_score, + threshold: parseFloat(data.threshold) || 20, + stamps: Object.keys(data.stamps || {}), + lastScoreTimestamp: data.last_score_timestamp, + expirationTimestamp: data.expiration_timestamp, + verifiedAt: Date.now(), + } + } +} + +export default HumanPassportProvider diff --git a/src/libs/network/manageGCRRoutines.ts b/src/libs/network/manageGCRRoutines.ts index f2246e53..a4fe5b78 100644 --- a/src/libs/network/manageGCRRoutines.ts +++ b/src/libs/network/manageGCRRoutines.ts @@ -7,6 +7,7 @@ import ensureGCRForUser from "../blockchain/gcr/gcr_routines/ensureGCRForUser" import { Referrals } from "@/features/incentive/referrals" import GCR from "../blockchain/gcr/gcr" import { NomisIdentityProvider } from "@/libs/identity/providers/nomisIdentityProvider" +import HumanPassportProvider from "@/libs/identity/tools/humanpassport" import { BroadcastManager } from "../communications/broadcastManager" interface GCRRoutinePayload { @@ -146,6 +147,54 @@ export default async function manageGCRRoutines( break } + case "getHumanPassportScore": { + const options = params[0] + + // Support both positional (string) and object ({ address }) param styles + const address = + typeof options === "string" ? options : options?.address + // Always force refresh to get latest score from API + const forceRefresh = true + + if (!address) { + response.result = 400 + response.response = null + response.extra = { error: "address is required" } + break + } + + try { + const provider = HumanPassportProvider.getInstance() + response.response = await provider.verifyAddress( + address, + forceRefresh, + ) + } catch (error) { + response.result = 400 + response.response = null + response.extra = { + error: + error instanceof Error ? error.message : String(error), + } + } + break + } + + case "getHumanPassportIdentities": { + try { + response.response = + await IdentityManager.getHumanPassportIdentities(sender) + } catch (error) { + response.result = 400 + response.response = null + response.extra = { + error: + error instanceof Error ? error.message : String(error), + } + } + break + } + case "syncNewBlock": { response.response = await BroadcastManager.handleNewBlock( sender, diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts index df967088..324ea8ca 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -3,6 +3,7 @@ import { InferFromSignaturePayload, Web2CoreTargetIdentityPayload, UDIdentityAssignPayload, + HumanPassportIdentityData, } from "@kynesyslabs/demosdk/abstraction" import { verifyWeb2Proof } from "@/libs/abstraction" import { Transaction } from "@kynesyslabs/demosdk/types" @@ -100,11 +101,16 @@ export default async function handleIdentityRequest( return await IdentityManager.verifyNomisPayload( payload.payload as NomisWalletIdentity, ) + case "humanpassport_identity_assign": + return await IdentityManager.verifyHumanPassportPayload( + payload.payload as HumanPassportIdentityData, + ) case "xm_identity_remove": case "pqc_identity_remove": case "web2_identity_remove": case "nomis_identity_remove": case "ud_identity_remove": + case "humanpassport_identity_remove": return { success: true, message: "Identity removed", diff --git a/src/model/entities/GCRv2/GCR_Main.ts b/src/model/entities/GCRv2/GCR_Main.ts index d3154f28..50bef3d6 100644 --- a/src/model/entities/GCRv2/GCR_Main.ts +++ b/src/model/entities/GCRv2/GCR_Main.ts @@ -41,6 +41,7 @@ export class GCRMain { points: number }> nomisScores: { [chain: string]: number } + humanPassport?: number } lastUpdated: Date } diff --git a/src/model/entities/types/IdentityTypes.ts b/src/model/entities/types/IdentityTypes.ts index b13ae73d..8b68ddac 100644 --- a/src/model/entities/types/IdentityTypes.ts +++ b/src/model/entities/types/IdentityTypes.ts @@ -44,6 +44,34 @@ export interface SavedNomisIdentity { } } +/** + * Human Passport (formerly Gitcoin Passport) identity saved in the GCR + * + * Stores verified humanity score from Human Passport's Stamps API. + * Users verify stamps (Google, Discord, GitHub, etc.) to build a score. + * Score >= 20 is considered "human" (Sybil resistant). + */ +export interface SavedHumanPassportIdentity { + /** EVM address */ + address: string + /** Humanity score (0-100+) */ + score: number + /** Whether score met threshold at verification time */ + passingScore: boolean + /** Score threshold used (default: 20) */ + threshold: number + /** List of verified stamp provider names */ + stamps: string[] + /** Verification method: "api" or "onchain" */ + verificationMethod: "api" | "onchain" + /** Chain ID for onchain verification */ + chainId?: number + /** Timestamp when verified */ + verifiedAt: number + /** Timestamp when score expires (if applicable) */ + expiresAt: number | null +} + /** * The PQC identity saved in the GCR */ @@ -105,4 +133,5 @@ export type StoredIdentities = { [subchain: string]: SavedNomisIdentity[] } } + humanpassport?: SavedHumanPassportIdentity[] // Human Passport (Proof of Personhood) identities } From 3eb8f93942155fc22e7a542a26c5fa4b9a90fa45 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Tue, 3 Feb 2026 12:26:21 +0400 Subject: [PATCH 2/3] Fixed qodo comments --- .env.example | 1 + src/features/incentive/PointSystem.ts | 34 +++-- .../gcr/gcr_routines/GCRIdentityRoutines.ts | 138 +++++++++++------- src/libs/identity/tools/humanpassport.ts | 8 +- src/model/entities/types/IdentityTypes.ts | 4 +- 5 files changed, 116 insertions(+), 69 deletions(-) diff --git a/.env.example b/.env.example index 104d11df..136c7602 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ DISCORD_BOT_TOKEN= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= +HUMAN_PASSPORT_API_URL=https://api.passport.xyz HUMAN_PASSPORT_API_KEY= HUMAN_PASSPORT_SCORER_ID= diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 84fd70ad..4b543452 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -161,17 +161,12 @@ export class PointSystem { } } - const linkedHumanPassport: { address: string; score: number; passingScore: boolean }[] = [] - const humanPassportIdentities = await IdentityManager.getIdentities(userId, "humanpassport") - if (Array.isArray(humanPassportIdentities)) { - for (const hp of humanPassportIdentities) { - linkedHumanPassport.push({ - address: hp.address, - score: hp.score || 0, - passingScore: hp.passingScore || false, - }) - } - } + const humanPassportIdentities: any[] = (await IdentityManager.getIdentities(userId, "humanpassport")) || [] + const linkedHumanPassport = humanPassportIdentities.map(hp => ({ + address: hp.address, + score: hp.score || 0, + passingScore: hp.passingScore || false, + })) return { linkedWallets, linkedSocials, linkedUDDomains, linkedNomis, linkedHumanPassport } } @@ -1584,6 +1579,23 @@ export class PointSystem { referralCode?: string, ): Promise { try { + // Verify the Human Passport identity is actually linked + const account = await ensureGCRForUser(userId) + const hpIdentities = account.identities.humanpassport || [] + + if (hpIdentities.length === 0) { + return { + result: 400, + response: { + pointsAwarded: 0, + totalPoints: account.points.totalPoints || 0, + message: "Human Passport not linked", + }, + require_reply: false, + extra: {}, + } + } + const userPointsWithIdentities = await this.getUserPointsInternal( userId, ) diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index 91750e4e..cae5283d 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -903,17 +903,31 @@ export default class GCRIdentityRoutines { | "telegram" | "discord" | "ud" - | "nomis", + | "nomis" + | "humanpassport", data: { userId?: string // for twitter/github/discord chain?: string // for web3 subchain?: string // for web3 - address?: string // for web3 + address?: string // for web3/humanpassport domain?: string // for ud }, gcrMainRepository: Repository, currentAccount?: string, ): Promise { + if (type === "humanpassport") { + const result = await gcrMainRepository + .createQueryBuilder("gcr") + .where( + "EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(gcr.identities->'humanpassport', '[]'::jsonb)) AS hp WHERE LOWER(hp->>'address') = LOWER(:address))", + { address: data.address }, + ) + .andWhere("gcr.pubkey != :currentAccount", { currentAccount }) + .getOne() + + return !result + } + if (type !== "web3" && type !== "ud" && type !== "nomis") { // Handle web2 identity types: twitter, github, telegram, discord const queryTemplate = ` @@ -1146,61 +1160,66 @@ export default class GCRIdentityRoutines { gcrMainRepository: Repository, simulate: boolean, ): Promise { - const clientData = editOperation.data as { address: string; verificationMethod: "api" | "onchain" } - const normalizedAddress = clientData.address.toLowerCase() - - // Fetch verified score from Human Passport API (uses cache from earlier verification) - const provider = HumanPassportProvider.getInstance() - const verification = await provider.verifyAddress(normalizedAddress) - - const savedIdentity: SavedHumanPassportIdentity = { - address: verification.address, - score: verification.score, - passingScore: verification.passingScore, - threshold: verification.threshold, - stamps: verification.stamps, - verificationMethod: clientData.verificationMethod, - verifiedAt: verification.verifiedAt, - expiresAt: verification.expirationTimestamp - ? new Date(verification.expirationTimestamp).getTime() - : null, - } - - const accountGCR = await ensureGCRForUser(editOperation.account) - - // Initialize humanpassport array if needed - if (!accountGCR.identities.humanpassport) { - accountGCR.identities.humanpassport = [] - } + try { + const clientData = editOperation.data as { address: string; verificationMethod: "api" | "onchain" } + const normalizedAddress = clientData.address.toLowerCase() + + // Fetch verified score from Human Passport API (uses cache from earlier verification) + const provider = HumanPassportProvider.getInstance() + const verification = await provider.verifyAddress(normalizedAddress) + + const savedIdentity: SavedHumanPassportIdentity = { + address: verification.address, + score: verification.score, + passingScore: verification.passingScore, + threshold: verification.threshold, + stamps: verification.stamps, + verificationMethod: clientData.verificationMethod, + verifiedAt: verification.verifiedAt, + expiresAt: verification.expirationTimestamp + ? new Date(verification.expirationTimestamp).getTime() + : null, + } - // Check if already linked - const existing = accountGCR.identities.humanpassport.find( - (hp: SavedHumanPassportIdentity) => - hp.address.toLowerCase() === normalizedAddress, - ) + const accountGCR = await ensureGCRForUser(editOperation.account) - const isFirst = !existing + // Initialize humanpassport array if needed + if (!accountGCR.identities.humanpassport) { + accountGCR.identities.humanpassport = [] + } - // Upsert: remove existing then add new - accountGCR.identities.humanpassport = - accountGCR.identities.humanpassport.filter( - (hp: SavedHumanPassportIdentity) => - hp.address.toLowerCase() !== normalizedAddress, + // Global uniqueness check across all accounts + const isFirst = await this.isFirstConnection( + "humanpassport", + { address: normalizedAddress }, + gcrMainRepository, + editOperation.account, ) - accountGCR.identities.humanpassport.push(savedIdentity) - if (!simulate) { - await gcrMainRepository.save(accountGCR) - - if (isFirst) { - await IncentiveManager.humanPassportLinked( - accountGCR.pubkey, - editOperation.referralCode, + // Upsert: remove existing then add new + accountGCR.identities.humanpassport = + accountGCR.identities.humanpassport.filter( + (hp: SavedHumanPassportIdentity) => + hp.address.toLowerCase() !== normalizedAddress, ) + accountGCR.identities.humanpassport.push(savedIdentity) + + if (!simulate) { + await gcrMainRepository.save(accountGCR) + + if (isFirst) { + await IncentiveManager.humanPassportLinked( + accountGCR.pubkey, + editOperation.referralCode, + ) + } } - } - return { success: true, message: "Human Passport identity added" } + return { success: true, message: "Human Passport identity added" } + } catch (error: any) { + log.error(`[GCRIdentityRoutines] Failed to add Human Passport identity: ${error.message}`) + return { success: false, message: error.message || "Failed to add Human Passport identity" } + } } private static async applyHumanPassportIdentityRemove( @@ -1211,10 +1230,25 @@ export default class GCRIdentityRoutines { const data = editOperation.data as { address: string } const normalizedAddress = data.address.toLowerCase() - const accountGCR = await ensureGCRForUser(editOperation.account) + const accountGCR = await gcrMainRepository.findOneBy({ + pubkey: editOperation.account, + }) + + if (!accountGCR) { + return { success: false, message: "Account not found" } + } + + if (!accountGCR.identities.humanpassport || accountGCR.identities.humanpassport.length === 0) { + return { success: false, message: "No Human Passport identities found" } + } - if (!accountGCR.identities.humanpassport) { - return { success: true, message: "No Human Passport identities to remove" } + const addressExists = accountGCR.identities.humanpassport.some( + (hp: SavedHumanPassportIdentity) => + hp.address.toLowerCase() === normalizedAddress, + ) + + if (!addressExists) { + return { success: false, message: "Identity not found" } } accountGCR.identities.humanpassport = diff --git a/src/libs/identity/tools/humanpassport.ts b/src/libs/identity/tools/humanpassport.ts index 0f4631e6..8da665c9 100644 --- a/src/libs/identity/tools/humanpassport.ts +++ b/src/libs/identity/tools/humanpassport.ts @@ -55,16 +55,16 @@ export class HumanPassportProvider { private readonly cache: Map = new Map() private constructor() { - this.scorerId = DEFAULT_SCORER_ID - if (!DEFAULT_API_KEY) { - log.warn("[HumanPassportProvider] HUMAN_PASSPORT_API_KEY not set") + throw new Error("HUMAN_PASSPORT_API_KEY is not set in environment variables") } if (!DEFAULT_SCORER_ID) { - log.warn("[HumanPassportProvider] HUMAN_PASSPORT_SCORER_ID not set") + throw new Error("HUMAN_PASSPORT_SCORER_ID is not set in environment variables") } + this.scorerId = DEFAULT_SCORER_ID + this.http = axios.create({ baseURL: DEFAULT_BASE_URL, timeout: 30000, diff --git a/src/model/entities/types/IdentityTypes.ts b/src/model/entities/types/IdentityTypes.ts index 8b68ddac..2e33796f 100644 --- a/src/model/entities/types/IdentityTypes.ts +++ b/src/model/entities/types/IdentityTypes.ts @@ -66,9 +66,9 @@ export interface SavedHumanPassportIdentity { verificationMethod: "api" | "onchain" /** Chain ID for onchain verification */ chainId?: number - /** Timestamp when verified */ + /** Milliseconds since Unix epoch when verified (converted from API date string by identityManager/GCRIdentityRoutines) */ verifiedAt: number - /** Timestamp when score expires (if applicable) */ + /** Milliseconds since Unix epoch when score expires, or null if not applicable (converted from API date string by identityManager/GCRIdentityRoutines) */ expiresAt: number | null } From c1052ca7ad65320df33ec947509015741d337ac3 Mon Sep 17 00:00:00 2001 From: SergeyG-Solicy Date: Tue, 3 Feb 2026 14:25:40 +0400 Subject: [PATCH 3/3] Fixed qodo comments --- src/libs/blockchain/gcr/gcr_routines/identityManager.ts | 4 +++- src/libs/identity/tools/humanpassport.ts | 9 ++++++++- src/libs/network/manageGCRRoutines.ts | 4 +++- .../routines/transactions/handleIdentityRequest.ts | 7 +++++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts index 4e258a00..a6de32a8 100644 --- a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts @@ -378,6 +378,7 @@ export default class IdentityManager { * Requires score >= 20 (passing threshold) to succeed. * * @param payload - The payload containing the address and signature + * @param sender - The transaction sender's address (for binding verification) * @returns {success: boolean, message: string, data?: SavedHumanPassportIdentity} */ static async verifyHumanPassportPayload( @@ -388,6 +389,7 @@ export default class IdentityManager { chainId?: number referralCode?: string }, + sender: string, ): Promise<{ success: boolean; message: string; data?: SavedHumanPassportIdentity }> { const { address, verificationMethod } = payload @@ -407,7 +409,7 @@ export default class IdentityManager { return { success: false, message: `Human Passport score ${verification.score} below threshold (${verification.threshold}). ` + - `User needs to verify more stamps at passport.human.tech. Transaction not applied.`, + `User needs to verify more stamps at https://app.passport.xyz/. Transaction not applied.`, } } diff --git a/src/libs/identity/tools/humanpassport.ts b/src/libs/identity/tools/humanpassport.ts index 8da665c9..a4714984 100644 --- a/src/libs/identity/tools/humanpassport.ts +++ b/src/libs/identity/tools/humanpassport.ts @@ -96,6 +96,13 @@ export class HumanPassportProvider { address: string, forceRefresh = false, ): Promise { + // Early validation guard for API credentials + if (!this.scorerId || !this.http.defaults.headers?.["X-API-KEY"]) { + throw new Error( + "Human Passport API credentials missing: set HUMAN_PASSPORT_API_KEY and HUMAN_PASSPORT_SCORER_ID" + ) + } + const normalizedAddress = address.toLowerCase() // Check cache @@ -123,7 +130,7 @@ export class HumanPassportProvider { if (error.response?.status === 404) { throw new Error( - "User has not created a Human Passport. Direct them to passport.human.tech", + "User has not created a Human Passport. Direct them to https://app.passport.xyz/", ) } diff --git a/src/libs/network/manageGCRRoutines.ts b/src/libs/network/manageGCRRoutines.ts index a4fe5b78..7367ee23 100644 --- a/src/libs/network/manageGCRRoutines.ts +++ b/src/libs/network/manageGCRRoutines.ts @@ -165,10 +165,12 @@ export default async function manageGCRRoutines( try { const provider = HumanPassportProvider.getInstance() - response.response = await provider.verifyAddress( + const verification = await provider.verifyAddress( address, forceRefresh, ) + // Return only the numeric score (method name implies a number, not full object) + response.response = verification.score } catch (error) { response.result = 400 response.response = null diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts index 324ea8ca..c413c592 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -101,10 +101,13 @@ export default async function handleIdentityRequest( return await IdentityManager.verifyNomisPayload( payload.payload as NomisWalletIdentity, ) - case "humanpassport_identity_assign": + case "humanpassport_identity_assign": { + const hpPayload = payload.payload as HumanPassportIdentityData return await IdentityManager.verifyHumanPassportPayload( - payload.payload as HumanPassportIdentityData, + hpPayload, + sender, ) + } case "xm_identity_remove": case "pqc_identity_remove": case "web2_identity_remove":