From 15d9cc462e2925528571b0ef0b78236909773d91 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Thu, 27 Mar 2025 21:55:49 +0100 Subject: [PATCH 01/14] create point system class --- src/features/incentive/IncentiveController.ts | 101 ++++++ src/features/incentive/PointSystem.ts | 300 ++++++++++++++++++ src/libs/network/manageGCRRoutines.ts | 37 +++ src/libs/network/server_rpc.ts | 3 +- src/model/entities/UserPoints.ts | 42 +++ 5 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 src/features/incentive/IncentiveController.ts create mode 100644 src/features/incentive/PointSystem.ts create mode 100644 src/model/entities/UserPoints.ts diff --git a/src/features/incentive/IncentiveController.ts b/src/features/incentive/IncentiveController.ts new file mode 100644 index 000000000..a5986bdf1 --- /dev/null +++ b/src/features/incentive/IncentiveController.ts @@ -0,0 +1,101 @@ +import { RPCResponse } from "@kynesyslabs/demosdk/types" +import { PointSystem } from "./PointSystem" + +export class IncentiveController { + private static instance: IncentiveController + private pointSystem: PointSystem + + private constructor() { + this.pointSystem = PointSystem.getInstance() + } + + public static getInstance(): IncentiveController { + if (!IncentiveController.instance) { + IncentiveController.instance = new IncentiveController() + } + return IncentiveController.instance + } + + /** + * Handle incentive-related RPC requests + */ + async handleIncentiveRequest( + sender: string, + method: string, + params: any[], + ): Promise { + console.log(`[IncentiveController] Handling method: ${method}`) + + switch (method) { + case "getPoints": { + const userPoints = await this.pointSystem.getUserPoints(sender) + return { + result: 200, + response: userPoints, + require_reply: false, + extra: {}, + } + } + + case "identityCreated": + return await this.pointSystem.awardIdentityCreationPoints( + sender, + ) + + case "walletLinked": + return await this.pointSystem.awardWeb3WalletPoints( + sender, + params[0], // walletAddress + params[1], // chain + ) + + case "twitterLinked": + return await this.pointSystem.awardTwitterPoints( + sender, + params[0], // twitterHandle + ) + + default: + return { + result: 400, + response: "Unknown method", + require_reply: false, + extra: { + message: `Method ${method} not supported`, + }, + } + } + } + + /** + * Hook to be called after identity creation + */ + async onIdentityCreated(userId: string): Promise { + await this.pointSystem.awardIdentityCreationPoints(userId) + } + + /** + * Hook to be called after Web3 wallet linking + */ + async onWalletLinked( + userId: string, + walletAddress: string, + chain: string, + ): Promise { + await this.pointSystem.awardWeb3WalletPoints( + userId, + walletAddress, + chain, + ) + } + + /** + * Hook to be called after Twitter linking + */ + async onTwitterLinked( + userId: string, + twitterHandle: string, + ): Promise { + await this.pointSystem.awardTwitterPoints(userId, twitterHandle) + } +} diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts new file mode 100644 index 000000000..e71ba886a --- /dev/null +++ b/src/features/incentive/PointSystem.ts @@ -0,0 +1,300 @@ +import Datasource from "../../model/datasource" +import { RPCResponse } from "@kynesyslabs/demosdk/types" + +// Define point values for different actions +const pointValues = { + CREATE_IDENTITY: 100, + LINK_WEB3_WALLET: 50, // per wallet + LINK_TWITTER: 75, + LINK_GITHUB: 75, + LINK_DISCORD: 75, // TODO: Implement Discord integration +} + +// Define reputation levels +const reputationLevels = [ + { name: "Newcomer", minPoints: 0 }, + { name: "Explorer", minPoints: 100 }, + { name: "Contributor", minPoints: 250 }, + { name: "Builder", minPoints: 500 }, + { name: "Pioneer", minPoints: 1000 }, +] + +export interface UserPoints { + userId: string + totalPoints: number + breakdown: { + identityCreation: number + web3Wallets: number + socialAccounts: number + } + linkedWallets: string[] + linkedSocials: { + twitter?: string + github?: string + discord?: string + } + reputationLevel: string + lastUpdated: Date +} + +export class PointSystem { + private static instance: PointSystem + + private constructor() {} + + public static getInstance(): PointSystem { + if (!PointSystem.instance) { + PointSystem.instance = new PointSystem() + } + return PointSystem.instance + } + + /** + * Award points for creating a Demos identity + */ + async awardIdentityCreationPoints(userId: string): Promise { + try { + // Get or create user points record + const userPoints = await this.getUserPoints(userId) + + // Only award points if not already awarded + if (userPoints.breakdown.identityCreation === 0) { + userPoints.breakdown.identityCreation = + pointValues.CREATE_IDENTITY + userPoints.totalPoints += pointValues.CREATE_IDENTITY + + await this.saveUserPoints(userPoints) + + return { + result: 200, + response: { + pointsAwarded: pointValues.CREATE_IDENTITY, + totalPoints: userPoints.totalPoints, + message: "Points awarded for identity creation", + }, + require_reply: false, + extra: {}, + } + } + + return { + result: 200, + response: { + pointsAwarded: 0, + totalPoints: userPoints.totalPoints, + message: "Points already awarded for identity creation", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + console.error("Error awarding identity points:", error) + return { + result: 500, + response: "Error awarding points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + + /** + * Award points for linking a Web3 wallet + */ + async awardWeb3WalletPoints( + userId: string, + walletAddress: string, + chain: string, + ): Promise { + try { + // Get user points record + const userPoints = await this.getUserPoints(userId) + + // Check if this wallet is already linked + if ( + userPoints.linkedWallets.includes(`${chain}:${walletAddress}`) + ) { + return { + result: 200, + response: { + pointsAwarded: 0, + totalPoints: userPoints.totalPoints, + message: "Wallet already linked, no new points awarded", + }, + require_reply: false, + extra: {}, + } + } + + // Award points for the new wallet + userPoints.linkedWallets.push(`${chain}:${walletAddress}`) + userPoints.breakdown.web3Wallets += pointValues.LINK_WEB3_WALLET + userPoints.totalPoints += pointValues.LINK_WEB3_WALLET + + await this.saveUserPoints(userPoints) + + return { + result: 200, + response: { + pointsAwarded: pointValues.LINK_WEB3_WALLET, + totalPoints: userPoints.totalPoints, + message: "Points awarded for linking Web3 wallet", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + console.error("Error awarding wallet points:", error) + return { + result: 500, + response: "Error awarding points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + + /** + * Award points for linking a Twitter account + */ + async awardTwitterPoints( + userId: string, + twitterHandle: string, + ): Promise { + try { + // Get user points record + const userPoints = await this.getUserPoints(userId) + + // Check if Twitter is already linked + if (userPoints.linkedSocials.twitter) { + return { + result: 200, + response: { + pointsAwarded: 0, + totalPoints: userPoints.totalPoints, + message: + "Twitter already linked, no new points awarded", + }, + require_reply: false, + extra: {}, + } + } + + // Award points for linking Twitter + userPoints.linkedSocials.twitter = twitterHandle + userPoints.breakdown.socialAccounts += pointValues.LINK_TWITTER + userPoints.totalPoints += pointValues.LINK_TWITTER + + await this.saveUserPoints(userPoints) + + return { + result: 200, + response: { + pointsAwarded: pointValues.LINK_TWITTER, + totalPoints: userPoints.totalPoints, + message: "Points awarded for linking Twitter", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + console.error("Error awarding Twitter points:", error) + return { + result: 500, + response: "Error awarding points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + + /** + * Get user's current points and reputation + */ + async getUserPoints(userId: string): Promise { + // TODO: Implement database storage for points + // For now, we'll create a new record if none exists + + // In a real implementation, this would fetch from the database + const defaultPoints: UserPoints = { + userId, + totalPoints: 0, + breakdown: { + identityCreation: 0, + web3Wallets: 0, + socialAccounts: 0, + }, + linkedWallets: [], + linkedSocials: {}, + reputationLevel: reputationLevels[0].name, + lastUpdated: new Date(), + } + + // Calculate reputation level + const reputationLevel = this.calculateReputationLevel( + defaultPoints.totalPoints, + ) + defaultPoints.reputationLevel = reputationLevel + + return defaultPoints + } + + /** + * Calculate reputation level based on points + */ + private calculateReputationLevel(points: number): string { + let level = reputationLevels[0].name + + for (const repLevel of reputationLevels) { + if (points >= repLevel.minPoints) { + level = repLevel.name + } else { + break + } + } + + return level + } + + /** + * Save user points to database + */ + private async saveUserPoints(userPoints: UserPoints): Promise { + // TODO: Implement database storage + // For now, we'll just update the reputation level and timestamp + userPoints.reputationLevel = this.calculateReputationLevel( + userPoints.totalPoints, + ) + userPoints.lastUpdated = new Date() + + console.log( + `[PointSystem] Updated points for user ${userPoints.userId}: ${userPoints.totalPoints} points`, + ) + } + + /** + * Check if a user has bot-like behavior + * This is a placeholder for more sophisticated bot detection + */ + async checkForBotBehavior(userId: string): Promise { + // TODO: Implement actual bot detection + // For now, we'll just return false (not a bot) + + // In a real implementation, this would check: + // 1. Account age + // 2. Activity patterns + // 3. Social account verification status + // 4. Reputation on linked platforms + + return false + } +} diff --git a/src/libs/network/manageGCRRoutines.ts b/src/libs/network/manageGCRRoutines.ts index ed3d95e91..42e6cf47a 100644 --- a/src/libs/network/manageGCRRoutines.ts +++ b/src/libs/network/manageGCRRoutines.ts @@ -2,6 +2,7 @@ import { RPCResponse } from "@kynesyslabs/demosdk/types" import _ from "lodash" import IdentityManager from "../blockchain/gcr/gcr_routines/identityManager" import { emptyResponse } from "./server_rpc" +import { IncentiveController } from "@/features/incentive/IncentiveController" interface GCRRoutinePayload { method: string @@ -16,6 +17,18 @@ export default async function manageGCRRoutines( response.result = 200 // Handle the payload const { method, params } = payload + + // Check if this is an incentive-related request + if (method.startsWith("incentive_")) { + // Remove the "incentive_" prefix and pass to incentive controller + const incentiveMethod = method.substring(10) + return await IncentiveController.getInstance().handleIncentiveRequest( + sender, + incentiveMethod, + params, + ) + } + switch (method) { // SECTION XM Identity Management @@ -27,10 +40,24 @@ export default async function manageGCRRoutines( case "identity_assign_from_signature": try { + // TODO: This method is deprecated. Identities should be updated as transactions. + // Consider replacing with a transaction-based approach response = await IdentityManager.inferIdentityFromSignature( sender, params[0], ) + + // If successful, award points for wallet linking + if (response.result === 200) { + const walletAddress = + params[0].target_identity.targetAddress + const chain = params[0].target_identity.chain + await IncentiveController.getInstance().onWalletLinked( + sender, + walletAddress, + chain, + ) + } } catch (error) { console.error(error) response.result = 400 @@ -69,6 +96,16 @@ export default async function manageGCRRoutines( "twitter", params[0], ) + // If successful, award points for Twitter linking + if (response.result === 200) { + // Extract Twitter handle from the proof + // This is a simplification - in reality, you'd need to parse the proof + const twitterHandle = "user_" + sender.substring(0, 8) + await IncentiveController.getInstance().onTwitterLinked( + sender, + twitterHandle, + ) + } break default: diff --git a/src/libs/network/server_rpc.ts b/src/libs/network/server_rpc.ts index 60f4999e1..15ada8c93 100644 --- a/src/libs/network/server_rpc.ts +++ b/src/libs/network/server_rpc.ts @@ -36,7 +36,8 @@ import { parseWeb2ProxyRequest } from "../utils/web2RequestUtils" // Reading the port from sharedState -const noAuthMethods = ["nodeCall"] +// TODO: Add Proper authentication +const noAuthMethods = ["nodeCall", "gcr_routine"] export const emptyResponse: RPCResponse = { result: 0, diff --git a/src/model/entities/UserPoints.ts b/src/model/entities/UserPoints.ts new file mode 100644 index 000000000..58b55c58b --- /dev/null +++ b/src/model/entities/UserPoints.ts @@ -0,0 +1,42 @@ +import { + Entity, + Column, + PrimaryColumn, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm" + +@Entity() +export class UserPointsEntity { + @PrimaryColumn() + userId: string + + @Column({ type: "integer", default: 0 }) + totalPoints: number + + @Column({ type: "jsonb", default: {} }) + breakdown: { + identityCreation: number + web3Wallets: number + socialAccounts: number + } + + @Column({ type: "jsonb", default: [] }) + linkedWallets: string[] + + @Column({ type: "jsonb", default: {} }) + linkedSocials: { + twitter?: string + github?: string + discord?: string + } + + @Column() + reputationLevel: string + + @CreateDateColumn() + createdAt: Date + + @UpdateDateColumn() + updatedAt: Date +} From 6f079ae2212b79ca1c9779f1911d1b8fadbe14ae Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Mon, 31 Mar 2025 16:18:01 +0200 Subject: [PATCH 02/14] update poinsystem and DB to be in line with designs --- src/features/incentive/IncentiveController.ts | 115 ++++++--- src/features/incentive/PointSystem.ts | 242 ++++++++---------- src/libs/network/manageGCRRoutines.ts | 26 +- src/model/datasource.ts | 4 +- src/model/entities/UserPoints.ts | 7 +- 5 files changed, 212 insertions(+), 182 deletions(-) diff --git a/src/features/incentive/IncentiveController.ts b/src/features/incentive/IncentiveController.ts index a5986bdf1..2ec11b3c6 100644 --- a/src/features/incentive/IncentiveController.ts +++ b/src/features/incentive/IncentiveController.ts @@ -25,55 +25,90 @@ export class IncentiveController { params: any[], ): Promise { console.log(`[IncentiveController] Handling method: ${method}`) + console.log(`[IncentiveController] With sender: ${sender}`) + console.log(`[IncentiveController] With params:`, params) - switch (method) { - case "getPoints": { - const userPoints = await this.pointSystem.getUserPoints(sender) - return { - result: 200, - response: userPoints, - require_reply: false, - extra: {}, + try { + switch (method) { + case "getPoints": { + console.log( + `[IncentiveController] Getting points for user: ${sender}`, + ) + const userPoints = await this.pointSystem.getUserPoints( + sender, + ) + return { + result: 200, + response: userPoints, + require_reply: false, + extra: {}, + } } - } - case "identityCreated": - return await this.pointSystem.awardIdentityCreationPoints( - sender, - ) + case "walletLinked": + if (params.length < 2) { + console.error( + `[IncentiveController] Invalid params for walletLinked. Expected 2, got ${params.length}`, + ) + return { + result: 400, + response: "Missing parameters for wallet linking", + require_reply: false, + extra: { + expected: 2, + received: params.length, + }, + } + } - case "walletLinked": - return await this.pointSystem.awardWeb3WalletPoints( - sender, - params[0], // walletAddress - params[1], // chain - ) + console.log( + `[IncentiveController] Linking wallet for user: ${sender}, wallet: ${params[0]}, chain: ${params[1]}`, + ) + return await this.pointSystem.awardWeb3WalletPoints( + sender, + params[0], // walletAddress + params[1], // chain + ) - case "twitterLinked": - return await this.pointSystem.awardTwitterPoints( - sender, - params[0], // twitterHandle - ) + case "twitterLinked": + console.log( + `[IncentiveController] Linking Twitter for user: ${sender}, handle: ${params[0]}`, + ) + return await this.pointSystem.awardTwitterPoints( + sender, + params[0], // twitterHandle + ) - default: - return { - result: 400, - response: "Unknown method", - require_reply: false, - extra: { - message: `Method ${method} not supported`, - }, - } + default: + console.warn( + `[IncentiveController] Unknown method: ${method}`, + ) + return { + result: 400, + response: "Unknown method", + require_reply: false, + extra: { + message: `Method ${method} not supported`, + }, + } + } + } catch (error) { + console.error( + `[IncentiveController] Error handling method ${method}:`, + error, + ) + return { + result: 500, + response: "Internal server error", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } } } - /** - * Hook to be called after identity creation - */ - async onIdentityCreated(userId: string): Promise { - await this.pointSystem.awardIdentityCreationPoints(userId) - } - /** * Hook to be called after Web3 wallet linking */ diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index e71ba886a..3ba246fcf 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -1,39 +1,24 @@ import Datasource from "../../model/datasource" import { RPCResponse } from "@kynesyslabs/demosdk/types" +import { UserPointsEntity } from "../../model/entities/UserPoints" // Define point values for different actions const pointValues = { - CREATE_IDENTITY: 100, - LINK_WEB3_WALLET: 50, // per wallet - LINK_TWITTER: 75, - LINK_GITHUB: 75, - LINK_DISCORD: 75, // TODO: Implement Discord integration + LINK_WEB3_WALLET: 2, // per wallet + LINK_TWITTER: 5, } -// Define reputation levels -const reputationLevels = [ - { name: "Newcomer", minPoints: 0 }, - { name: "Explorer", minPoints: 100 }, - { name: "Contributor", minPoints: 250 }, - { name: "Builder", minPoints: 500 }, - { name: "Pioneer", minPoints: 1000 }, -] - export interface UserPoints { userId: string totalPoints: number breakdown: { - identityCreation: number web3Wallets: number socialAccounts: number } linkedWallets: string[] linkedSocials: { twitter?: string - github?: string - discord?: string } - reputationLevel: string lastUpdated: Date } @@ -49,58 +34,6 @@ export class PointSystem { return PointSystem.instance } - /** - * Award points for creating a Demos identity - */ - async awardIdentityCreationPoints(userId: string): Promise { - try { - // Get or create user points record - const userPoints = await this.getUserPoints(userId) - - // Only award points if not already awarded - if (userPoints.breakdown.identityCreation === 0) { - userPoints.breakdown.identityCreation = - pointValues.CREATE_IDENTITY - userPoints.totalPoints += pointValues.CREATE_IDENTITY - - await this.saveUserPoints(userPoints) - - return { - result: 200, - response: { - pointsAwarded: pointValues.CREATE_IDENTITY, - totalPoints: userPoints.totalPoints, - message: "Points awarded for identity creation", - }, - require_reply: false, - extra: {}, - } - } - - return { - result: 200, - response: { - pointsAwarded: 0, - totalPoints: userPoints.totalPoints, - message: "Points already awarded for identity creation", - }, - require_reply: false, - extra: {}, - } - } catch (error) { - console.error("Error awarding identity points:", error) - return { - result: 500, - response: "Error awarding points", - require_reply: false, - extra: { - error: - error instanceof Error ? error.message : String(error), - }, - } - } - } - /** * Award points for linking a Web3 wallet */ @@ -218,83 +151,126 @@ export class PointSystem { } /** - * Get user's current points and reputation + * Get user's current points */ async getUserPoints(userId: string): Promise { - // TODO: Implement database storage for points - // For now, we'll create a new record if none exists - - // In a real implementation, this would fetch from the database - const defaultPoints: UserPoints = { - userId, - totalPoints: 0, - breakdown: { - identityCreation: 0, - web3Wallets: 0, - socialAccounts: 0, - }, - linkedWallets: [], - linkedSocials: {}, - reputationLevel: reputationLevels[0].name, - lastUpdated: new Date(), - } - - // Calculate reputation level - const reputationLevel = this.calculateReputationLevel( - defaultPoints.totalPoints, - ) - defaultPoints.reputationLevel = reputationLevel + try { + // Get database connection + const datasource = await Datasource.getInstance() + const connection = datasource.getDataSource() + const userPointsRepo = connection.getRepository(UserPointsEntity) + + // Try to find existing user points + const userPointsEntity = await userPointsRepo.findOne({ + where: { userId }, + }) + + if (userPointsEntity) { + // Convert entity to UserPoints interface + const userPoints: UserPoints = { + userId: userPointsEntity.userId, + totalPoints: userPointsEntity.totalPoints, + breakdown: { + web3Wallets: + userPointsEntity.breakdown.web3Wallets || 0, + socialAccounts: + userPointsEntity.breakdown.socialAccounts || 0, + }, + linkedWallets: userPointsEntity.linkedWallets, + linkedSocials: { + twitter: userPointsEntity.linkedSocials.twitter, + }, + lastUpdated: userPointsEntity.updatedAt, + } + return userPoints + } - return defaultPoints - } + // If no record exists, create a default one + const defaultPoints: UserPoints = { + userId, + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + linkedWallets: [], + linkedSocials: {}, + lastUpdated: new Date(), + } - /** - * Calculate reputation level based on points - */ - private calculateReputationLevel(points: number): string { - let level = reputationLevels[0].name + return defaultPoints + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error) + console.error( + `[PointSystem] Error getting user points: ${errorMessage}`, + error, + ) - for (const repLevel of reputationLevels) { - if (points >= repLevel.minPoints) { - level = repLevel.name - } else { - break + // Return default points in case of error + return { + userId, + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + linkedWallets: [], + linkedSocials: {}, + lastUpdated: new Date(), } } - - return level } /** * Save user points to database */ private async saveUserPoints(userPoints: UserPoints): Promise { - // TODO: Implement database storage - // For now, we'll just update the reputation level and timestamp - userPoints.reputationLevel = this.calculateReputationLevel( - userPoints.totalPoints, - ) - userPoints.lastUpdated = new Date() - - console.log( - `[PointSystem] Updated points for user ${userPoints.userId}: ${userPoints.totalPoints} points`, - ) - } - - /** - * Check if a user has bot-like behavior - * This is a placeholder for more sophisticated bot detection - */ - async checkForBotBehavior(userId: string): Promise { - // TODO: Implement actual bot detection - // For now, we'll just return false (not a bot) + try { + // Update the last updated timestamp + userPoints.lastUpdated = new Date() + + // Get database connection + const datasource = await Datasource.getInstance() + const connection = datasource.getDataSource() + const userPointsRepo = connection.getRepository(UserPointsEntity) + + // Check if user already has a record + let userPointsEntity = await userPointsRepo.findOne({ + where: { userId: userPoints.userId }, + }) + + if (!userPointsEntity) { + // Create new entity if it doesn't exist + userPointsEntity = new UserPointsEntity() + userPointsEntity.userId = userPoints.userId + } - // In a real implementation, this would check: - // 1. Account age - // 2. Activity patterns - // 3. Social account verification status - // 4. Reputation on linked platforms + // Update entity with new values + userPointsEntity.totalPoints = userPoints.totalPoints + userPointsEntity.breakdown = { + web3Wallets: userPoints.breakdown.web3Wallets, + socialAccounts: userPoints.breakdown.socialAccounts, + } + userPointsEntity.linkedWallets = userPoints.linkedWallets + userPointsEntity.linkedSocials = { + twitter: userPoints.linkedSocials.twitter, + } - return false + // Save to database + await userPointsRepo.save(userPointsEntity) + + console.log( + `[PointSystem] Saved points for user ${userPoints.userId}: ${userPoints.totalPoints} points`, + ) + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error) + console.error( + `[PointSystem] Error saving user points: ${errorMessage}`, + error, + ) + throw error + } } } diff --git a/src/libs/network/manageGCRRoutines.ts b/src/libs/network/manageGCRRoutines.ts index 42e6cf47a..83c10dc8b 100644 --- a/src/libs/network/manageGCRRoutines.ts +++ b/src/libs/network/manageGCRRoutines.ts @@ -22,11 +22,31 @@ export default async function manageGCRRoutines( if (method.startsWith("incentive_")) { // Remove the "incentive_" prefix and pass to incentive controller const incentiveMethod = method.substring(10) - return await IncentiveController.getInstance().handleIncentiveRequest( - sender, - incentiveMethod, + console.log( + `[GCR_ROUTINE] Processing incentive request: ${incentiveMethod} with params:`, params, ) + try { + return await IncentiveController.getInstance().handleIncentiveRequest( + sender, + incentiveMethod, + params, + ) + } catch (error) { + console.error( + `[GCR_ROUTINE] Error processing incentive request ${incentiveMethod}:`, + error, + ) + return { + result: 500, + response: "Error processing incentive request", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } } switch (method) { diff --git a/src/model/datasource.ts b/src/model/datasource.ts index a7e98745a..e9926e471 100644 --- a/src/model/datasource.ts +++ b/src/model/datasource.ts @@ -24,6 +24,7 @@ import { GCRHashes } from "./entities/GCRv2/GCRHashes" import { GCRSubnetsTxs } from "./entities/GCRv2/GCRSubnetsTxs" import { GCRMain } from "./entities/GCRv2/GCR_Main" import { GCRTracker } from "./entities/GCR/GCRTracker" +import { UserPointsEntity } from "./entities/UserPoints" class Datasource { private static instance: Datasource @@ -53,9 +54,10 @@ class Datasource { GCRTracker, GCRMain, GCRTracker, + UserPointsEntity, ], synchronize: true, // set this to false in production - logging: false + logging: false, }) } diff --git a/src/model/entities/UserPoints.ts b/src/model/entities/UserPoints.ts index 58b55c58b..5a03d9131 100644 --- a/src/model/entities/UserPoints.ts +++ b/src/model/entities/UserPoints.ts @@ -1,3 +1,4 @@ +import "reflect-metadata" import { Entity, Column, @@ -8,7 +9,7 @@ import { @Entity() export class UserPointsEntity { - @PrimaryColumn() + @PrimaryColumn({ type: "varchar" }) userId: string @Column({ type: "integer", default: 0 }) @@ -16,7 +17,6 @@ export class UserPointsEntity { @Column({ type: "jsonb", default: {} }) breakdown: { - identityCreation: number web3Wallets: number socialAccounts: number } @@ -31,9 +31,6 @@ export class UserPointsEntity { discord?: string } - @Column() - reputationLevel: string - @CreateDateColumn() createdAt: Date From c7475f3fa1fc028644c6756183d876c1adcff45d Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Mon, 31 Mar 2025 16:29:19 +0200 Subject: [PATCH 03/14] remove unused fields from DB --- src/model/entities/UserPoints.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/model/entities/UserPoints.ts b/src/model/entities/UserPoints.ts index 5a03d9131..0019916c3 100644 --- a/src/model/entities/UserPoints.ts +++ b/src/model/entities/UserPoints.ts @@ -27,8 +27,6 @@ export class UserPointsEntity { @Column({ type: "jsonb", default: {} }) linkedSocials: { twitter?: string - github?: string - discord?: string } @CreateDateColumn() From 07ad5655d37ac583f462100f9c1c0dfd5b3a2ad5 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Thu, 17 Apr 2025 19:36:47 +0200 Subject: [PATCH 04/14] Update Pointsystem and use Transaction flow --- package.json | 2 +- src/features/incentive/IncentiveController.ts | 105 +---- src/features/incentive/PointSystem.ts | 420 ++++++++++++++---- .../gcr/gcr_routines/GCRIncentiveRoutines.ts | 234 ++++++++++ src/libs/blockchain/gcr/handleGCR.ts | 6 +- src/libs/network/endpointHandlers.ts | 30 +- 6 files changed, 607 insertions(+), 190 deletions(-) create mode 100644 src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts diff --git a/package.json b/package.json index 47b173f90..d6aa15168 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@fastify/swagger-ui": "^4.1.0", "@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", - "@kynesyslabs/demosdk": "2.1.9", + "@kynesyslabs/demosdk": "file:../sdks", "@the-convocation/twitter-scraper": "^0.15.1", "@types/express": "^4.17.21", "@types/http-proxy": "^1.17.14", diff --git a/src/features/incentive/IncentiveController.ts b/src/features/incentive/IncentiveController.ts index 2ec11b3c6..7fcb97768 100644 --- a/src/features/incentive/IncentiveController.ts +++ b/src/features/incentive/IncentiveController.ts @@ -16,99 +16,6 @@ export class IncentiveController { return IncentiveController.instance } - /** - * Handle incentive-related RPC requests - */ - async handleIncentiveRequest( - sender: string, - method: string, - params: any[], - ): Promise { - console.log(`[IncentiveController] Handling method: ${method}`) - console.log(`[IncentiveController] With sender: ${sender}`) - console.log(`[IncentiveController] With params:`, params) - - try { - switch (method) { - case "getPoints": { - console.log( - `[IncentiveController] Getting points for user: ${sender}`, - ) - const userPoints = await this.pointSystem.getUserPoints( - sender, - ) - return { - result: 200, - response: userPoints, - require_reply: false, - extra: {}, - } - } - - case "walletLinked": - if (params.length < 2) { - console.error( - `[IncentiveController] Invalid params for walletLinked. Expected 2, got ${params.length}`, - ) - return { - result: 400, - response: "Missing parameters for wallet linking", - require_reply: false, - extra: { - expected: 2, - received: params.length, - }, - } - } - - console.log( - `[IncentiveController] Linking wallet for user: ${sender}, wallet: ${params[0]}, chain: ${params[1]}`, - ) - return await this.pointSystem.awardWeb3WalletPoints( - sender, - params[0], // walletAddress - params[1], // chain - ) - - case "twitterLinked": - console.log( - `[IncentiveController] Linking Twitter for user: ${sender}, handle: ${params[0]}`, - ) - return await this.pointSystem.awardTwitterPoints( - sender, - params[0], // twitterHandle - ) - - default: - console.warn( - `[IncentiveController] Unknown method: ${method}`, - ) - return { - result: 400, - response: "Unknown method", - require_reply: false, - extra: { - message: `Method ${method} not supported`, - }, - } - } - } catch (error) { - console.error( - `[IncentiveController] Error handling method ${method}:`, - error, - ) - return { - result: 500, - response: "Internal server error", - require_reply: false, - extra: { - error: - error instanceof Error ? error.message : String(error), - }, - } - } - } - /** * Hook to be called after Web3 wallet linking */ @@ -116,8 +23,8 @@ export class IncentiveController { userId: string, walletAddress: string, chain: string, - ): Promise { - await this.pointSystem.awardWeb3WalletPoints( + ): Promise { + return await this.pointSystem.awardWeb3WalletPoints( userId, walletAddress, chain, @@ -130,7 +37,11 @@ export class IncentiveController { async onTwitterLinked( userId: string, twitterHandle: string, - ): Promise { - await this.pointSystem.awardTwitterPoints(userId, twitterHandle) + ): Promise { + return await this.pointSystem.awardTwitterPoints(userId, twitterHandle) + } + + async onGetPoints(userId: string): Promise { + return await this.pointSystem.getUserPoints(userId) } } diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 3ba246fcf..8111d6fed 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -22,6 +22,27 @@ export interface UserPoints { lastUpdated: Date } +// Define a type for successful responses containing UserPoints +type UserPointsResponse = { + result: 200 + response: UserPoints + require_reply: boolean + extra: Record +} + +// Define a type for error responses +type ErrorResponse = { + result: 400 | 500 + response: string + require_reply: boolean + extra: { + error: string + } +} + +// Combine the response types +type PointSystemResponse = UserPointsResponse | ErrorResponse + export class PointSystem { private static instance: PointSystem @@ -34,6 +55,69 @@ export class PointSystem { return PointSystem.instance } + /** + * Get user's current points + */ + private async getUserPointsInternal(userId: string): Promise { + const db = await Datasource.getInstance() + const userPointsRepository = db + .getDataSource() + .getRepository(UserPointsEntity) + + // Get or create user points record + let userPointsEntity = await userPointsRepository.findOneBy({ userId }) + if (!userPointsEntity) { + userPointsEntity = new UserPointsEntity() + userPointsEntity.userId = userId + userPointsEntity.totalPoints = 0 + userPointsEntity.breakdown = { + web3Wallets: 0, + socialAccounts: 0, + } + userPointsEntity.linkedWallets = [] + userPointsEntity.linkedSocials = {} + await userPointsRepository.save(userPointsEntity) + } + + // Convert entity to UserPoints interface + return { + userId: userPointsEntity.userId, + totalPoints: userPointsEntity.totalPoints, + breakdown: { + web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, + socialAccounts: userPointsEntity.breakdown?.socialAccounts || 0, + }, + linkedWallets: userPointsEntity.linkedWallets || [], + linkedSocials: { + twitter: userPointsEntity.linkedSocials?.twitter, + }, + lastUpdated: userPointsEntity.updatedAt, + } + } + + async getUserPoints(userId: string): Promise { + try { + const userPoints = await this.getUserPointsInternal(userId) + return { + result: 200, + response: userPoints, + require_reply: false, + extra: {}, + } + } catch (error) { + console.error("Error getting user points:", error) + return { + result: 500, + response: "Error getting user points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } + /** * Award points for linking a Web3 wallet */ @@ -43,10 +127,27 @@ export class PointSystem { chain: string, ): Promise { try { - // Get user points record - const userPoints = await this.getUserPoints(userId) + const userPoints = await this.getUserPointsInternal(userId) - // Check if this wallet is already linked + // Check if any wallet of this chain type is already linked + const hasExistingChainWallet = userPoints.linkedWallets.some( + wallet => wallet.startsWith(`${chain}:`), + ) + + if (hasExistingChainWallet) { + return { + result: 400, + response: { + pointsAwarded: 0, + totalPoints: userPoints.totalPoints, + message: `A ${chain} wallet is already linked. Please disconnect it first.`, + }, + require_reply: false, + extra: {}, + } + } + + // Check if this exact wallet is already linked if ( userPoints.linkedWallets.includes(`${chain}:${walletAddress}`) ) { @@ -55,7 +156,7 @@ export class PointSystem { response: { pointsAwarded: 0, totalPoints: userPoints.totalPoints, - message: "Wallet already linked, no new points awarded", + message: "This wallet is already linked", }, require_reply: false, extra: {}, @@ -101,8 +202,7 @@ export class PointSystem { twitterHandle: string, ): Promise { try { - // Get user points record - const userPoints = await this.getUserPoints(userId) + const userPoints = await this.getUserPointsInternal(userId) // Check if Twitter is already linked if (userPoints.linkedSocials.twitter) { @@ -151,126 +251,266 @@ export class PointSystem { } /** - * Get user's current points + * Save user points to database */ - async getUserPoints(userId: string): Promise { + private async saveUserPoints(userPoints: UserPoints): Promise { try { // Get database connection const datasource = await Datasource.getInstance() const connection = datasource.getDataSource() - const userPointsRepo = connection.getRepository(UserPointsEntity) - // Try to find existing user points - const userPointsEntity = await userPointsRepo.findOne({ - where: { userId }, - }) + // Use a transaction for database operations + await connection.transaction(async transactionalEntityManager => { + const userPointsRepo = + transactionalEntityManager.getRepository(UserPointsEntity) - if (userPointsEntity) { - // Convert entity to UserPoints interface - const userPoints: UserPoints = { - userId: userPointsEntity.userId, - totalPoints: userPointsEntity.totalPoints, - breakdown: { - web3Wallets: - userPointsEntity.breakdown.web3Wallets || 0, - socialAccounts: - userPointsEntity.breakdown.socialAccounts || 0, - }, - linkedWallets: userPointsEntity.linkedWallets, - linkedSocials: { - twitter: userPointsEntity.linkedSocials.twitter, - }, - lastUpdated: userPointsEntity.updatedAt, + // Check if user already has a record + let userPointsEntity = await userPointsRepo.findOne({ + where: { userId: userPoints.userId }, + }) + + if (!userPointsEntity) { + // Create new entity if it doesn't exist + userPointsEntity = new UserPointsEntity() + userPointsEntity.userId = userPoints.userId } - return userPoints - } - // If no record exists, create a default one - const defaultPoints: UserPoints = { - userId, - totalPoints: 0, - breakdown: { - web3Wallets: 0, - socialAccounts: 0, - }, - linkedWallets: [], - linkedSocials: {}, - lastUpdated: new Date(), - } + // Update entity with new values + userPointsEntity.totalPoints = userPoints.totalPoints + userPointsEntity.breakdown = { + web3Wallets: userPoints.breakdown.web3Wallets, + socialAccounts: userPoints.breakdown.socialAccounts, + } + userPointsEntity.linkedWallets = userPoints.linkedWallets + userPointsEntity.linkedSocials = { + twitter: userPoints.linkedSocials.twitter, + } - return defaultPoints + // Save to database within transaction + await userPointsRepo.save(userPointsEntity) + }) } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) console.error( - `[PointSystem] Error getting user points: ${errorMessage}`, + `[PointSystem] Error saving user points: ${errorMessage}`, error, ) + throw error + } + } - // Return default points in case of error - return { + async addPoints( + userId: string, + points: number, + type: "web3Wallets" | "socialAccounts", + ): Promise { + try { + const db = await Datasource.getInstance() + const userPointsRepository = db + .getDataSource() + .getRepository(UserPointsEntity) + + // Get or create user points record + let userPointsEntity = await userPointsRepository.findOneBy({ userId, - totalPoints: 0, - breakdown: { + }) + if (!userPointsEntity) { + userPointsEntity = new UserPointsEntity() + userPointsEntity.userId = userId + userPointsEntity.totalPoints = 0 + userPointsEntity.breakdown = { + web3Wallets: 0, + socialAccounts: 0, + } + userPointsEntity.linkedWallets = [] + userPointsEntity.linkedSocials = {} + } + + // Update points + userPointsEntity.totalPoints += points + if (!userPointsEntity.breakdown) { + userPointsEntity.breakdown = { web3Wallets: 0, socialAccounts: 0, + } + } + userPointsEntity.breakdown[type] += points + + await userPointsRepository.save(userPointsEntity) + + // Convert to UserPoints for response + const userPoints: UserPoints = { + userId: userPointsEntity.userId, + totalPoints: userPointsEntity.totalPoints, + breakdown: { + web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, + socialAccounts: + userPointsEntity.breakdown?.socialAccounts || 0, + }, + linkedWallets: userPointsEntity.linkedWallets || [], + linkedSocials: { + twitter: userPointsEntity.linkedSocials?.twitter, + }, + lastUpdated: userPointsEntity.updatedAt, + } + + return { + result: 200, + response: userPoints, + require_reply: false, + extra: {}, + } + } catch (error) { + console.error("Error adding points:", error) + return { + result: 500, + response: "Error adding points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), }, - linkedWallets: [], - linkedSocials: {}, - lastUpdated: new Date(), } } } - /** - * Save user points to database - */ - private async saveUserPoints(userPoints: UserPoints): Promise { + async linkWallet( + userId: string, + walletAddress: string, + ): Promise { try { - // Update the last updated timestamp - userPoints.lastUpdated = new Date() + const db = await Datasource.getInstance() + const userPointsRepository = db + .getDataSource() + .getRepository(UserPointsEntity) - // Get database connection - const datasource = await Datasource.getInstance() - const connection = datasource.getDataSource() - const userPointsRepo = connection.getRepository(UserPointsEntity) - - // Check if user already has a record - let userPointsEntity = await userPointsRepo.findOne({ - where: { userId: userPoints.userId }, + // Get or create user points record + let userPointsEntity = await userPointsRepository.findOneBy({ + userId, }) - if (!userPointsEntity) { - // Create new entity if it doesn't exist userPointsEntity = new UserPointsEntity() - userPointsEntity.userId = userPoints.userId + userPointsEntity.userId = userId + userPointsEntity.totalPoints = 0 + userPointsEntity.breakdown = { + web3Wallets: 0, + socialAccounts: 0, + } + userPointsEntity.linkedWallets = [] + userPointsEntity.linkedSocials = {} } - // Update entity with new values - userPointsEntity.totalPoints = userPoints.totalPoints - userPointsEntity.breakdown = { - web3Wallets: userPoints.breakdown.web3Wallets, - socialAccounts: userPoints.breakdown.socialAccounts, + // Add wallet if not already linked + if (!userPointsEntity.linkedWallets.includes(walletAddress)) { + userPointsEntity.linkedWallets.push(walletAddress) + await userPointsRepository.save(userPointsEntity) + } + + // Convert to UserPoints for response + const userPoints: UserPoints = { + userId: userPointsEntity.userId, + totalPoints: userPointsEntity.totalPoints, + breakdown: { + web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, + socialAccounts: + userPointsEntity.breakdown?.socialAccounts || 0, + }, + linkedWallets: userPointsEntity.linkedWallets || [], + linkedSocials: { + twitter: userPointsEntity.linkedSocials?.twitter, + }, + lastUpdated: userPointsEntity.updatedAt, + } + + return { + result: 200, + response: userPoints, + require_reply: false, + extra: {}, + } + } catch (error) { + console.error("Error linking wallet:", error) + return { + result: 500, + response: "Error linking wallet", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, } - userPointsEntity.linkedWallets = userPoints.linkedWallets - userPointsEntity.linkedSocials = { - twitter: userPoints.linkedSocials.twitter, + } + } + + async linkSocialAccount( + userId: string, + platform: "twitter", + accountId: string, + ): Promise { + try { + const db = await Datasource.getInstance() + const userPointsRepository = db + .getDataSource() + .getRepository(UserPointsEntity) + + // Get or create user points record + let userPointsEntity = await userPointsRepository.findOneBy({ + userId, + }) + if (!userPointsEntity) { + userPointsEntity = new UserPointsEntity() + userPointsEntity.userId = userId + userPointsEntity.totalPoints = 0 + userPointsEntity.breakdown = { + web3Wallets: 0, + socialAccounts: 0, + } + userPointsEntity.linkedWallets = [] + userPointsEntity.linkedSocials = {} } - // Save to database - await userPointsRepo.save(userPointsEntity) + // Update social account + if (!userPointsEntity.linkedSocials) { + userPointsEntity.linkedSocials = {} + } + userPointsEntity.linkedSocials[platform] = accountId - console.log( - `[PointSystem] Saved points for user ${userPoints.userId}: ${userPoints.totalPoints} points`, - ) - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error) - console.error( - `[PointSystem] Error saving user points: ${errorMessage}`, - error, - ) - throw error + await userPointsRepository.save(userPointsEntity) + + // Convert to UserPoints for response + const userPoints: UserPoints = { + userId: userPointsEntity.userId, + totalPoints: userPointsEntity.totalPoints, + breakdown: { + web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, + socialAccounts: + userPointsEntity.breakdown?.socialAccounts || 0, + }, + linkedWallets: userPointsEntity.linkedWallets || [], + linkedSocials: { + twitter: userPointsEntity.linkedSocials?.twitter, + }, + lastUpdated: userPointsEntity.updatedAt, + } + + return { + result: 200, + response: userPoints, + require_reply: false, + extra: {}, + } + } catch (error) { + console.error("Error linking social account:", error) + return { + result: 500, + response: "Error linking social account", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } } } } diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts new file mode 100644 index 000000000..f0e0e5b61 --- /dev/null +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts @@ -0,0 +1,234 @@ +import { GCRResult } from "../handleGCR" +import { GCREdit, GCREditIncentive } from "@kynesyslabs/demosdk/types" +import { forgeToHex } from "@/libs/crypto/forgeUtils" +import { IncentiveController } from "@/features/incentive/IncentiveController" +import log from "@/utilities/logger" + +export default class GCRIncentiveRoutines { + /** + * Process wallet linking incentive + */ + static async applyWalletLinkedIncentive( + editOperation: GCREditIncentive, + simulate: boolean, + ): Promise { + const { walletAddress, chain } = editOperation.data + + if (!walletAddress || !chain) { + return { + success: false, + message: "Invalid wallet linked incentive data", + } + } + + // Only actually award points if not simulating + if (!simulate) { + try { + const incentiveController = IncentiveController.getInstance() + await incentiveController.onWalletLinked( + editOperation.account, + walletAddress, + chain, + ) + log.info( + `Awarded wallet linking points to ${editOperation.account} for ${chain}:${walletAddress}`, + ) + return { + success: true, + message: "Wallet linking points awarded", + } + } catch (error: any) { + log.error(`Failed to award wallet linking points: ${error}`) + return { + success: false, + message: `Failed to award wallet linking points: ${ + error.message || String(error) + }`, + } + } + } + + // When simulating, just return success + return { + success: true, + message: "Wallet linking points would be awarded (simulation)", + } + } + + /** + * Process social media linking incentive + */ + static async applySocialLinkedIncentive( + editOperation: GCREditIncentive, + simulate: boolean, + ): Promise { + const { username, platform } = editOperation.data + + if (!username || !platform) { + return { + success: false, + message: "Invalid social linked incentive data", + } + } + + // Only actually award points if not simulating + if (!simulate) { + try { + const incentiveController = IncentiveController.getInstance() + + // Currently only Twitter is supported + if (platform.toLowerCase() === "twitter") { + await incentiveController.onTwitterLinked( + editOperation.account, + username, + ) + log.info( + `Awarded Twitter linking points to ${editOperation.account} for ${username}`, + ) + return { + success: true, + message: "Twitter linking points awarded", + } + } else { + // For future expansion to other platforms + return { + success: false, + message: `Unsupported social platform: ${platform}`, + } + } + } catch (error: any) { + log.error(`Failed to award social linking points: ${error}`) + return { + success: false, + message: `Failed to award social linking points: ${ + error.message || String(error) + }`, + } + } + } + + // When simulating, just return success + return { + success: true, + message: "Social linking points would be awarded (simulation)", + } + } + + /** + * Process get points request + */ + static async applyGetPointsIncentive( + editOperation: GCREditIncentive, + simulate: boolean, + ): Promise { + const { account } = editOperation + + if (!account) { + return { + success: false, + message: "Invalid account for get points request", + } + } + + // Only actually get points if not simulating + if (!simulate) { + try { + const incentiveController = IncentiveController.getInstance() + const response = await incentiveController.onGetPoints(account) + log.info( + `Retrieved points for ${account}: ${JSON.stringify( + response, + )}`, + ) + return { + success: true, + message: "Points retrieved successfully", + response: response, + } + } catch (error: any) { + log.error(`Failed to get points: ${error}`) + return { + success: false, + message: `Failed to get points: ${ + error.message || String(error) + }`, + } + } + } + + // When simulating, return a mock response + return { + success: true, + message: "Points would be retrieved (simulation)", + response: { + /* Empty points object for simulation */ userId: account, + totalPoints: 0, + breakdown: { web3Wallets: 0, socialAccounts: 0 }, + linkedWallets: [], + linkedSocials: {}, + lastUpdated: new Date(), + }, + } + } + + /** + * Main apply method - handles all incentive operations + */ + static async apply( + editOperation: GCREdit, + simulate: boolean, + ): Promise { + if (editOperation.type !== "incentive") { + return { + success: false, + message: "Invalid edit operation for incentive routine", + } + } + + const incentiveEdit = editOperation as GCREditIncentive + + // Convert account to hex if needed + incentiveEdit.account = + typeof incentiveEdit.account === "string" + ? incentiveEdit.account + : forgeToHex(incentiveEdit.account) + + // Handle rollbacks for incentives + if (incentiveEdit.isRollback) { + // For most incentives, there's no need to rollback points + // Points are not typically removed once awarded + return { + success: true, + message: + "Incentive rollbacks are not required - points remain awarded", + } + } + + // Process based on incentive type + switch (incentiveEdit.incentiveType) { + case "wallet_linked": + return await this.applyWalletLinkedIncentive( + incentiveEdit, + simulate, + ) + + case "social_linked": + return await this.applySocialLinkedIncentive( + incentiveEdit, + simulate, + ) + + case "get_points": + return await this.applyGetPointsIncentive( + incentiveEdit, + simulate, + ) + + default: + return { + success: false, + message: `Unsupported incentive type: ${incentiveEdit.incentiveType}`, + } + } + } +} diff --git a/src/libs/blockchain/gcr/handleGCR.ts b/src/libs/blockchain/gcr/handleGCR.ts index 71f7acce8..07b44c0dd 100644 --- a/src/libs/blockchain/gcr/handleGCR.ts +++ b/src/libs/blockchain/gcr/handleGCR.ts @@ -48,6 +48,7 @@ import GCRNonceRoutines from "./gcr_routines/GCRNonceRoutines" import Chain from "../chain" import { Repository } from "typeorm" import GCRIdentityRoutines from "./gcr_routines/GCRIdentityRoutines" +import GCRIncentiveRoutines from "./gcr_routines/GCRIncentiveRoutines" export type GetNativeStatusOptions = { balance?: boolean @@ -69,9 +70,10 @@ export type GetNativeSubnetsTxsOptions = { txData?: boolean } -export type GCRResult = { +export interface GCRResult { success: boolean message: string + response?: any } // ? Maybe sanitize the options? @@ -272,6 +274,8 @@ export default class HandleGCR { repositories.main as Repository, simulate, ) + case "incentive": + return GCRIncentiveRoutines.apply(editOperation, simulate) case "assign": case "subnetsTx": // TODO implementations diff --git a/src/libs/network/endpointHandlers.ts b/src/libs/network/endpointHandlers.ts index dccf6a0e5..9bbb344fd 100644 --- a/src/libs/network/endpointHandlers.ts +++ b/src/libs/network/endpointHandlers.ts @@ -33,6 +33,7 @@ import { RPCResponse, IWeb2Payload, GCREdit, + GCREditIncentive, } from "@kynesyslabs/demosdk/types" import PeerManager from "src/libs/peer/PeerManager" import log from "src/utilities/logger" @@ -52,9 +53,9 @@ import { SubnetPayload } from "@kynesyslabs/demosdk/l2ps" import { L2PSMessage, L2PSRegisterTxMessage } from "../l2ps/parallelNetworks" import { handleWeb2ProxyRequest } from "./routines/transactions/handleWeb2ProxyRequest" import { parseWeb2ProxyRequest } from "../utils/web2RequestUtils" -import IdentityManager from "../blockchain/gcr/gcr_routines/identityManager" import handleIdentityRequest from "./routines/transactions/handleIdentityRequest" import { IdentityPayload } from "@kynesyslabs/demosdk/abstraction" +import GCRIncentiveRoutines from "../blockchain/gcr/gcr_routines/GCRIncentiveRoutines" /* // ! Note: this will be removed once demosWork is in place import { NativePayload, @@ -392,6 +393,33 @@ export default class ServerHandlers { } } + break + case "incentive": + try { + const incentivePayload = tx.content + .data[1] as GCREditIncentive + + incentivePayload.txhash = tx.hash + const incentiveResult = await GCRIncentiveRoutines.apply( + incentivePayload, + false, + ) + + result.success = incentiveResult.success + result.response = incentiveResult.response + } catch (e) { + console.error(e) + log.error( + "[handleExecuteTransaction] Error in incentive: " + e, + ) + result.success = false + result.response = { + message: "Failed to process incentive request", + } + result.extra = { + error: e, + } + } break } From f62c98c7be2707e5dceb40190b7f0758c6ad9388 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Sat, 19 Apr 2025 14:44:21 +0200 Subject: [PATCH 05/14] Propagate Twitter points message from point system --- .../blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts | 6 +++--- src/libs/network/endpointHandlers.ts | 3 +++ src/libs/network/server_rpc.ts | 9 +++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts index f0e0e5b61..793ec618b 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts @@ -78,7 +78,7 @@ export default class GCRIncentiveRoutines { // Currently only Twitter is supported if (platform.toLowerCase() === "twitter") { - await incentiveController.onTwitterLinked( + const response = await incentiveController.onTwitterLinked( editOperation.account, username, ) @@ -86,8 +86,8 @@ export default class GCRIncentiveRoutines { `Awarded Twitter linking points to ${editOperation.account} for ${username}`, ) return { - success: true, - message: "Twitter linking points awarded", + success: response.result === 200 ? true : false, + message: response.response.message, } } else { // For future expansion to other platforms diff --git a/src/libs/network/endpointHandlers.ts b/src/libs/network/endpointHandlers.ts index 9bbb344fd..4326c4f44 100644 --- a/src/libs/network/endpointHandlers.ts +++ b/src/libs/network/endpointHandlers.ts @@ -407,6 +407,9 @@ export default class ServerHandlers { result.success = incentiveResult.success result.response = incentiveResult.response + result.extra = { + message: incentiveResult.message, + } } catch (e) { console.error(e) log.error( diff --git a/src/libs/network/server_rpc.ts b/src/libs/network/server_rpc.ts index 27280e85f..e5a97cf3b 100644 --- a/src/libs/network/server_rpc.ts +++ b/src/libs/network/server_rpc.ts @@ -37,8 +37,7 @@ import manageBridges from "./manageBridge" // Reading the port from sharedState -// TODO: Add Proper authentication -const noAuthMethods = ["nodeCall", "gcr_routine"] +const noAuthMethods = ["nodeCall"] export const emptyResponse: RPCResponse = { result: 0, @@ -288,10 +287,8 @@ export default async function serverRpc(): Promise { // Excluding due to noAuthMethods from header validation if (!noAuthMethods.includes(payload.method)) { const headerValidation = validateHeaders(headers) - - log.info( - "[RPC Call] Header validation: " + headerValidation[0], - ) + + log.info("[RPC Call] Header validation: " + headerValidation[0]) if (!headerValidation[0]) { reply.status(401).send({ error: "Invalid headers:" + headerValidation[1], From 37657642c5ddb636a1049898c3e66941fc35f160 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Sat, 19 Apr 2025 17:45:54 +0200 Subject: [PATCH 06/14] propagate response message for wallet linking --- .../blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts index 793ec618b..829c4a180 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts @@ -25,7 +25,7 @@ export default class GCRIncentiveRoutines { if (!simulate) { try { const incentiveController = IncentiveController.getInstance() - await incentiveController.onWalletLinked( + const response = await incentiveController.onWalletLinked( editOperation.account, walletAddress, chain, @@ -34,8 +34,8 @@ export default class GCRIncentiveRoutines { `Awarded wallet linking points to ${editOperation.account} for ${chain}:${walletAddress}`, ) return { - success: true, - message: "Wallet linking points awarded", + success: response.result === 200 ? true : false, + message: response.response.message, } } catch (error: any) { log.error(`Failed to award wallet linking points: ${error}`) From a642c5d7a94ce55a0f239271925dd1f0acbeb40a Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Thu, 1 May 2025 14:49:07 +0200 Subject: [PATCH 07/14] Refactor PointSystem to integrate GCR for user identities and points management - Replaced UserPointsEntity with direct GCR integration for user points. - Updated methods to retrieve user identities and points from GCR. - Removed unused UserPointsEntity and related database fields. - Enhanced wallet and Twitter linking methods to award points through GCR. - Improved error handling and response messages for identity and points queries. --- src/features/incentive/PointSystem.ts | 573 ++++++------------ src/libs/blockchain/gcr/gcr.ts | 28 +- .../gcr/gcr_routines/GCRIdentityRoutines.ts | 55 +- .../gcr/gcr_routines/GCRIncentiveRoutines.ts | 234 ------- .../gcr/gcr_routines/identityManager.ts | 55 +- .../gcr/gcr_routines/manageNative.ts | 12 +- src/libs/blockchain/gcr/handleGCR.ts | 15 +- src/libs/network/endpointHandlers.ts | 74 +-- src/libs/network/manageGCRRoutines.ts | 54 -- .../transactions/handleIdentityRequest.ts | 75 ++- src/model/datasource.ts | 2 - src/model/entities/GCRv2/GCR_Main.ts | 9 + src/model/entities/UserPoints.ts | 37 -- src/utilities/turnstile.ts | 42 ++ 14 files changed, 463 insertions(+), 802 deletions(-) delete mode 100644 src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts delete mode 100644 src/model/entities/UserPoints.ts create mode 100644 src/utilities/turnstile.ts diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 8111d6fed..7fbc031ba 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -1,48 +1,15 @@ import Datasource from "../../model/datasource" import { RPCResponse } from "@kynesyslabs/demosdk/types" -import { UserPointsEntity } from "../../model/entities/UserPoints" +import { UserPoints } from "@kynesyslabs/demosdk/abstraction" +import IdentityManager from "@/libs/blockchain/gcr/gcr_routines/identityManager" +import { GCRMain } from "@/model/entities/GCRv2/GCR_Main" +import HandleGCR from "@/libs/blockchain/gcr/handleGCR" -// Define point values for different actions const pointValues = { - LINK_WEB3_WALLET: 2, // per wallet + LINK_WEB3_WALLET: 2, LINK_TWITTER: 5, } -export interface UserPoints { - userId: string - totalPoints: number - breakdown: { - web3Wallets: number - socialAccounts: number - } - linkedWallets: string[] - linkedSocials: { - twitter?: string - } - lastUpdated: Date -} - -// Define a type for successful responses containing UserPoints -type UserPointsResponse = { - result: 200 - response: UserPoints - require_reply: boolean - extra: Record -} - -// Define a type for error responses -type ErrorResponse = { - result: 400 | 500 - response: string - require_reply: boolean - extra: { - error: string - } -} - -// Combine the response types -type PointSystemResponse = UserPointsResponse | ErrorResponse - export class PointSystem { private static instance: PointSystem @@ -56,56 +23,142 @@ export class PointSystem { } /** - * Get user's current points + * Get user's identities directly from the GCR */ - private async getUserPointsInternal(userId: string): Promise { - const db = await Datasource.getInstance() - const userPointsRepository = db - .getDataSource() - .getRepository(UserPointsEntity) - - // Get or create user points record - let userPointsEntity = await userPointsRepository.findOneBy({ userId }) - if (!userPointsEntity) { - userPointsEntity = new UserPointsEntity() - userPointsEntity.userId = userId - userPointsEntity.totalPoints = 0 - userPointsEntity.breakdown = { - web3Wallets: 0, - socialAccounts: 0, + private async getUserIdentitiesFromGCR(userId: string): Promise<{ + linkedWallets: string[] + linkedSocials: { twitter?: string } + }> { + const xmIdentities = await IdentityManager.getXmIdentities(userId) + const web2Identities = await IdentityManager.getWeb2Identifiers(userId) + + const linkedWallets: string[] = [] + + if (xmIdentities) { + const chains = Object.keys(xmIdentities) + + for (const chain of chains) { + const subChains = xmIdentities[chain] + const subChainKeys = Object.keys(subChains) + + for (const subChain of subChainKeys) { + const addresses = subChains[subChain] + + if (Array.isArray(addresses)) { + addresses.forEach(address => { + const walletId = `${chain}:${address}` + linkedWallets.push(walletId) + }) + } + } } - userPointsEntity.linkedWallets = [] - userPointsEntity.linkedSocials = {} - await userPointsRepository.save(userPointsEntity) } - // Convert entity to UserPoints interface - return { - userId: userPointsEntity.userId, - totalPoints: userPointsEntity.totalPoints, + const linkedSocials: { twitter?: string } = {} + + if ( + web2Identities && + typeof web2Identities === "object" && + "twitter" in web2Identities && + Array.isArray(web2Identities.twitter) && + web2Identities.twitter.length > 0 + ) { + const twitterProof = web2Identities.twitter[0] + linkedSocials.twitter = + typeof twitterProof === "string" + ? twitterProof + : twitterProof.username || "" + } + + return { linkedWallets, linkedSocials } + } + + /** + * Get user's points from GCR + */ + private async getUserPointsInternal(userId: string): Promise { + // Convert userId to hex string if it's a Buffer + const userIdStr = Buffer.isBuffer(userId) + ? userId.toString("hex") + : userId + + const db = await Datasource.getInstance() + const gcrMainRepository = db.getDataSource().getRepository(GCRMain) + const account = await gcrMainRepository.findOneBy({ pubkey: userIdStr }) + + const { linkedWallets, linkedSocials } = + await this.getUserIdentitiesFromGCR(userIdStr) + + // Create the response object + const userPoints = { + userId: userIdStr, + totalPoints: account.points.totalPoints || 0, breakdown: { - web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, - socialAccounts: userPointsEntity.breakdown?.socialAccounts || 0, - }, - linkedWallets: userPointsEntity.linkedWallets || [], - linkedSocials: { - twitter: userPointsEntity.linkedSocials?.twitter, + web3Wallets: account.points.breakdown?.web3Wallets || 0, + socialAccounts: account.points.breakdown?.socialAccounts || 0, }, - lastUpdated: userPointsEntity.updatedAt, + linkedWallets, + linkedSocials, + lastUpdated: account.points.lastUpdated || new Date(), } + + return userPoints } + /** + * Add points to the GCR for a user + */ + private async addPointsToGCR( + userId: string, + points: number, + type: "web3Wallets" | "socialAccounts", + ): Promise { + const db = await Datasource.getInstance() + const gcrMainRepository = db.getDataSource().getRepository(GCRMain) + const account = await gcrMainRepository.findOneBy({ pubkey: userId }) + + if (!account) { + const newAccount = await HandleGCR.createAccount(userId) + newAccount.points.totalPoints = points + newAccount.points.breakdown[type] = points + newAccount.points.lastUpdated = new Date() + + await gcrMainRepository.save(newAccount) + } else { + const oldTotal = account.points.totalPoints || 0 + account.points.totalPoints = oldTotal + points + + const oldCategoryPoints = account.points.breakdown[type] || 0 + account.points.breakdown[type] = oldCategoryPoints + points + account.points.lastUpdated = new Date() + + await gcrMainRepository.save(account) + } + } + + /** + * Get user's current points + * @param userId The user's Demos address + * @returns User points with identity information from GCR + */ async getUserPoints(userId: string): Promise { try { const userPoints = await this.getUserPointsInternal(userId) + return { result: 200, - response: userPoints, + response: { + userId: userPoints.userId, + totalPoints: userPoints.totalPoints, + breakdown: userPoints.breakdown, + linkedWallets: userPoints.linkedWallets, + linkedSocials: userPoints.linkedSocials, + lastUpdated: userPoints.lastUpdated, + }, require_reply: false, extra: {}, } } catch (error) { - console.error("Error getting user points:", error) return { result: 500, response: "Error getting user points", @@ -120,68 +173,75 @@ export class PointSystem { /** * Award points for linking a Web3 wallet + * @param userId The user's Demos address + * @param walletAddress The wallet address + * @param chain The chain type + * @returns RPCResponse */ async awardWeb3WalletPoints( userId: string, walletAddress: string, chain: string, ): Promise { + let walletIsAlreadyLinked = false + let hasExistingWalletOnChain = false + const walletIsAlreadyLinkedMessage = "This wallet is already linked" + const hasExistingWalletOnChainMessage = `A ${chain} wallet is already linked. Please disconnect it first.` try { - const userPoints = await this.getUserPointsInternal(userId) - - // Check if any wallet of this chain type is already linked - const hasExistingChainWallet = userPoints.linkedWallets.some( - wallet => wallet.startsWith(`${chain}:`), + // Get current points and identities from GCR + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, ) - if (hasExistingChainWallet) { - return { - result: 400, - response: { - pointsAwarded: 0, - totalPoints: userPoints.totalPoints, - message: `A ${chain} wallet is already linked. Please disconnect it first.`, - }, - require_reply: false, - extra: {}, - } - } - - // Check if this exact wallet is already linked if ( - userPoints.linkedWallets.includes(`${chain}:${walletAddress}`) + userPointsWithIdentities.linkedWallets.includes( + `${chain}:${walletAddress}`, + ) ) { - return { - result: 200, - response: { - pointsAwarded: 0, - totalPoints: userPoints.totalPoints, - message: "This wallet is already linked", - }, - require_reply: false, - extra: {}, - } + walletIsAlreadyLinked = true } - // Award points for the new wallet - userPoints.linkedWallets.push(`${chain}:${walletAddress}`) - userPoints.breakdown.web3Wallets += pointValues.LINK_WEB3_WALLET - userPoints.totalPoints += pointValues.LINK_WEB3_WALLET + // Check if any wallet of this chain type is already linked + const hasExistingChainWallet = + userPointsWithIdentities.linkedWallets.some(wallet => + wallet.startsWith(`${chain}:`), + ) - await this.saveUserPoints(userPoints) + if (hasExistingChainWallet) { + hasExistingWalletOnChain = true + } + + // Award points by updating the GCR + await this.addPointsToGCR( + userId, + pointValues.LINK_WEB3_WALLET, + "web3Wallets", + ) + + // Get updated points + const updatedPoints = await this.getUserPointsInternal(userId) return { - result: 200, + result: + walletIsAlreadyLinked || hasExistingWalletOnChain + ? 400 + : 200, response: { - pointsAwarded: pointValues.LINK_WEB3_WALLET, - totalPoints: userPoints.totalPoints, - message: "Points awarded for linking Web3 wallet", + pointsAwarded: + !walletIsAlreadyLinked && !hasExistingWalletOnChain + ? pointValues.LINK_WEB3_WALLET + : 0, + totalPoints: updatedPoints.totalPoints, + message: walletIsAlreadyLinked + ? walletIsAlreadyLinkedMessage + : hasExistingWalletOnChain + ? hasExistingWalletOnChainMessage + : "Points awarded for linking wallet", }, require_reply: false, extra: {}, } } catch (error) { - console.error("Error awarding wallet points:", error) return { result: 500, response: "Error awarding points", @@ -196,48 +256,51 @@ export class PointSystem { /** * Award points for linking a Twitter account + * @param userId The user's Demos address + * @param twitterHandle The user's Twitter handle + * @returns RPCResponse */ async awardTwitterPoints( userId: string, twitterHandle: string, ): Promise { try { - const userPoints = await this.getUserPointsInternal(userId) + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) - // Check if Twitter is already linked - if (userPoints.linkedSocials.twitter) { + if (userPointsWithIdentities.linkedSocials.twitter) { return { result: 200, response: { pointsAwarded: 0, - totalPoints: userPoints.totalPoints, - message: - "Twitter already linked, no new points awarded", + totalPoints: userPointsWithIdentities.totalPoints, + message: "Twitter is already linked", }, require_reply: false, extra: {}, } } - // Award points for linking Twitter - userPoints.linkedSocials.twitter = twitterHandle - userPoints.breakdown.socialAccounts += pointValues.LINK_TWITTER - userPoints.totalPoints += pointValues.LINK_TWITTER + await this.addPointsToGCR( + userId, + pointValues.LINK_TWITTER, + "socialAccounts", + ) - await this.saveUserPoints(userPoints) + const updatedPoints = await this.getUserPointsInternal(userId) return { result: 200, response: { pointsAwarded: pointValues.LINK_TWITTER, - totalPoints: userPoints.totalPoints, + totalPoints: updatedPoints.totalPoints, message: "Points awarded for linking Twitter", }, require_reply: false, extra: {}, } } catch (error) { - console.error("Error awarding Twitter points:", error) return { result: 500, response: "Error awarding points", @@ -249,268 +312,4 @@ export class PointSystem { } } } - - /** - * Save user points to database - */ - private async saveUserPoints(userPoints: UserPoints): Promise { - try { - // Get database connection - const datasource = await Datasource.getInstance() - const connection = datasource.getDataSource() - - // Use a transaction for database operations - await connection.transaction(async transactionalEntityManager => { - const userPointsRepo = - transactionalEntityManager.getRepository(UserPointsEntity) - - // Check if user already has a record - let userPointsEntity = await userPointsRepo.findOne({ - where: { userId: userPoints.userId }, - }) - - if (!userPointsEntity) { - // Create new entity if it doesn't exist - userPointsEntity = new UserPointsEntity() - userPointsEntity.userId = userPoints.userId - } - - // Update entity with new values - userPointsEntity.totalPoints = userPoints.totalPoints - userPointsEntity.breakdown = { - web3Wallets: userPoints.breakdown.web3Wallets, - socialAccounts: userPoints.breakdown.socialAccounts, - } - userPointsEntity.linkedWallets = userPoints.linkedWallets - userPointsEntity.linkedSocials = { - twitter: userPoints.linkedSocials.twitter, - } - - // Save to database within transaction - await userPointsRepo.save(userPointsEntity) - }) - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error) - console.error( - `[PointSystem] Error saving user points: ${errorMessage}`, - error, - ) - throw error - } - } - - async addPoints( - userId: string, - points: number, - type: "web3Wallets" | "socialAccounts", - ): Promise { - try { - const db = await Datasource.getInstance() - const userPointsRepository = db - .getDataSource() - .getRepository(UserPointsEntity) - - // Get or create user points record - let userPointsEntity = await userPointsRepository.findOneBy({ - userId, - }) - if (!userPointsEntity) { - userPointsEntity = new UserPointsEntity() - userPointsEntity.userId = userId - userPointsEntity.totalPoints = 0 - userPointsEntity.breakdown = { - web3Wallets: 0, - socialAccounts: 0, - } - userPointsEntity.linkedWallets = [] - userPointsEntity.linkedSocials = {} - } - - // Update points - userPointsEntity.totalPoints += points - if (!userPointsEntity.breakdown) { - userPointsEntity.breakdown = { - web3Wallets: 0, - socialAccounts: 0, - } - } - userPointsEntity.breakdown[type] += points - - await userPointsRepository.save(userPointsEntity) - - // Convert to UserPoints for response - const userPoints: UserPoints = { - userId: userPointsEntity.userId, - totalPoints: userPointsEntity.totalPoints, - breakdown: { - web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, - socialAccounts: - userPointsEntity.breakdown?.socialAccounts || 0, - }, - linkedWallets: userPointsEntity.linkedWallets || [], - linkedSocials: { - twitter: userPointsEntity.linkedSocials?.twitter, - }, - lastUpdated: userPointsEntity.updatedAt, - } - - return { - result: 200, - response: userPoints, - require_reply: false, - extra: {}, - } - } catch (error) { - console.error("Error adding points:", error) - return { - result: 500, - response: "Error adding points", - require_reply: false, - extra: { - error: - error instanceof Error ? error.message : String(error), - }, - } - } - } - - async linkWallet( - userId: string, - walletAddress: string, - ): Promise { - try { - const db = await Datasource.getInstance() - const userPointsRepository = db - .getDataSource() - .getRepository(UserPointsEntity) - - // Get or create user points record - let userPointsEntity = await userPointsRepository.findOneBy({ - userId, - }) - if (!userPointsEntity) { - userPointsEntity = new UserPointsEntity() - userPointsEntity.userId = userId - userPointsEntity.totalPoints = 0 - userPointsEntity.breakdown = { - web3Wallets: 0, - socialAccounts: 0, - } - userPointsEntity.linkedWallets = [] - userPointsEntity.linkedSocials = {} - } - - // Add wallet if not already linked - if (!userPointsEntity.linkedWallets.includes(walletAddress)) { - userPointsEntity.linkedWallets.push(walletAddress) - await userPointsRepository.save(userPointsEntity) - } - - // Convert to UserPoints for response - const userPoints: UserPoints = { - userId: userPointsEntity.userId, - totalPoints: userPointsEntity.totalPoints, - breakdown: { - web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, - socialAccounts: - userPointsEntity.breakdown?.socialAccounts || 0, - }, - linkedWallets: userPointsEntity.linkedWallets || [], - linkedSocials: { - twitter: userPointsEntity.linkedSocials?.twitter, - }, - lastUpdated: userPointsEntity.updatedAt, - } - - return { - result: 200, - response: userPoints, - require_reply: false, - extra: {}, - } - } catch (error) { - console.error("Error linking wallet:", error) - return { - result: 500, - response: "Error linking wallet", - require_reply: false, - extra: { - error: - error instanceof Error ? error.message : String(error), - }, - } - } - } - - async linkSocialAccount( - userId: string, - platform: "twitter", - accountId: string, - ): Promise { - try { - const db = await Datasource.getInstance() - const userPointsRepository = db - .getDataSource() - .getRepository(UserPointsEntity) - - // Get or create user points record - let userPointsEntity = await userPointsRepository.findOneBy({ - userId, - }) - if (!userPointsEntity) { - userPointsEntity = new UserPointsEntity() - userPointsEntity.userId = userId - userPointsEntity.totalPoints = 0 - userPointsEntity.breakdown = { - web3Wallets: 0, - socialAccounts: 0, - } - userPointsEntity.linkedWallets = [] - userPointsEntity.linkedSocials = {} - } - - // Update social account - if (!userPointsEntity.linkedSocials) { - userPointsEntity.linkedSocials = {} - } - userPointsEntity.linkedSocials[platform] = accountId - - await userPointsRepository.save(userPointsEntity) - - // Convert to UserPoints for response - const userPoints: UserPoints = { - userId: userPointsEntity.userId, - totalPoints: userPointsEntity.totalPoints, - breakdown: { - web3Wallets: userPointsEntity.breakdown?.web3Wallets || 0, - socialAccounts: - userPointsEntity.breakdown?.socialAccounts || 0, - }, - linkedWallets: userPointsEntity.linkedWallets || [], - linkedSocials: { - twitter: userPointsEntity.linkedSocials?.twitter, - }, - lastUpdated: userPointsEntity.updatedAt, - } - - return { - result: 200, - response: userPoints, - require_reply: false, - extra: {}, - } - } catch (error) { - console.error("Error linking social account:", error) - return { - result: 500, - response: "Error linking social account", - require_reply: false, - extra: { - error: - error instanceof Error ? error.message : String(error), - }, - } - } - } } diff --git a/src/libs/blockchain/gcr/gcr.ts b/src/libs/blockchain/gcr/gcr.ts index c1c40196b..11a00b04b 100644 --- a/src/libs/blockchain/gcr/gcr.ts +++ b/src/libs/blockchain/gcr/gcr.ts @@ -366,8 +366,16 @@ export default class GCR { nonce: 0, balance: BigInt(0), identities: { - xm: new Map(), - web2: new Map(), + xm: {}, + web2: {}, + }, + points: { + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + lastUpdated: new Date(), }, } } @@ -378,8 +386,16 @@ export default class GCR { nonce: 0, balance: BigInt(0), identities: { - xm: new Map(), - web2: new Map(), + xm: {}, + web2: {}, + }, + points: { + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + lastUpdated: new Date(), }, } } @@ -544,8 +560,8 @@ export default class GCR { content: { balance: 0, identities: { - xm: new Map(), - web2: new Map(), + xm: {}, + web2: {}, }, txs: [], nonce: 0, diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index c0a13532e..7da40bfd8 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -5,6 +5,8 @@ import { Repository } from "typeorm" import { forgeToHex } from "@/libs/crypto/forgeUtils" import ensureGCRForUser from "./ensureGCRForUser" import Hashing from "@/libs/crypto/hashing" +import { IncentiveController } from "@/features/incentive/IncentiveController" +import log from "@/utilities/logger" export default class GCRIdentityRoutines { static async applyXmIdentityAdd( @@ -45,10 +47,19 @@ export default class GCRIdentityRoutines { } accountGCR.identities.xm[chain][subchain].push(normalizedAddress) + if (!simulate) { await gcrMainRepository.save(accountGCR) } + // Award incentive points for wallet linking + const incentiveController = IncentiveController.getInstance() + await incentiveController.onWalletLinked( + accountGCR.pubkey, + normalizedAddress, + chain, + ) + return { success: true, message: "Identity applied" } } @@ -67,7 +78,9 @@ export default class GCRIdentityRoutines { ? targetAddress.toLowerCase() : targetAddress - const accountGCR = await gcrMainRepository.findOneBy({ pubkey: editOperation.account }) + const accountGCR = await gcrMainRepository.findOneBy({ + pubkey: editOperation.account, + }) if (!accountGCR) { return { success: false, message: "Account not found" } @@ -122,7 +135,6 @@ export default class GCRIdentityRoutines { accountGCR.identities.web2[context] = accountGCR.identities.web2[context] || [] - const exists = accountGCR.identities.web2[context].some( (id: Web2GCRData["data"]) => id.username === data.username, ) @@ -134,7 +146,14 @@ export default class GCRIdentityRoutines { const proofOk = Hashing.sha256(data.proof) === data.proofHash if (!proofOk) { - return { success: false, message: "Sha256 proof mismatch: Expected " + data.proofHash + " but got " + Hashing.sha256(data.proof) } + return { + success: false, + message: + "Sha256 proof mismatch: Expected " + + data.proofHash + + " but got " + + Hashing.sha256(data.proof), + } } accountGCR.identities.web2[context].push(data) @@ -143,8 +162,24 @@ export default class GCRIdentityRoutines { await gcrMainRepository.save(accountGCR) } + // Award incentive points for social media linking + const incentiveController = IncentiveController.getInstance() + if (context === "twitter") { + await incentiveController.onTwitterLinked( + editOperation.account, + data.username, + ) + } else if (context === "github") { + // Future implementation for GitHub + log.info( + `GitHub linking for ${data.username}, no incentive handler yet`, + ) + } else { + log.info(`Web2 identity linked: ${context}/${data.username}`) + } + return { success: true, message: "Web2 identity added" } - } + } static async applyWeb2IdentityRemove( editOperation: any, @@ -195,7 +230,7 @@ export default class GCRIdentityRoutines { const identityEdit = structuredClone(editOperation) let operation = identityEdit.operation - if (identityEdit.isRollback) { + if (identityEdit.isRollback && operation !== "query") { operation = operation === "add" ? "remove" : "add" } @@ -207,6 +242,16 @@ export default class GCRIdentityRoutines { ? identityEdit.account : forgeToHex(identityEdit.account) + // Check for query operation first + if (operation === "query") { + // For query operations, we don't need to modify any data + // Just return success since queries are handled separately in handleIdentityRequest + return { + success: true, + message: "Query operation handled by identity request handler", + } + } + switch (identityEdit.context + operation) { case "xmadd": result = await this.applyXmIdentityAdd( diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts deleted file mode 100644 index 829c4a180..000000000 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIncentiveRoutines.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { GCRResult } from "../handleGCR" -import { GCREdit, GCREditIncentive } from "@kynesyslabs/demosdk/types" -import { forgeToHex } from "@/libs/crypto/forgeUtils" -import { IncentiveController } from "@/features/incentive/IncentiveController" -import log from "@/utilities/logger" - -export default class GCRIncentiveRoutines { - /** - * Process wallet linking incentive - */ - static async applyWalletLinkedIncentive( - editOperation: GCREditIncentive, - simulate: boolean, - ): Promise { - const { walletAddress, chain } = editOperation.data - - if (!walletAddress || !chain) { - return { - success: false, - message: "Invalid wallet linked incentive data", - } - } - - // Only actually award points if not simulating - if (!simulate) { - try { - const incentiveController = IncentiveController.getInstance() - const response = await incentiveController.onWalletLinked( - editOperation.account, - walletAddress, - chain, - ) - log.info( - `Awarded wallet linking points to ${editOperation.account} for ${chain}:${walletAddress}`, - ) - return { - success: response.result === 200 ? true : false, - message: response.response.message, - } - } catch (error: any) { - log.error(`Failed to award wallet linking points: ${error}`) - return { - success: false, - message: `Failed to award wallet linking points: ${ - error.message || String(error) - }`, - } - } - } - - // When simulating, just return success - return { - success: true, - message: "Wallet linking points would be awarded (simulation)", - } - } - - /** - * Process social media linking incentive - */ - static async applySocialLinkedIncentive( - editOperation: GCREditIncentive, - simulate: boolean, - ): Promise { - const { username, platform } = editOperation.data - - if (!username || !platform) { - return { - success: false, - message: "Invalid social linked incentive data", - } - } - - // Only actually award points if not simulating - if (!simulate) { - try { - const incentiveController = IncentiveController.getInstance() - - // Currently only Twitter is supported - if (platform.toLowerCase() === "twitter") { - const response = await incentiveController.onTwitterLinked( - editOperation.account, - username, - ) - log.info( - `Awarded Twitter linking points to ${editOperation.account} for ${username}`, - ) - return { - success: response.result === 200 ? true : false, - message: response.response.message, - } - } else { - // For future expansion to other platforms - return { - success: false, - message: `Unsupported social platform: ${platform}`, - } - } - } catch (error: any) { - log.error(`Failed to award social linking points: ${error}`) - return { - success: false, - message: `Failed to award social linking points: ${ - error.message || String(error) - }`, - } - } - } - - // When simulating, just return success - return { - success: true, - message: "Social linking points would be awarded (simulation)", - } - } - - /** - * Process get points request - */ - static async applyGetPointsIncentive( - editOperation: GCREditIncentive, - simulate: boolean, - ): Promise { - const { account } = editOperation - - if (!account) { - return { - success: false, - message: "Invalid account for get points request", - } - } - - // Only actually get points if not simulating - if (!simulate) { - try { - const incentiveController = IncentiveController.getInstance() - const response = await incentiveController.onGetPoints(account) - log.info( - `Retrieved points for ${account}: ${JSON.stringify( - response, - )}`, - ) - return { - success: true, - message: "Points retrieved successfully", - response: response, - } - } catch (error: any) { - log.error(`Failed to get points: ${error}`) - return { - success: false, - message: `Failed to get points: ${ - error.message || String(error) - }`, - } - } - } - - // When simulating, return a mock response - return { - success: true, - message: "Points would be retrieved (simulation)", - response: { - /* Empty points object for simulation */ userId: account, - totalPoints: 0, - breakdown: { web3Wallets: 0, socialAccounts: 0 }, - linkedWallets: [], - linkedSocials: {}, - lastUpdated: new Date(), - }, - } - } - - /** - * Main apply method - handles all incentive operations - */ - static async apply( - editOperation: GCREdit, - simulate: boolean, - ): Promise { - if (editOperation.type !== "incentive") { - return { - success: false, - message: "Invalid edit operation for incentive routine", - } - } - - const incentiveEdit = editOperation as GCREditIncentive - - // Convert account to hex if needed - incentiveEdit.account = - typeof incentiveEdit.account === "string" - ? incentiveEdit.account - : forgeToHex(incentiveEdit.account) - - // Handle rollbacks for incentives - if (incentiveEdit.isRollback) { - // For most incentives, there's no need to rollback points - // Points are not typically removed once awarded - return { - success: true, - message: - "Incentive rollbacks are not required - points remain awarded", - } - } - - // Process based on incentive type - switch (incentiveEdit.incentiveType) { - case "wallet_linked": - return await this.applyWalletLinkedIncentive( - incentiveEdit, - simulate, - ) - - case "social_linked": - return await this.applySocialLinkedIncentive( - incentiveEdit, - simulate, - ) - - case "get_points": - return await this.applyGetPointsIncentive( - incentiveEdit, - simulate, - ) - - default: - return { - success: false, - message: `Unsupported incentive type: ${incentiveEdit.incentiveType}`, - } - } - } -} diff --git a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts index da2b43182..edac42006 100644 --- a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts @@ -1,4 +1,12 @@ -// TODO Implement the identity manager +import { GCRMain } from "@/model/entities/GCRv2/GCR_Main" +import { DefaultChain } from "node_modules/@kynesyslabs/demosdk/build/multichain/core" +import Datasource from "src/model/datasource" +import ensureGCRForUser from "./ensureGCRForUser" +import log from "src/utilities/logger" +import { updateJSONBValue } from "./gcrJSONBHandler" +import { Cryptography } from "@kynesyslabs/demosdk/encryption" +import { StoredIdentities } from "@/model/entities/types/IdentityTypes" +import { RPCResponse } from "@kynesyslabs/demosdk/types" import { InferFromGithubPayload, InferFromWritePayload, @@ -7,7 +15,6 @@ import { InferFromSignaturePayload, InferFromTwitterPayload, } from "@kynesyslabs/demosdk/abstraction" -import { ProviderIdentities, RPCResponse } from "@kynesyslabs/demosdk/types" import { EVM, IBC, @@ -18,14 +25,6 @@ import { XRPL, } from "@kynesyslabs/demosdk/xm-localsdk" -import { GCRMain } from "@/model/entities/GCRv2/GCR_Main" -import { DefaultChain } from "node_modules/@kynesyslabs/demosdk/build/multichain/core" -import Datasource from "src/model/datasource" -import ensureGCRForUser from "./ensureGCRForUser" -import log from "src/utilities/logger" -import { updateJSONBValue } from "./gcrJSONBHandler" -import { Cryptography } from "@kynesyslabs/demosdk/encryption" - /* * Example of a payload for the gcr_routine method * payload = { @@ -39,6 +38,7 @@ import { Cryptography } from "@kynesyslabs/demosdk/encryption" * } */ +// SUPPORTED CHAINS const chains: { [key: string]: typeof DefaultChain } = { solana: SOLANA, evm: EVM, @@ -56,8 +56,6 @@ export default class IdentityManager { twitter: this.inferTwitterIdentity, } - // ? SUPPORTED CHAINS - constructor() {} // SECTION XM Identities @@ -289,22 +287,22 @@ export default class IdentityManager { const db = await Datasource.getInstance() const gcrRepository = db.getDataSource().getRepository(GCRMain) - const identities = await gcrRepository.findOne({ + const account = await gcrRepository.findOne({ where: { pubkey: address }, select: ["identities"], }) - const data = identities?.identities.xm + if (!account) { + return null + } - let result = null + const data = account.identities.xm if (chain) { - result = (data[chain] || {})[subchain] || [] + return (data[chain] || {})[subchain] || [] } else { - result = data + return data } - - return result } static async removeXmIdentity( @@ -370,16 +368,20 @@ export default class IdentityManager { // Web2 Identities static async getWeb2Identifiers( address: string, - ): Promise { + ): Promise { const db = await Datasource.getInstance() const gcrRepository = db.getDataSource().getRepository(GCRMain) - const identities = await gcrRepository.findOne({ + const account = await gcrRepository.findOne({ where: { pubkey: address }, select: ["identities"], }) - return identities?.identities.web2 + if (!account) { + return null + } + + return account.identities.web2 } static async addWeb2Identifier( @@ -422,7 +424,14 @@ export default class IdentityManager { select: ["identities"], }) - identities.identities.web2[context] = [proof] + // Create a proper Web2GCRData entry format + identities.identities.web2[context] = [ + { + username: typeof proof === "string" ? proof : "", + proof: typeof proof === "string" ? proof : "", + proofHash: "", + }, + ] await updateJSONBValue( address, diff --git a/src/libs/blockchain/gcr/gcr_routines/manageNative.ts b/src/libs/blockchain/gcr/gcr_routines/manageNative.ts index 6aeee63ce..ebfc60c53 100644 --- a/src/libs/blockchain/gcr/gcr_routines/manageNative.ts +++ b/src/libs/blockchain/gcr/gcr_routines/manageNative.ts @@ -29,12 +29,20 @@ async function setBalance( const rawData: GCRMain = { assignedTxs: [], identities: { - xm: new Map(), - web2: new Map(), + xm: {}, + web2: {}, }, balance: BigInt(balance), nonce: 0, pubkey: publicKey, + points: { + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + lastUpdated: new Date(), + }, } const db = await Datasource.getInstance() diff --git a/src/libs/blockchain/gcr/handleGCR.ts b/src/libs/blockchain/gcr/handleGCR.ts index 07b44c0dd..e8e22f559 100644 --- a/src/libs/blockchain/gcr/handleGCR.ts +++ b/src/libs/blockchain/gcr/handleGCR.ts @@ -48,7 +48,6 @@ import GCRNonceRoutines from "./gcr_routines/GCRNonceRoutines" import Chain from "../chain" import { Repository } from "typeorm" import GCRIdentityRoutines from "./gcr_routines/GCRIdentityRoutines" -import GCRIncentiveRoutines from "./gcr_routines/GCRIncentiveRoutines" export type GetNativeStatusOptions = { balance?: boolean @@ -274,8 +273,6 @@ export default class HandleGCR { repositories.main as Repository, simulate, ) - case "incentive": - return GCRIncentiveRoutines.apply(editOperation, simulate) case "assign": case "subnetsTx": // TODO implementations @@ -478,11 +475,19 @@ export default class HandleGCR { account.pubkey = pubkey account.balance = 0n account.identities = { - xm: new Map(), - web2: new Map(), + xm: {}, + web2: {}, } account.assignedTxs = [] account.nonce = 0 + account.points = { + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + lastUpdated: new Date(), + } return await repository.save(account) } } diff --git a/src/libs/network/endpointHandlers.ts b/src/libs/network/endpointHandlers.ts index 4326c4f44..0a9e1b20d 100644 --- a/src/libs/network/endpointHandlers.ts +++ b/src/libs/network/endpointHandlers.ts @@ -13,18 +13,15 @@ KyneSys Labs: https://www.kynesys.xyz/ // REVIEW Pay attention to the return types (RPCResponse) import Chain from "src/libs/blockchain/chain" -import { abstraction } from "@kynesyslabs/demosdk" import Mempool from "src/libs/blockchain/mempool_v2" import { confirmTransaction } from "src/libs/blockchain/routines/validateTransaction" import Transaction from "src/libs/blockchain/transaction" -// import { Transaction as TransactionType } from "@kynesyslabs/demosdk/types" import Cryptography from "src/libs/crypto/cryptography" import Hashing from "src/libs/crypto/hashing" import handleL2PS from "./routines/transactions/handleL2PS" import { getSharedState } from "src/utilities/sharedState" import _ from "lodash" -// NOTE Terminal kit for useful logging -import terminalkit from "terminal-kit" +import terminalKit from "terminal-kit" import { ExecutionResult, ValidityData, @@ -33,7 +30,6 @@ import { RPCResponse, IWeb2Payload, GCREdit, - GCREditIncentive, } from "@kynesyslabs/demosdk/types" import PeerManager from "src/libs/peer/PeerManager" import log from "src/utilities/logger" @@ -55,7 +51,7 @@ import { handleWeb2ProxyRequest } from "./routines/transactions/handleWeb2ProxyR import { parseWeb2ProxyRequest } from "../utils/web2RequestUtils" import handleIdentityRequest from "./routines/transactions/handleIdentityRequest" import { IdentityPayload } from "@kynesyslabs/demosdk/abstraction" -import GCRIncentiveRoutines from "../blockchain/gcr/gcr_routines/GCRIncentiveRoutines" +import { IncentiveController } from "@/features/incentive/IncentiveController" /* // ! Note: this will be removed once demosWork is in place import { NativePayload, @@ -65,7 +61,7 @@ import { } from "@kynesyslabs/demosdk/types" */ -const term = terminalkit.terminal +const term = terminalKit.terminal function isReferenceBlockAllowed(referenceBlock: number, lastBlock: number) { return ( @@ -363,6 +359,7 @@ export default class ServerHandlers { result.extra = "Error in demosWork" } break + case "native": // INFO: Just update the response text result.response = { @@ -370,16 +367,34 @@ export default class ServerHandlers { } result.success = true break + case "identity": try { - const { success, message } = await handleIdentityRequest( + const identityResult = await handleIdentityRequest( tx.content.data[1] as IdentityPayload, + tx.content.from as string, ) - const status = success ? "applied" : "not applied" - - result.success = success - result.response = { - message: message + `. Transaction ${status}.`, + const status = identityResult.success + ? "applied" + : "not applied" + + result.success = identityResult.success + + // If we have a nested response (like from points query), include it + if (identityResult.response) { + result.response = identityResult.response + result.extra = { + message: + identityResult.message + + `. Transaction ${status}.`, + } + } else { + // Default case for normal identity operations + result.response = { + message: + identityResult.message + + `. Transaction ${status}.`, + } } } catch (e) { console.error(e) @@ -392,44 +407,13 @@ export default class ServerHandlers { error: e, } } - - break - case "incentive": - try { - const incentivePayload = tx.content - .data[1] as GCREditIncentive - - incentivePayload.txhash = tx.hash - const incentiveResult = await GCRIncentiveRoutines.apply( - incentivePayload, - false, - ) - - result.success = incentiveResult.success - result.response = incentiveResult.response - result.extra = { - message: incentiveResult.message, - } - } catch (e) { - console.error(e) - log.error( - "[handleExecuteTransaction] Error in incentive: " + e, - ) - result.success = false - result.response = { - message: "Failed to process incentive request", - } - result.extra = { - error: e, - } - } break } // Only if the transaction is valid we add it to the mempool if (result.success) { // REVIEW Simulating gcr edits application as we will apply them in the consensus - const simulate = true + const simulate = false // NOTE We apply the GCREdit to the GCR and check if it is successful. If not, we return an error const editsResults = await HandleGCR.applyToTx( queriedTx, diff --git a/src/libs/network/manageGCRRoutines.ts b/src/libs/network/manageGCRRoutines.ts index 83c10dc8b..396dc04f6 100644 --- a/src/libs/network/manageGCRRoutines.ts +++ b/src/libs/network/manageGCRRoutines.ts @@ -2,7 +2,6 @@ import { RPCResponse } from "@kynesyslabs/demosdk/types" import _ from "lodash" import IdentityManager from "../blockchain/gcr/gcr_routines/identityManager" import { emptyResponse } from "./server_rpc" -import { IncentiveController } from "@/features/incentive/IncentiveController" interface GCRRoutinePayload { method: string @@ -18,37 +17,6 @@ export default async function manageGCRRoutines( // Handle the payload const { method, params } = payload - // Check if this is an incentive-related request - if (method.startsWith("incentive_")) { - // Remove the "incentive_" prefix and pass to incentive controller - const incentiveMethod = method.substring(10) - console.log( - `[GCR_ROUTINE] Processing incentive request: ${incentiveMethod} with params:`, - params, - ) - try { - return await IncentiveController.getInstance().handleIncentiveRequest( - sender, - incentiveMethod, - params, - ) - } catch (error) { - console.error( - `[GCR_ROUTINE] Error processing incentive request ${incentiveMethod}:`, - error, - ) - return { - result: 500, - response: "Error processing incentive request", - require_reply: false, - extra: { - error: - error instanceof Error ? error.message : String(error), - }, - } - } - } - switch (method) { // SECTION XM Identity Management @@ -66,18 +34,6 @@ export default async function manageGCRRoutines( sender, params[0], ) - - // If successful, award points for wallet linking - if (response.result === 200) { - const walletAddress = - params[0].target_identity.targetAddress - const chain = params[0].target_identity.chain - await IncentiveController.getInstance().onWalletLinked( - sender, - walletAddress, - chain, - ) - } } catch (error) { console.error(error) response.result = 400 @@ -116,16 +72,6 @@ export default async function manageGCRRoutines( "twitter", params[0], ) - // If successful, award points for Twitter linking - if (response.result === 200) { - // Extract Twitter handle from the proof - // This is a simplification - in reality, you'd need to parse the proof - const twitterHandle = "user_" + sender.substring(0, 8) - await IncentiveController.getInstance().onTwitterLinked( - sender, - twitterHandle, - ) - } break default: diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts index 254bbaf3d..8307250cf 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -5,14 +5,29 @@ import { } from "@kynesyslabs/demosdk/abstraction" import IdentityManager from "@/libs/blockchain/gcr/gcr_routines/identityManager" import { verifyWeb2Proof } from "@/libs/abstraction" +import { IncentiveController } from "@/features/incentive/IncentiveController" +import { verifyCloudflareTurnstileToken } from "@/utilities/turnstile" +import { RPCResponse } from "@kynesyslabs/demosdk/types" +import { forgeToHex } from "@/libs/crypto/forgeUtils" + +// Define response types for better type checking +interface IdentityResponse { + success: boolean + message: string + response?: RPCResponse +} /** * Verifies the signature in the identity payload using the appropriate handler * * @param payload - The identity payload - * @returns true if the identity request is valid, false otherwise + * @param sender - The sender's address (from the transaction) + * @returns Response with success status, message, and optional data */ -export default async function handleIdentityRequest(payload: IdentityPayload) { +export default async function handleIdentityRequest( + payload: IdentityPayload, + sender: string, +): Promise { switch (payload.method) { case "xm_identity_assign": return await IdentityManager.verifyPayload( @@ -28,6 +43,62 @@ export default async function handleIdentityRequest(payload: IdentityPayload) { success: true, message: "Identity removed", } + case "query_points": + try { + if (!sender) { + return { + success: false, + message: "Missing sender address for points query", + } + } + const senderHex = forgeToHex(sender) + const incentiveController = IncentiveController.getInstance() + const pointsResponse = await incentiveController.onGetPoints( + senderHex, + ) + + return { + success: true, + message: "Points retrieved successfully", + response: pointsResponse, + } + } catch (error) { + return { + success: false, + message: `Error querying points: ${error}`, + } + } + + //TODO To be implemented + case "verify_turnstile": + try { + const tokenData = + // @ts-expect-error - need to fix this + payload.payload as Web2CoreTargetIdentityPayload + const token = tokenData?.proof + + if (!token) { + return { + success: false, + message: "Missing Turnstile token", + } + } + + const isValid = await verifyCloudflareTurnstileToken(token) + + return { + success: isValid, + message: isValid + ? "Turnstile token verified successfully" + : "Invalid Turnstile token", + } + } catch (error) { + return { + success: false, + message: `Error verifying Turnstile token: ${error}`, + } + } + default: return { success: false, diff --git a/src/model/datasource.ts b/src/model/datasource.ts index e9926e471..4b195ee0b 100644 --- a/src/model/datasource.ts +++ b/src/model/datasource.ts @@ -24,7 +24,6 @@ import { GCRHashes } from "./entities/GCRv2/GCRHashes" import { GCRSubnetsTxs } from "./entities/GCRv2/GCRSubnetsTxs" import { GCRMain } from "./entities/GCRv2/GCR_Main" import { GCRTracker } from "./entities/GCR/GCRTracker" -import { UserPointsEntity } from "./entities/UserPoints" class Datasource { private static instance: Datasource @@ -54,7 +53,6 @@ class Datasource { GCRTracker, GCRMain, GCRTracker, - UserPointsEntity, ], synchronize: true, // set this to false in production logging: false, diff --git a/src/model/entities/GCRv2/GCR_Main.ts b/src/model/entities/GCRv2/GCR_Main.ts index 9681b0cc3..442d6153b 100644 --- a/src/model/entities/GCRv2/GCR_Main.ts +++ b/src/model/entities/GCRv2/GCR_Main.ts @@ -14,4 +14,13 @@ export class GCRMain { balance: bigint @Column({ type: "jsonb", name: "identities" }) identities: StoredIdentities + @Column({ type: "jsonb", name: "points", default: () => "'{}'" }) + points: { + totalPoints: number + breakdown: { + web3Wallets: number + socialAccounts: number + } + lastUpdated: Date + } } diff --git a/src/model/entities/UserPoints.ts b/src/model/entities/UserPoints.ts deleted file mode 100644 index 0019916c3..000000000 --- a/src/model/entities/UserPoints.ts +++ /dev/null @@ -1,37 +0,0 @@ -import "reflect-metadata" -import { - Entity, - Column, - PrimaryColumn, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm" - -@Entity() -export class UserPointsEntity { - @PrimaryColumn({ type: "varchar" }) - userId: string - - @Column({ type: "integer", default: 0 }) - totalPoints: number - - @Column({ type: "jsonb", default: {} }) - breakdown: { - web3Wallets: number - socialAccounts: number - } - - @Column({ type: "jsonb", default: [] }) - linkedWallets: string[] - - @Column({ type: "jsonb", default: {} }) - linkedSocials: { - twitter?: string - } - - @CreateDateColumn() - createdAt: Date - - @UpdateDateColumn() - updatedAt: Date -} diff --git a/src/utilities/turnstile.ts b/src/utilities/turnstile.ts new file mode 100644 index 000000000..659a1ab66 --- /dev/null +++ b/src/utilities/turnstile.ts @@ -0,0 +1,42 @@ +import axios from "axios" +import log from "@/utilities/logger" + +/** + * Verifies a Cloudflare Turnstile token + * + * @param token The Turnstile token to verify + * @returns True if the token is valid, false otherwise + */ +export async function verifyCloudflareTurnstileToken( + token: string, +): Promise { + try { + // Get secret key from environment + const secretKey = process.env.TURNSTILE_SECRET_KEY + + if (!secretKey) { + log.error("Missing TURNSTILE_SECRET_KEY in environment") + return false + } + + // Verify the token with Cloudflare's API + const response = await axios.post( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + new URLSearchParams({ + secret: secretKey, + response: token, + }), + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }, + ) + + // Return true if success + return response.data?.success === true + } catch (error) { + log.error(`Error verifying Turnstile token: ${error}`) + return false + } +} From 760de6e54be6734f6fb589823dcb4dd61934531e Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Sat, 3 May 2025 18:07:01 +0200 Subject: [PATCH 08/14] Update dependencies and refactor identity retrieval in PointSystem - Added @cosmjs/encoding dependency to package.json. - Refactored identity retrieval methods in PointSystem to use getIdentities and include GitHub as a source for web2 identities. --- package.json | 1 + src/features/incentive/PointSystem.ts | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 186937e4d..b7247e290 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "typescript": "^4.9.3" }, "dependencies": { + "@cosmjs/encoding": "^0.33.1", "@fastify/cors": "^9.0.1", "@fastify/swagger": "^8.15.0", "@fastify/swagger-ui": "^4.1.0", diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 7fbc031ba..6be7cfa74 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -29,8 +29,11 @@ export class PointSystem { linkedWallets: string[] linkedSocials: { twitter?: string } }> { - const xmIdentities = await IdentityManager.getXmIdentities(userId) - const web2Identities = await IdentityManager.getWeb2Identifiers(userId) + const xmIdentities = await IdentityManager.getIdentities(userId) + const web2Identities = await IdentityManager.getWeb2Identities( + userId, + "github", + ) const linkedWallets: string[] = [] From 4fa5fc07d185e6d6ff29b6e5d3deb077eeb87e27 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Sun, 4 May 2025 18:16:14 +0200 Subject: [PATCH 09/14] Refactor proof verification to support multiple parser types - Updated the typeof of the field variable in verifyWeb2Proof function to TwitterProofParser | GithubProofParser instead of Web2ProofParser, to get the correct instance --- src/libs/abstraction/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts index eab9336bb..bbcee09e4 100644 --- a/src/libs/abstraction/index.ts +++ b/src/libs/abstraction/index.ts @@ -1,5 +1,4 @@ import Cryptography from "../crypto/cryptography" - import { GithubProofParser } from "./web2/github" import { TwitterProofParser } from "./web2/twitter" import { type Web2ProofParser } from "./web2/parsers" @@ -12,7 +11,7 @@ import { Web2CoreTargetIdentityPayload } from "@kynesyslabs/demosdk/abstraction" * @returns true if the proof is valid, false otherwise */ export async function verifyWeb2Proof(payload: Web2CoreTargetIdentityPayload) { - let parser: typeof Web2ProofParser + let parser: typeof TwitterProofParser | typeof GithubProofParser switch (payload.context) { case "twitter": From ee7adf7b9a0c980c5fd9b8bf8f7d8b62094f29dd Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Tue, 6 May 2025 19:17:17 +0200 Subject: [PATCH 10/14] Implement account creation in PointSystem for new users - Added logic to create a new account in GCR if one does not exist for the given user ID. - Initialized total points and breakdown for new accounts. --- src/features/incentive/PointSystem.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index 6be7cfa74..d514a25a8 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -87,13 +87,25 @@ export class PointSystem { const db = await Datasource.getInstance() const gcrMainRepository = db.getDataSource().getRepository(GCRMain) - const account = await gcrMainRepository.findOneBy({ pubkey: userIdStr }) + let account = await gcrMainRepository.findOneBy({ pubkey: userIdStr }) const { linkedWallets, linkedSocials } = await this.getUserIdentitiesFromGCR(userIdStr) - // Create the response object - const userPoints = { + if (!account) { + account = await HandleGCR.createAccount(userIdStr) + account.points.totalPoints = 0 + account.points.breakdown = { + web3Wallets: 0, + socialAccounts: 0, + } + account.points.lastUpdated = new Date() + + await gcrMainRepository.save(account) + } + + // Create and return the response object + return { userId: userIdStr, totalPoints: account.points.totalPoints || 0, breakdown: { @@ -104,8 +116,6 @@ export class PointSystem { linkedSocials, lastUpdated: account.points.lastUpdated || new Date(), } - - return userPoints } /** From e7be1a9e3a6bca6f5822b1bf976090b2d105546c Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Thu, 8 May 2025 19:35:10 +0200 Subject: [PATCH 11/14] Update environment configuration and refactor identity request handling - Added TURNSTILE_SECRET_KEY to the environment configuration. - Updated handleIdentityRequest to use the correct type for Turnstile verification payload. --- .env | 2 +- .../blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts | 7 ++++--- .../routines/transactions/handleIdentityRequest.ts | 8 +++----- src/utilities/turnstile.ts | 8 +------- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.env b/.env index 4ed16dba9..1e48f732d 100644 --- a/.env +++ b/.env @@ -4,9 +4,9 @@ SERVER_PORT=53550 EXPOSED_URL=http://127.0.0.1:53550 PROD=false SIGNALING_SERVER_PORT=3005 +TURNSTILE_SECRET_KEY=0x4AAAAAABav_NndHphc_W7tGJIKMuaSBqc TWITTER_USERNAME= TWITTER_PASSWORD= TWITTER_EMAIL= - GITHUB_TOKEN= \ No newline at end of file diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index 7da40bfd8..cdd63c557 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -242,10 +242,11 @@ export default class GCRIdentityRoutines { ? identityEdit.account : forgeToHex(identityEdit.account) - // Check for query operation first + /** + * INFO: For query operations, we don't need to modify any data + * Just return success since queries are handled separately in handleIdentityRequest + */ if (operation === "query") { - // For query operations, we don't need to modify any data - // Just return success since queries are handled separately in handleIdentityRequest return { success: true, message: "Query operation handled by identity request handler", diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts index 8307250cf..64816f826 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -9,6 +9,7 @@ import { IncentiveController } from "@/features/incentive/IncentiveController" import { verifyCloudflareTurnstileToken } from "@/utilities/turnstile" import { RPCResponse } from "@kynesyslabs/demosdk/types" import { forgeToHex } from "@/libs/crypto/forgeUtils" +import { TurnstileVerificationPayload } from "node_modules/@kynesyslabs/demosdk/build/types/abstraction" // Define response types for better type checking interface IdentityResponse { @@ -68,14 +69,11 @@ export default async function handleIdentityRequest( message: `Error querying points: ${error}`, } } - - //TODO To be implemented case "verify_turnstile": try { const tokenData = - // @ts-expect-error - need to fix this - payload.payload as Web2CoreTargetIdentityPayload - const token = tokenData?.proof + payload.payload as TurnstileVerificationPayload["payload"] + const token = tokenData.token if (!token) { return { diff --git a/src/utilities/turnstile.ts b/src/utilities/turnstile.ts index 659a1ab66..8de8c2360 100644 --- a/src/utilities/turnstile.ts +++ b/src/utilities/turnstile.ts @@ -1,5 +1,4 @@ import axios from "axios" -import log from "@/utilities/logger" /** * Verifies a Cloudflare Turnstile token @@ -11,13 +10,9 @@ export async function verifyCloudflareTurnstileToken( token: string, ): Promise { try { - // Get secret key from environment const secretKey = process.env.TURNSTILE_SECRET_KEY - if (!secretKey) { - log.error("Missing TURNSTILE_SECRET_KEY in environment") - return false - } + if (!secretKey) return false // Verify the token with Cloudflare's API const response = await axios.post( @@ -36,7 +31,6 @@ export async function verifyCloudflareTurnstileToken( // Return true if success return response.data?.success === true } catch (error) { - log.error(`Error verifying Turnstile token: ${error}`) return false } } From 641f082c892c4671c2b1770e83711bace04edcd0 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Mon, 12 May 2025 15:55:06 +0200 Subject: [PATCH 12/14] Refactor Twitter linking and points management in IncentiveController and PointSystem - Simplified the onTwitterLinked method to only require userId. - Updated PointSystem to handle Twitter identities more efficiently and added platform-specific breakdown for social accounts. - Enhanced awardTwitterPoints method to check for existing points and award them accordingly. - Adjusted data structures to support multiple social platforms in points breakdown. --- src/features/incentive/IncentiveController.ts | 7 +- src/features/incentive/PointSystem.ts | 86 +++++++++++++------ .../gcr/gcr_routines/GCRIdentityRoutines.ts | 5 +- src/model/entities/GCRv2/GCR_Main.ts | 6 +- 4 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/features/incentive/IncentiveController.ts b/src/features/incentive/IncentiveController.ts index 7fcb97768..7dda4833c 100644 --- a/src/features/incentive/IncentiveController.ts +++ b/src/features/incentive/IncentiveController.ts @@ -34,11 +34,8 @@ export class IncentiveController { /** * Hook to be called after Twitter linking */ - async onTwitterLinked( - userId: string, - twitterHandle: string, - ): Promise { - return await this.pointSystem.awardTwitterPoints(userId, twitterHandle) + async onTwitterLinked(userId: string): Promise { + return await this.pointSystem.awardTwitterPoints(userId) } async onGetPoints(userId: string): Promise { diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts index d514a25a8..5f26750e5 100644 --- a/src/features/incentive/PointSystem.ts +++ b/src/features/incentive/PointSystem.ts @@ -30,18 +30,22 @@ export class PointSystem { linkedSocials: { twitter?: string } }> { const xmIdentities = await IdentityManager.getIdentities(userId) - const web2Identities = await IdentityManager.getWeb2Identities( + const twitterIdentities = await IdentityManager.getWeb2Identities( + userId, + "twitter", + ) + const githubIdentities = await IdentityManager.getWeb2Identities( userId, "github", ) const linkedWallets: string[] = [] - if (xmIdentities) { - const chains = Object.keys(xmIdentities) + if (xmIdentities?.xm) { + const chains = Object.keys(xmIdentities.xm) for (const chain of chains) { - const subChains = xmIdentities[chain] + const subChains = xmIdentities.xm[chain] const subChainKeys = Object.keys(subChains) for (const subChain of subChainKeys) { @@ -59,18 +63,8 @@ export class PointSystem { const linkedSocials: { twitter?: string } = {} - if ( - web2Identities && - typeof web2Identities === "object" && - "twitter" in web2Identities && - Array.isArray(web2Identities.twitter) && - web2Identities.twitter.length > 0 - ) { - const twitterProof = web2Identities.twitter[0] - linkedSocials.twitter = - typeof twitterProof === "string" - ? twitterProof - : twitterProof.username || "" + if (Array.isArray(twitterIdentities) && twitterIdentities.length > 0) { + linkedSocials.twitter = twitterIdentities[0].username } return { linkedWallets, linkedSocials } @@ -97,7 +91,11 @@ export class PointSystem { account.points.totalPoints = 0 account.points.breakdown = { web3Wallets: 0, - socialAccounts: 0, + socialAccounts: { + twitter: 0, + github: 0, + discord: 0, + }, } account.points.lastUpdated = new Date() @@ -110,7 +108,11 @@ export class PointSystem { totalPoints: account.points.totalPoints || 0, breakdown: { web3Wallets: account.points.breakdown?.web3Wallets || 0, - socialAccounts: account.points.breakdown?.socialAccounts || 0, + socialAccounts: account.points.breakdown?.socialAccounts || { + twitter: 0, + github: 0, + discord: 0, + }, }, linkedWallets, linkedSocials, @@ -125,6 +127,7 @@ export class PointSystem { userId: string, points: number, type: "web3Wallets" | "socialAccounts", + platform?: "twitter" | "github" | "discord", ): Promise { const db = await Datasource.getInstance() const gcrMainRepository = db.getDataSource().getRepository(GCRMain) @@ -133,7 +136,25 @@ export class PointSystem { if (!account) { const newAccount = await HandleGCR.createAccount(userId) newAccount.points.totalPoints = points - newAccount.points.breakdown[type] = points + if (type === "socialAccounts" && platform) { + newAccount.points.breakdown = { + web3Wallets: 0, + socialAccounts: { + twitter: platform === "twitter" ? points : 0, + github: platform === "github" ? points : 0, + discord: platform === "discord" ? points : 0, + }, + } + } else { + newAccount.points.breakdown = { + web3Wallets: type === "web3Wallets" ? points : 0, + socialAccounts: { + twitter: 0, + github: 0, + discord: 0, + }, + } + } newAccount.points.lastUpdated = new Date() await gcrMainRepository.save(newAccount) @@ -141,8 +162,19 @@ export class PointSystem { const oldTotal = account.points.totalPoints || 0 account.points.totalPoints = oldTotal + points - const oldCategoryPoints = account.points.breakdown[type] || 0 - account.points.breakdown[type] = oldCategoryPoints + points + if (type === "socialAccounts" && platform) { + const oldPlatformPoints = + account.points.breakdown?.socialAccounts?.[platform] || 0 + account.points.breakdown.socialAccounts[platform] = + oldPlatformPoints + points + } else { + if (type === "web3Wallets") { + const oldCategoryPoints = + account.points.breakdown.web3Wallets || 0 + account.points.breakdown.web3Wallets = + oldCategoryPoints + points + } + } account.points.lastUpdated = new Date() await gcrMainRepository.save(account) @@ -270,25 +302,22 @@ export class PointSystem { /** * Award points for linking a Twitter account * @param userId The user's Demos address - * @param twitterHandle The user's Twitter handle * @returns RPCResponse */ - async awardTwitterPoints( - userId: string, - twitterHandle: string, - ): Promise { + async awardTwitterPoints(userId: string): Promise { try { const userPointsWithIdentities = await this.getUserPointsInternal( userId, ) - if (userPointsWithIdentities.linkedSocials.twitter) { + // Check if user already has Twitter points specifically + if (userPointsWithIdentities.breakdown.socialAccounts.twitter > 0) { return { result: 200, response: { pointsAwarded: 0, totalPoints: userPointsWithIdentities.totalPoints, - message: "Twitter is already linked", + message: "Twitter points already awarded", }, require_reply: false, extra: {}, @@ -299,6 +328,7 @@ export class PointSystem { userId, pointValues.LINK_TWITTER, "socialAccounts", + "twitter", ) const updatedPoints = 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 cdd63c557..a908944db 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -131,6 +131,7 @@ export default class GCRIdentityRoutines { ): Promise { const { context, data } = editOperation.data as Web2GCRData const accountGCR = await ensureGCRForUser(editOperation.account) + accountGCR.identities.web2 = accountGCR.identities.web2 || {} accountGCR.identities.web2[context] = accountGCR.identities.web2[context] || [] @@ -143,6 +144,9 @@ export default class GCRIdentityRoutines { return { success: false, message: "Identity already exists" } } + /** + * Verify the proof + */ const proofOk = Hashing.sha256(data.proof) === data.proofHash if (!proofOk) { @@ -167,7 +171,6 @@ export default class GCRIdentityRoutines { if (context === "twitter") { await incentiveController.onTwitterLinked( editOperation.account, - data.username, ) } else if (context === "github") { // Future implementation for GitHub diff --git a/src/model/entities/GCRv2/GCR_Main.ts b/src/model/entities/GCRv2/GCR_Main.ts index 02b80fda4..863a683e4 100644 --- a/src/model/entities/GCRv2/GCR_Main.ts +++ b/src/model/entities/GCRv2/GCR_Main.ts @@ -19,7 +19,11 @@ export class GCRMain { totalPoints: number breakdown: { web3Wallets: number - socialAccounts: number + socialAccounts: { + twitter: number + github: number + discord: number + } } lastUpdated: Date } From 66a1c2a63f5a07eb37d9370b08cac2e0988d6367 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Tue, 13 May 2025 20:39:14 +0200 Subject: [PATCH 13/14] Refactor incentive point awarding logic in GCRIdentityRoutines - Moved the incentive point awarding logic for wallet and social media linking to be more readable and maintainable. - Updated the simulate flag in endpointHandlers to true --- .../gcr/gcr_routines/GCRIdentityRoutines.ts | 42 +++++++++---------- src/libs/network/endpointHandlers.ts | 2 +- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index a908944db..24436c1b5 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -50,15 +50,15 @@ export default class GCRIdentityRoutines { if (!simulate) { await gcrMainRepository.save(accountGCR) - } - // Award incentive points for wallet linking - const incentiveController = IncentiveController.getInstance() - await incentiveController.onWalletLinked( - accountGCR.pubkey, - normalizedAddress, - chain, - ) + // Award incentive points for wallet linking + const incentiveController = IncentiveController.getInstance() + await incentiveController.onWalletLinked( + accountGCR.pubkey, + normalizedAddress, + chain, + ) + } return { success: true, message: "Identity applied" } } @@ -164,21 +164,19 @@ export default class GCRIdentityRoutines { if (!simulate) { await gcrMainRepository.save(accountGCR) - } - // Award incentive points for social media linking - const incentiveController = IncentiveController.getInstance() - if (context === "twitter") { - await incentiveController.onTwitterLinked( - editOperation.account, - ) - } else if (context === "github") { - // Future implementation for GitHub - log.info( - `GitHub linking for ${data.username}, no incentive handler yet`, - ) - } else { - log.info(`Web2 identity linked: ${context}/${data.username}`) + // Award incentive points for social media linking + const incentiveController = IncentiveController.getInstance() + if (context === "twitter") { + await incentiveController.onTwitterLinked(editOperation.account) + } else if (context === "github") { + // Future implementation for GitHub + log.info( + `GitHub linking for ${data.username}, no incentive handler yet`, + ) + } else { + log.info(`Web2 identity linked: ${context}/${data.username}`) + } } return { success: true, message: "Web2 identity added" } diff --git a/src/libs/network/endpointHandlers.ts b/src/libs/network/endpointHandlers.ts index 41a3779f3..3b114c285 100644 --- a/src/libs/network/endpointHandlers.ts +++ b/src/libs/network/endpointHandlers.ts @@ -412,7 +412,7 @@ export default class ServerHandlers { // Only if the transaction is valid we add it to the mempool if (result.success) { // REVIEW Simulating gcr edits application as we will apply them in the consensus - const simulate = false + const simulate = true // NOTE We apply the GCREdit to the GCR and check if it is successful. If not, we return an error const editsResults = await HandleGCR.applyToTx( queriedTx, From e9263cec35af29a4e1a0fc2c8906a732e9e0ab73 Mon Sep 17 00:00:00 2001 From: Massoud Valipoor Date: Sat, 17 May 2025 00:11:09 +0200 Subject: [PATCH 14/14] Remove IncentiveController and refactor incentive point logic - Deleted the IncentiveController class to streamline the incentive point awarding process. - Updated GCRIdentityRoutines to utilize IncentiveManager for wallet and social media linking. - Removed references to IncentiveController in endpointHandlers and handleIdentityRequest for cleaner code structure. --- src/features/incentive/IncentiveController.ts | 44 -------------- .../gcr/gcr_routines/GCRIdentityRoutines.ts | 8 +-- .../gcr/gcr_routines/IncentiveManager.ts | 36 ++++++++++++ .../gcr/gcr_routines/SecurityManager.ts | 42 ++++++++++++++ src/libs/network/endpointHandlers.ts | 1 - src/libs/network/manageGCRRoutines.ts | 20 ++++++- .../transactions/handleIdentityRequest.ts | 57 ------------------- 7 files changed, 99 insertions(+), 109 deletions(-) delete mode 100644 src/features/incentive/IncentiveController.ts create mode 100644 src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts create mode 100644 src/libs/blockchain/gcr/gcr_routines/SecurityManager.ts diff --git a/src/features/incentive/IncentiveController.ts b/src/features/incentive/IncentiveController.ts deleted file mode 100644 index 7dda4833c..000000000 --- a/src/features/incentive/IncentiveController.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { RPCResponse } from "@kynesyslabs/demosdk/types" -import { PointSystem } from "./PointSystem" - -export class IncentiveController { - private static instance: IncentiveController - private pointSystem: PointSystem - - private constructor() { - this.pointSystem = PointSystem.getInstance() - } - - public static getInstance(): IncentiveController { - if (!IncentiveController.instance) { - IncentiveController.instance = new IncentiveController() - } - return IncentiveController.instance - } - - /** - * Hook to be called after Web3 wallet linking - */ - async onWalletLinked( - userId: string, - walletAddress: string, - chain: string, - ): Promise { - return await this.pointSystem.awardWeb3WalletPoints( - userId, - walletAddress, - chain, - ) - } - - /** - * Hook to be called after Twitter linking - */ - async onTwitterLinked(userId: string): Promise { - return await this.pointSystem.awardTwitterPoints(userId) - } - - async onGetPoints(userId: string): Promise { - return await this.pointSystem.getUserPoints(userId) - } -} diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts index 24436c1b5..71aa616c5 100644 --- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts +++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts @@ -5,8 +5,8 @@ import { Repository } from "typeorm" import { forgeToHex } from "@/libs/crypto/forgeUtils" import ensureGCRForUser from "./ensureGCRForUser" import Hashing from "@/libs/crypto/hashing" -import { IncentiveController } from "@/features/incentive/IncentiveController" import log from "@/utilities/logger" +import { IncentiveManager } from "./IncentiveManager" export default class GCRIdentityRoutines { static async applyXmIdentityAdd( @@ -52,8 +52,7 @@ export default class GCRIdentityRoutines { await gcrMainRepository.save(accountGCR) // Award incentive points for wallet linking - const incentiveController = IncentiveController.getInstance() - await incentiveController.onWalletLinked( + await IncentiveManager.walletLinked( accountGCR.pubkey, normalizedAddress, chain, @@ -166,9 +165,8 @@ export default class GCRIdentityRoutines { await gcrMainRepository.save(accountGCR) // Award incentive points for social media linking - const incentiveController = IncentiveController.getInstance() if (context === "twitter") { - await incentiveController.onTwitterLinked(editOperation.account) + await IncentiveManager.twitterLinked(editOperation.account) } else if (context === "github") { // Future implementation for GitHub log.info( diff --git a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts new file mode 100644 index 000000000..cf761e675 --- /dev/null +++ b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts @@ -0,0 +1,36 @@ +import { RPCResponse } from "@kynesyslabs/demosdk/types" +import { PointSystem } from "@/features/incentive/PointSystem" + +/** + * This class is used to manage the incentives for the user. + * It is used to award points to the user for linking their wallet and Twitter account. + * It is also used to get the points for the user. + */ +export class IncentiveManager { + private static pointSystem = PointSystem.getInstance() + /** + * Hook to be called after Web3 wallet linking + */ + static async walletLinked( + userId: string, + walletAddress: string, + chain: string, + ): Promise { + return await this.pointSystem.awardWeb3WalletPoints( + userId, + walletAddress, + chain, + ) + } + + /** + * Hook to be called after Twitter linking + */ + static async twitterLinked(userId: string): Promise { + return await this.pointSystem.awardTwitterPoints(userId) + } + + static async getPoints(address: string): Promise { + return await this.pointSystem.getUserPoints(address) + } +} diff --git a/src/libs/blockchain/gcr/gcr_routines/SecurityManager.ts b/src/libs/blockchain/gcr/gcr_routines/SecurityManager.ts new file mode 100644 index 000000000..96b317ed8 --- /dev/null +++ b/src/libs/blockchain/gcr/gcr_routines/SecurityManager.ts @@ -0,0 +1,42 @@ +import { RPCResponse } from "@kynesyslabs/demosdk/types" +import { verifyCloudflareTurnstileToken } from "@/utilities/turnstile" + +export class SecurityManager { + static async verifyTurnstile(token: string): Promise { + try { + if (!token) { + return { + result: 400, + response: false, + require_reply: false, + extra: "Missing Turnstile token", + } + } + + const isValid = await verifyCloudflareTurnstileToken(token) + + if (!isValid) { + return { + result: 400, + response: false, + require_reply: false, + extra: "Invalid Turnstile token", + } + } + + return { + result: 200, + response: true, + require_reply: false, + extra: "Turnstile token verified successfully", + } + } catch (error) { + return { + result: 400, + response: false, + require_reply: false, + extra: `Error verifying Turnstile token: ${error}`, + } + } + } +} diff --git a/src/libs/network/endpointHandlers.ts b/src/libs/network/endpointHandlers.ts index 3b114c285..9332b249b 100644 --- a/src/libs/network/endpointHandlers.ts +++ b/src/libs/network/endpointHandlers.ts @@ -50,7 +50,6 @@ import { handleWeb2ProxyRequest } from "./routines/transactions/handleWeb2ProxyR import { parseWeb2ProxyRequest } from "../utils/web2RequestUtils" import handleIdentityRequest from "./routines/transactions/handleIdentityRequest" import { IdentityPayload } from "@kynesyslabs/demosdk/abstraction" -import { IncentiveController } from "@/features/incentive/IncentiveController" /* // ! Note: this will be removed once demosWork is in place import { NativePayload, diff --git a/src/libs/network/manageGCRRoutines.ts b/src/libs/network/manageGCRRoutines.ts index eebe0d195..a4ba9df2a 100644 --- a/src/libs/network/manageGCRRoutines.ts +++ b/src/libs/network/manageGCRRoutines.ts @@ -2,6 +2,8 @@ import { RPCResponse } from "@kynesyslabs/demosdk/types" import _ from "lodash" import IdentityManager from "../blockchain/gcr/gcr_routines/identityManager" import { emptyResponse } from "./server_rpc" +import { IncentiveManager } from "../blockchain/gcr/gcr_routines/IncentiveManager" +import { SecurityManager } from "../blockchain/gcr/gcr_routines/SecurityManager" interface GCRRoutinePayload { method: string @@ -31,11 +33,25 @@ export default async function manageGCRRoutines( break case "getWeb2Identities": - response.response = await IdentityManager.getIdentities(sender, "web2") + response.response = await IdentityManager.getIdentities( + sender, + "web2", + ) break case "getXmIdentities": - response.response = await IdentityManager.getIdentities(sender, "xm") + response.response = await IdentityManager.getIdentities( + sender, + "xm", + ) + break + + case "getPoints": + response.response = await IncentiveManager.getPoints(sender) + break + + case "verifyTurnstile": + response.response = await SecurityManager.verifyTurnstile(params[0]) break // SECTION Web2 Identity Management diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts index 64816f826..840e01a87 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -5,11 +5,7 @@ import { } from "@kynesyslabs/demosdk/abstraction" import IdentityManager from "@/libs/blockchain/gcr/gcr_routines/identityManager" import { verifyWeb2Proof } from "@/libs/abstraction" -import { IncentiveController } from "@/features/incentive/IncentiveController" -import { verifyCloudflareTurnstileToken } from "@/utilities/turnstile" import { RPCResponse } from "@kynesyslabs/demosdk/types" -import { forgeToHex } from "@/libs/crypto/forgeUtils" -import { TurnstileVerificationPayload } from "node_modules/@kynesyslabs/demosdk/build/types/abstraction" // Define response types for better type checking interface IdentityResponse { @@ -44,59 +40,6 @@ export default async function handleIdentityRequest( success: true, message: "Identity removed", } - case "query_points": - try { - if (!sender) { - return { - success: false, - message: "Missing sender address for points query", - } - } - const senderHex = forgeToHex(sender) - const incentiveController = IncentiveController.getInstance() - const pointsResponse = await incentiveController.onGetPoints( - senderHex, - ) - - return { - success: true, - message: "Points retrieved successfully", - response: pointsResponse, - } - } catch (error) { - return { - success: false, - message: `Error querying points: ${error}`, - } - } - case "verify_turnstile": - try { - const tokenData = - payload.payload as TurnstileVerificationPayload["payload"] - const token = tokenData.token - - if (!token) { - return { - success: false, - message: "Missing Turnstile token", - } - } - - const isValid = await verifyCloudflareTurnstileToken(token) - - return { - success: isValid, - message: isValid - ? "Turnstile token verified successfully" - : "Invalid Turnstile token", - } - } catch (error) { - return { - success: false, - message: `Error verifying Turnstile token: ${error}`, - } - } - default: return { success: false,