Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 233 additions & 13 deletions src/features/incentive/PointSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import IdentityManager from "@/libs/blockchain/gcr/gcr_routines/identityManager"
import ensureGCRForUser from "@/libs/blockchain/gcr/gcr_routines/ensureGCRForUser"
import { Twitter } from "@/libs/identity/tools/twitter"
import { UDIdentityManager } from "@/libs/blockchain/gcr/gcr_routines/udIdentityManager"
import { SavedUdIdentity } from "@/model/entities/types/IdentityTypes"
import { AgentIdentityManager } from "@/libs/blockchain/gcr/gcr_routines/agentIdentityManager"
import { SavedUdIdentity, SavedAgentIdentity } from "@/model/entities/types/IdentityTypes"
import { UserPoints } from "@kynesyslabs/demosdk/abstraction"

const pointValues = {
Expand All @@ -20,12 +21,13 @@ const pointValues = {
LINK_DISCORD: 1,
LINK_UD_DOMAIN_DEMOS: 3,
LINK_UD_DOMAIN: 1,
LINK_AGENT: 2,
}

export class PointSystem {
private static instance: PointSystem

private constructor() {}
private constructor() { }

public static getInstance(): PointSystem {
if (!PointSystem.instance) {
Expand All @@ -43,6 +45,14 @@ export class PointSystem {
linkedUDDomains: {
[network: string]: string[]
}
linkedAgents: {
[chain: string]: Array<{
agentId: string
evmAddress: string
tokenUri?: string
timestamp: number
}>
}
}> {
const xmIdentities = await IdentityManager.getIdentities(userId)
const twitterIdentities = await IdentityManager.getWeb2Identities(
Expand All @@ -61,6 +71,7 @@ export class PointSystem {
)

const udIdentities = await IdentityManager.getUDIdentities(userId)
const agentIdentities = await AgentIdentityManager.getAgentIdentities(userId)

const linkedWallets: string[] = []
const linkedUDDomains: {
Expand Down Expand Up @@ -119,7 +130,31 @@ export class PointSystem {
}
}

return { linkedWallets, linkedSocials, linkedUDDomains }
const linkedAgents: {
[chain: string]: Array<{
agentId: string
evmAddress: string
tokenUri?: string
timestamp: number
}>
} = {}

if (Array.isArray(agentIdentities) && agentIdentities.length > 0) {
for (const agent of agentIdentities as SavedAgentIdentity[]) {
if (!linkedAgents[agent.chain]) {
linkedAgents[agent.chain] = []
}

linkedAgents[agent.chain].push({
agentId: agent.agentId,
evmAddress: agent.evmAddress,
tokenUri: agent.tokenUri,
timestamp: agent.timestamp,
})
}
}

return { linkedWallets, linkedSocials, linkedUDDomains, linkedAgents }
}

/**
Expand All @@ -135,7 +170,7 @@ export class PointSystem {
const gcrMainRepository = db.getDataSource().getRepository(GCRMain)
let account = await gcrMainRepository.findOneBy({ pubkey: userIdStr })

const { linkedWallets, linkedSocials, linkedUDDomains } =
const { linkedWallets, linkedSocials, linkedUDDomains, linkedAgents } =
await this.getUserIdentitiesFromGCR(userIdStr)

if (!account) {
Expand Down Expand Up @@ -171,12 +206,14 @@ export class PointSystem {
account.points.breakdown?.socialAccounts?.discord ?? 0,
},
udDomains: account.points.breakdown?.udDomains || {},
agents: account.points.breakdown?.agents || {},
referrals: account.points.breakdown?.referrals || 0,
demosFollow: account.points.breakdown?.demosFollow || 0,
},
linkedWallets,
linkedSocials,
linkedUDDomains,
linkedAgents,
lastUpdated: account.points.lastUpdated || new Date(),
flagged: account.flagged || null,
flaggedReason: account.flaggedReason || null,
Expand All @@ -189,7 +226,7 @@ export class PointSystem {
private async addPointsToGCR(
userId: string,
points: number,
type: "web3Wallets" | "socialAccounts" | "udDomains",
type: "web3Wallets" | "socialAccounts" | "udDomains" | "agents",
platform: string,
referralCode?: string,
twitterUserId?: string,
Expand Down Expand Up @@ -237,6 +274,14 @@ export class PointSystem {
account.points.breakdown.udDomains[platform] || 0
account.points.breakdown.udDomains[platform] =
oldDomainPoints + points
} else if (type === "agents") {
// Explicitly initialize agents if undefined
if (!account.points.breakdown.agents) {
account.points.breakdown.agents = {}
}
const oldAgentPoints =
account.points.breakdown.agents[platform] || 0
account.points.breakdown.agents[platform] = oldAgentPoints + points
}
account.points.lastUpdated = new Date()

Expand Down Expand Up @@ -376,8 +421,8 @@ export class PointSystem {
message: walletIsAlreadyLinked
? walletIsAlreadyLinkedMessage
: hasExistingWalletOnChain
? hasExistingWalletOnChainMessage
: "Points awarded for linking wallet",
? hasExistingWalletOnChainMessage
: "Points awarded for linking wallet",
},
require_reply: false,
extra: {},
Expand Down Expand Up @@ -1135,9 +1180,8 @@ export class PointSystem {
response: {
pointsAwarded: pointValue,
totalPoints: updatedPoints.totalPoints,
message: `Points awarded for linking ${
isDemosDomain ? ".demos" : "UD"
} domain`,
message: `Points awarded for linking ${isDemosDomain ? ".demos" : "UD"
} domain`,
},
require_reply: false,
extra: {},
Expand Down Expand Up @@ -1218,9 +1262,8 @@ export class PointSystem {
response: {
pointsDeducted: pointValue,
totalPoints: updatedPoints.totalPoints,
message: `Points deducted for unlinking ${
isDemosDomain ? ".demos" : "UD"
} domain`,
message: `Points deducted for unlinking ${isDemosDomain ? ".demos" : "UD"
} domain`,
},
require_reply: false,
extra: {},
Expand All @@ -1237,4 +1280,181 @@ export class PointSystem {
}
}
}

/**
* Award points for linking an ERC-8004 agent
* @param userId The user's Demos address
* @param agentId The ERC-8004 agent token ID
* @param chain The chain where agent is registered (e.g., "base.sepolia")
* @param referralCode Optional referral code
* @returns RPCResponse
*/
async awardAgentPoints(
userId: string,
agentId: string,
chain: string,
referralCode?: string,
): Promise<RPCResponse> {
try {
// Validate and normalize chain to canonical form
const canonicalChain = AgentIdentityManager.validateChain(chain)
if (!canonicalChain) {
return {
result: 400,
response: {
pointsAwarded: 0,
totalPoints: 0,
message: `Invalid chain: ${chain}. Only base.sepolia is currently supported.`,
},
require_reply: false,
extra: {},
}
}

// Create agentKey for point tracking: "chain:agentId"
const agentKey = `${canonicalChain}:${agentId}`

// Verify the agent is actually linked to this user
const account = await ensureGCRForUser(userId)
const agentIdentities = account.identities.agent?.[canonicalChain] || []
const hasAgent = agentIdentities.some(
(agent: SavedAgentIdentity) => agent.agentId === agentId,
)

if (!hasAgent) {
return {
result: 400,
response: {
pointsAwarded: 0,
totalPoints: account.points.totalPoints || 0,
message: "Error: Agent not linked to this user",
},
require_reply: false,
extra: {},
}
}

// Check if agent points already awarded
const agents = account.points.breakdown?.agents || {}
const agentAlreadyAwarded = agentKey in agents

if (agentAlreadyAwarded) {
const userPointsWithIdentities =
await this.getUserPointsInternal(userId)
return {
result: 200,
response: {
pointsAwarded: 0,
totalPoints: userPointsWithIdentities.totalPoints,
message: "Agent points already awarded",
},
require_reply: false,
extra: {},
}
}

// Award points by updating the GCR
await this.addPointsToGCR(
userId,
pointValues.LINK_AGENT,
"agents",
agentKey,
referralCode,
)

// Get updated points
const updatedPoints = await this.getUserPointsInternal(userId)

return {
result: 200,
response: {
pointsAwarded: pointValues.LINK_AGENT,
totalPoints: updatedPoints.totalPoints,
message: "Points awarded for linking ERC-8004 agent",
},
require_reply: false,
extra: {},
}
} catch (error) {
return {
result: 500,
response: "Error awarding agent points",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
},
}
}
}

/**
* Deduct points for unlinking an ERC-8004 agent
* @param userId The user's Demos address
* @param agentId The ERC-8004 agent token ID
* @param chain The chain where agent was registered
* @returns RPCResponse
*/
async deductAgentPoints(
userId: string,
agentId: string,
chain: string,
): Promise<RPCResponse> {
try {
// Create agentKey for point tracking: "chain:agentId"
const agentKey = `${chain}:${agentId}`

// Check if user has points for this agent to deduct
const account = await ensureGCRForUser(userId)
const agents = account.points.breakdown?.agents || {}
const hasAgentPoints = agentKey in agents && agents[agentKey] > 0
Comment on lines +1398 to +1410
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Chain identifier not validated before use in deductAgentPoints.

Line 1405 constructs agentKey using the raw chain parameter without validation or normalization. This is inconsistent with awardAgentPoints (line 1300), which validates and normalizes the chain using AgentIdentityManager.validateChain. If the chain format differs between award and deduct operations, the keys won't match, potentially preventing proper point deduction.

Proposed fix
     async deductAgentPoints(
         userId: string,
         agentId: string,
         chain: string,
     ): Promise<RPCResponse> {
         try {
+            // Validate and normalize chain to canonical form
+            const canonicalChain = AgentIdentityManager.validateChain(chain)
+            if (!canonicalChain) {
+                return {
+                    result: 400,
+                    response: {
+                        pointsDeducted: 0,
+                        totalPoints: 0,
+                        message: `Invalid chain: ${chain}. Only base.sepolia is currently supported.`,
+                    },
+                    require_reply: false,
+                    extra: {},
+                }
+            }
+
             // Create agentKey for point tracking: "chain:agentId"
-            const agentKey = `${chain}:${agentId}`
+            const agentKey = `${canonicalChain}:${agentId}`
 
             // Check if user has points for this agent to deduct
             const account = await ensureGCRForUser(userId)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async deductAgentPoints(
userId: string,
agentId: string,
chain: string,
): Promise<RPCResponse> {
try {
// Create agentKey for point tracking: "chain:agentId"
const agentKey = `${chain}:${agentId}`
// Check if user has points for this agent to deduct
const account = await ensureGCRForUser(userId)
const agents = account.points.breakdown?.agents || {}
const hasAgentPoints = agentKey in agents && agents[agentKey] > 0
async deductAgentPoints(
userId: string,
agentId: string,
chain: string,
): Promise<RPCResponse> {
try {
// Validate and normalize chain to canonical form
const canonicalChain = AgentIdentityManager.validateChain(chain)
if (!canonicalChain) {
return {
result: 400,
response: {
pointsDeducted: 0,
totalPoints: 0,
message: `Invalid chain: ${chain}. Only base.sepolia is currently supported.`,
},
require_reply: false,
extra: {},
}
}
// Create agentKey for point tracking: "chain:agentId"
const agentKey = `${canonicalChain}:${agentId}`
// Check if user has points for this agent to deduct
const account = await ensureGCRForUser(userId)
const agents = account.points.breakdown?.agents || {}
const hasAgentPoints = agentKey in agents && agents[agentKey] > 0
🤖 Prompt for AI Agents
In @src/features/incentive/PointSystem.ts around lines 1398 - 1410, The
deductAgentPoints method builds agentKey from the raw chain parameter without
normalization, causing mismatched keys versus awardAgentPoints; call
AgentIdentityManager.validateChain(chain) (as done in awardAgentPoints) to
validate/normalize the chain before constructing agentKey (`const agentKey =
`${normalizedChain}:${agentId}``) and use that normalized value for all
subsequent lookups and updates (e.g., hasAgentPoints, agents access, and any
writes), ensuring behavior matches awardAgentPoints and handling/propagating any
validation errors as needed.


if (!hasAgentPoints) {
const userPointsWithIdentities =
await this.getUserPointsInternal(userId)
return {
result: 200,
response: {
pointsDeducted: 0,
totalPoints: userPointsWithIdentities.totalPoints,
message: "No agent points to deduct",
},
require_reply: false,
extra: {},
}
}

// Deduct points by updating the GCR
await this.addPointsToGCR(
userId,
-pointValues.LINK_AGENT,
"agents",
agentKey,
)

// Get updated points
const updatedPoints = await this.getUserPointsInternal(userId)

return {
result: 200,
response: {
pointsDeducted: pointValues.LINK_AGENT,
totalPoints: updatedPoints.totalPoints,
message: "Points deducted for unlinking ERC-8004 agent",
},
require_reply: false,
extra: {},
}
} catch (error) {
return {
result: 500,
response: "Error deducting agent points",
require_reply: false,
extra: {
error:
error instanceof Error ? error.message : String(error),
},
}
}
}
}
Loading