-
Notifications
You must be signed in to change notification settings - Fork 2
Added ERC-8004 agent identity support #573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: testnet
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = { | ||
|
|
@@ -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) { | ||
|
|
@@ -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( | ||
|
|
@@ -61,6 +71,7 @@ export class PointSystem { | |
| ) | ||
|
|
||
| const udIdentities = await IdentityManager.getUDIdentities(userId) | ||
| const agentIdentities = await AgentIdentityManager.getAgentIdentities(userId) | ||
|
|
||
| const linkedWallets: string[] = [] | ||
| const linkedUDDomains: { | ||
|
|
@@ -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 } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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) { | ||
|
|
@@ -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, | ||
|
|
@@ -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, | ||
|
|
@@ -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() | ||
|
|
||
|
|
@@ -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: {}, | ||
|
|
@@ -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: {}, | ||
|
|
@@ -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: {}, | ||
|
|
@@ -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 | ||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find relevant files and chain definitions
fd -t f "\.ts$" | head -20Repository: 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 2Repository: 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 3Repository: kynesyslabs/node Length of output: 11263 🏁 Script executed: # Check for chain validation or verification logic
rg "chain.*verify|verify.*chain|supported.*chain" -t ts -iRepository: 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 10Repository: 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 -150Repository: kynesyslabs/node Length of output: 6525 🏁 Script executed: # Search for AgentIdentityAssignPayload validation
rg "AgentIdentityAssignPayload" -t ts -B 3 -A 10Repository: 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 -10Repository: 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 -80Repository: 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.tsRepository: 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 Add a chain validation step in 🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Chain identifier not validated before use in deductAgentPoints.
Line 1405 constructs
agentKeyusing the rawchainparameter without validation or normalization. This is inconsistent withawardAgentPoints(line 1300), which validates and normalizes the chain usingAgentIdentityManager.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
🤖 Prompt for AI Agents