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/package.json b/package.json index 1b48ad95f..a7f6f1f91 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 new file mode 100644 index 000000000..5f26750e5 --- /dev/null +++ b/src/features/incentive/PointSystem.ts @@ -0,0 +1,358 @@ +import Datasource from "../../model/datasource" +import { RPCResponse } from "@kynesyslabs/demosdk/types" +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" + +const pointValues = { + LINK_WEB3_WALLET: 2, + LINK_TWITTER: 5, +} + +export class PointSystem { + private static instance: PointSystem + + private constructor() {} + + public static getInstance(): PointSystem { + if (!PointSystem.instance) { + PointSystem.instance = new PointSystem() + } + return PointSystem.instance + } + + /** + * Get user's identities directly from the GCR + */ + private async getUserIdentitiesFromGCR(userId: string): Promise<{ + linkedWallets: string[] + linkedSocials: { twitter?: string } + }> { + const xmIdentities = await IdentityManager.getIdentities(userId) + const twitterIdentities = await IdentityManager.getWeb2Identities( + userId, + "twitter", + ) + const githubIdentities = await IdentityManager.getWeb2Identities( + userId, + "github", + ) + + const linkedWallets: string[] = [] + + if (xmIdentities?.xm) { + const chains = Object.keys(xmIdentities.xm) + + for (const chain of chains) { + const subChains = xmIdentities.xm[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) + }) + } + } + } + } + + const linkedSocials: { twitter?: string } = {} + + if (Array.isArray(twitterIdentities) && twitterIdentities.length > 0) { + linkedSocials.twitter = twitterIdentities[0].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) + let account = await gcrMainRepository.findOneBy({ pubkey: userIdStr }) + + const { linkedWallets, linkedSocials } = + await this.getUserIdentitiesFromGCR(userIdStr) + + if (!account) { + account = await HandleGCR.createAccount(userIdStr) + account.points.totalPoints = 0 + account.points.breakdown = { + web3Wallets: 0, + socialAccounts: { + twitter: 0, + github: 0, + discord: 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: { + web3Wallets: account.points.breakdown?.web3Wallets || 0, + socialAccounts: account.points.breakdown?.socialAccounts || { + twitter: 0, + github: 0, + discord: 0, + }, + }, + linkedWallets, + linkedSocials, + lastUpdated: account.points.lastUpdated || new Date(), + } + } + + /** + * Add points to the GCR for a user + */ + private async addPointsToGCR( + userId: string, + points: number, + type: "web3Wallets" | "socialAccounts", + platform?: "twitter" | "github" | "discord", + ): 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 + 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) + } else { + const oldTotal = account.points.totalPoints || 0 + account.points.totalPoints = oldTotal + 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) + } + } + + /** + * 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: { + userId: userPoints.userId, + totalPoints: userPoints.totalPoints, + breakdown: userPoints.breakdown, + linkedWallets: userPoints.linkedWallets, + linkedSocials: userPoints.linkedSocials, + lastUpdated: userPoints.lastUpdated, + }, + require_reply: false, + extra: {}, + } + } catch (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 + * @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 { + // Get current points and identities from GCR + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) + + if ( + userPointsWithIdentities.linkedWallets.includes( + `${chain}:${walletAddress}`, + ) + ) { + walletIsAlreadyLinked = true + } + + // Check if any wallet of this chain type is already linked + const hasExistingChainWallet = + userPointsWithIdentities.linkedWallets.some(wallet => + wallet.startsWith(`${chain}:`), + ) + + 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: + walletIsAlreadyLinked || hasExistingWalletOnChain + ? 400 + : 200, + response: { + 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) { + 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 + * @param userId The user's Demos address + * @returns RPCResponse + */ + async awardTwitterPoints(userId: string): Promise { + try { + const userPointsWithIdentities = await this.getUserPointsInternal( + userId, + ) + + // 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 points already awarded", + }, + require_reply: false, + extra: {}, + } + } + + await this.addPointsToGCR( + userId, + pointValues.LINK_TWITTER, + "socialAccounts", + "twitter", + ) + + const updatedPoints = await this.getUserPointsInternal(userId) + + return { + result: 200, + response: { + pointsAwarded: pointValues.LINK_TWITTER, + totalPoints: updatedPoints.totalPoints, + message: "Points awarded for linking Twitter", + }, + require_reply: false, + extra: {}, + } + } catch (error) { + return { + result: 500, + response: "Error awarding points", + require_reply: false, + extra: { + error: + error instanceof Error ? error.message : String(error), + }, + } + } + } +} 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": diff --git a/src/libs/blockchain/gcr/gcr.ts b/src/libs/blockchain/gcr/gcr.ts index 328b6bb24..dc41828f0 100644 --- a/src/libs/blockchain/gcr/gcr.ts +++ b/src/libs/blockchain/gcr/gcr.ts @@ -369,6 +369,14 @@ export default class GCR { xm: {}, web2: {}, }, + points: { + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + lastUpdated: new Date(), + }, } } } catch (e) { @@ -381,6 +389,14 @@ export default class GCR { xm: {}, web2: {}, }, + points: { + totalPoints: 0, + breakdown: { + web3Wallets: 0, + socialAccounts: 0, + }, + lastUpdated: new Date(), + }, } } return nativeStatus @@ -546,6 +562,8 @@ export default class GCR { identities: { xm: {}, web2: {}, + 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..71aa616c5 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 log from "@/utilities/logger" +import { IncentiveManager } from "./IncentiveManager" export default class GCRIdentityRoutines { static async applyXmIdentityAdd( @@ -45,8 +47,16 @@ export default class GCRIdentityRoutines { } accountGCR.identities.xm[chain][subchain].push(normalizedAddress) + if (!simulate) { await gcrMainRepository.save(accountGCR) + + // Award incentive points for wallet linking + await IncentiveManager.walletLinked( + accountGCR.pubkey, + normalizedAddress, + chain, + ) } return { success: true, message: "Identity applied" } @@ -67,7 +77,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" } @@ -118,11 +130,11 @@ 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] || [] - const exists = accountGCR.identities.web2[context].some( (id: Web2GCRData["data"]) => id.username === data.username, ) @@ -131,20 +143,42 @@ export default class GCRIdentityRoutines { return { success: false, message: "Identity already exists" } } + /** + * Verify the proof + */ 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) if (!simulate) { await gcrMainRepository.save(accountGCR) + + // Award incentive points for social media linking + if (context === "twitter") { + await IncentiveManager.twitterLinked(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" } - } + } static async applyWeb2IdentityRemove( editOperation: any, @@ -195,7 +229,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 +241,17 @@ export default class GCRIdentityRoutines { ? identityEdit.account : forgeToHex(identityEdit.account) + /** + * 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") { + 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/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/blockchain/gcr/gcr_routines/identityManager.ts b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts index e1fe26707..062a066a8 100644 --- a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts +++ b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts @@ -1,4 +1,6 @@ -// TODO Implement the identity manager +import { DefaultChain } from "node_modules/@kynesyslabs/demosdk/build/multichain/core" +import ensureGCRForUser from "./ensureGCRForUser" +import log from "src/utilities/logger" import { InferFromWritePayload, InferFromSignatureTargetIdentityPayload, @@ -15,10 +17,6 @@ import { BTC, } from "@kynesyslabs/demosdk/xm-localsdk" -import { DefaultChain } from "node_modules/@kynesyslabs/demosdk/build/multichain/core" -import ensureGCRForUser from "./ensureGCRForUser" -import log from "src/utilities/logger" - /* * Example of a payload for the gcr_routine method * payload = { @@ -32,6 +30,7 @@ import log from "src/utilities/logger" * } */ +// SUPPORTED CHAINS const chains: { [key: string]: typeof DefaultChain } = { solana: SOLANA, evm: EVM, diff --git a/src/libs/blockchain/gcr/gcr_routines/manageNative.ts b/src/libs/blockchain/gcr/gcr_routines/manageNative.ts index f7eebd02f..9c3004a85 100644 --- a/src/libs/blockchain/gcr/gcr_routines/manageNative.ts +++ b/src/libs/blockchain/gcr/gcr_routines/manageNative.ts @@ -33,6 +33,14 @@ async function setBalance( 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 c3be67638..7118c99d6 100644 --- a/src/libs/blockchain/gcr/handleGCR.ts +++ b/src/libs/blockchain/gcr/handleGCR.ts @@ -69,9 +69,10 @@ export type GetNativeSubnetsTxsOptions = { txData?: boolean } -export type GCRResult = { +export interface GCRResult { success: boolean message: string + response?: any } // ? Maybe sanitize the options? @@ -478,6 +479,14 @@ export default class HandleGCR { } 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 d78ae4506..eb635060b 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, @@ -51,7 +48,6 @@ 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 { NativeBridgeOperation, NativeBridgeOperationCompiled } from "@kynesyslabs/demosdk/bridge" @@ -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,7 +407,6 @@ export default class ServerHandlers { error: e, } } - break case "nativeBridge": diff --git a/src/libs/network/manageGCRRoutines.ts b/src/libs/network/manageGCRRoutines.ts index d3e38a5cb..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 @@ -16,6 +18,7 @@ export default async function manageGCRRoutines( response.result = 200 // Handle the payload const { method, params } = payload + switch (method) { // SECTION XM Identity Management @@ -30,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 254bbaf3d..840e01a87 100644 --- a/src/libs/network/routines/transactions/handleIdentityRequest.ts +++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts @@ -5,14 +5,26 @@ import { } from "@kynesyslabs/demosdk/abstraction" import IdentityManager from "@/libs/blockchain/gcr/gcr_routines/identityManager" import { verifyWeb2Proof } from "@/libs/abstraction" +import { RPCResponse } from "@kynesyslabs/demosdk/types" + +// 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( diff --git a/src/model/entities/GCRv2/GCR_Main.ts b/src/model/entities/GCRv2/GCR_Main.ts index a2eb7f246..863a683e4 100644 --- a/src/model/entities/GCRv2/GCR_Main.ts +++ b/src/model/entities/GCRv2/GCR_Main.ts @@ -14,4 +14,17 @@ 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: { + twitter: number + github: number + discord: number + } + } + lastUpdated: Date + } } diff --git a/src/utilities/turnstile.ts b/src/utilities/turnstile.ts new file mode 100644 index 000000000..8de8c2360 --- /dev/null +++ b/src/utilities/turnstile.ts @@ -0,0 +1,36 @@ +import axios from "axios" + +/** + * 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 { + const secretKey = process.env.TURNSTILE_SECRET_KEY + + if (!secretKey) 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) { + return false + } +}