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),
},
}
}
}
Comment on lines 1284 to 1459
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find relevant files and chain definitions
fd -t f "\.ts$" | head -20

Repository: kynesyslabs/node

Length of output: 966


🏁 Script executed:

# Search for chain identifier patterns and definitions
rg "base\.sepolia|chain.*identity|SUPPORTED_CHAINS" -t ts -B 2 -A 2

Repository: kynesyslabs/node

Length of output: 6686


🏁 Script executed:

# Look for SavedAgentIdentity type definition and how chain is used
rg "SavedAgentIdentity|identities\.agent" -t ts -B 3 -A 3

Repository: kynesyslabs/node

Length of output: 11263


🏁 Script executed:

# Check for chain validation or verification logic
rg "chain.*verify|verify.*chain|supported.*chain" -t ts -i

Repository: kynesyslabs/node

Length of output: 1862


🏁 Script executed:

# Look at the SUPPORTED CHAINS definition in identityManager.ts
rg "SUPPORTED_CHAINS|SUPPORTED CHAINS" -t ts -B 5 -A 10

Repository: kynesyslabs/node

Length of output: 1281


🏁 Script executed:

# Check agentIdentityManager for chain validation during agent registration
cat -n src/libs/blockchain/gcr/gcr_routines/agentIdentityManager.ts | head -150

Repository: kynesyslabs/node

Length of output: 6525


🏁 Script executed:

# Search for AgentIdentityAssignPayload validation
rg "AgentIdentityAssignPayload" -t ts -B 3 -A 10

Repository: kynesyslabs/node

Length of output: 6828


🏁 Script executed:

# Look for any supported chains configuration/constants
fd -t f "chain" | xargs grep -l "SUPPORTED\|supported.*chain" 2>/dev/null | head -10

Repository: kynesyslabs/node

Length of output: 42


🏁 Script executed:

# Get the rest of the agentIdentityManager.ts file to see chain validation
cat -n src/libs/blockchain/gcr/gcr_routines/agentIdentityManager.ts | sed -n '150,300p'

Repository: kynesyslabs/node

Length of output: 6308


🏁 Script executed:

# Check if there's explicit chain validation in verifyPayload
rg "chain" src/libs/blockchain/gcr/gcr_routines/agentIdentityManager.ts -B 2 -A 2 | head -80

Repository: kynesyslabs/node

Length of output: 1799


🏁 Script executed:

# Look at the full flow to see where chain is validated
sed -n '150,400p' src/libs/blockchain/gcr/gcr_routines/agentIdentityManager.ts

Repository: kynesyslabs/node

Length of output: 6780


Add explicit chain validation to prevent spoofing of agent's home chain.

The agent identity is always verified on-chain for Base Sepolia (hardcoded in AgentIdentityManager.verifyAgentOwnership), but the chain parameter from the payload is accepted without validation and used directly as a storage key in identities.agent[payload.chain]. This creates a mismatch: an agent verified on Base Sepolia could be stored under an arbitrary chain key (e.g., "ethereum.mainnet"), and awardAgentPoints would trust the caller-provided chain to retrieve it.

Add a chain validation step in AgentIdentityManager.verifyPayload to enforce that chain === "base.sepolia" (or map to a canonical chain identifier), then apply the same validation in awardAgentPoints before using the chain parameter for identity lookup.

🤖 Prompt for AI Agents
In @src/features/incentive/PointSystem.ts around lines 1284 - 1444,
AgentIdentityManager.verifyPayload currently allows caller-supplied chain
values; update it to explicitly validate or normalize the chain (e.g., require
chain === "base.sepolia" or map known aliases to the canonical "base.sepolia")
and reject/throw on mismatch so verifyAgentOwnership cannot be spoofed; then in
PointSystem.awardAgentPoints validate/normalize the incoming chain parameter (or
call the new AgentIdentityManager validation helper) before using it to read
account.identities.agent?.[chain] and before constructing agentKey so
stored/looked-up keys always use the canonical chain identifier.

}
Loading