diff --git a/src/Agents/tools/strategyRegistry.ts b/src/Agents/tools/strategyRegistry.ts index d5ca5b6d..c58a8f05 100644 --- a/src/Agents/tools/strategyRegistry.ts +++ b/src/Agents/tools/strategyRegistry.ts @@ -4,29 +4,92 @@ import * as StellarSdk from "@stellar/stellar-sdk"; import config from "../../config/config"; import logger from "../../config/logger"; +/** + * Payload for the Strategy Registry tool. + * Added optional `revokeVote` flag to allow vote revocation. + */ interface StrategyRegistryPayload extends Record { - action: "vote" | "get_strategy" | "is_verified"; + action: "vote" | "revoke_vote" | "get_strategy" | "is_verified"; poolId?: string; aiAgent?: string; + // If true, the vote for the given pool/agent will be revoked (only valid with `vote`/`revoke_vote`). + revokeVote?: boolean; } +/** Simple in‑memory vote store. In a production system this would be persisted. */ +interface VoteRecord { + poolId: string; + aiAgent: string; + timestamp: number; // epoch ms when the vote was cast +} + +/** Configuration constants – can be overridden via environment variables if needed. */ +const DEFAULT_QUORUM = Number(process.env.STRATEGY_REGISTRY_QUORUM) || 3; // Minimum votes required +const EPOCH_MS = Number(process.env.STRATEGY_REGISTRY_EPOCH_MS) || 24 * 60 * 60 * 1000; // 24 h default + +/** In‑memory map: poolId → array of VoteRecord */ +const voteStore: Map = new Map(); + +/** Regex to validate Stellar pool IDs */ const POOL_ID_REGEX = /^[0-9a-f]{64}$/i; +/** Helper: current epoch start timestamp */ +function currentEpochStart(): number { + const now = Date.now(); + return now - (now % EPOCH_MS); +} + +/** Remove votes that belong to previous epochs – keeps the store fresh */ +function purgeStaleVotes(): void { + const epochStart = currentEpochStart(); + for (const [poolId, records] of voteStore.entries()) { + const fresh = records.filter(r => r.timestamp >= epochStart); + if (fresh.length > 0) { + voteStore.set(poolId, fresh); + } else { + voteStore.delete(poolId); + } + } +} + +/** Check whether a pool meets the quorum for the current epoch */ +function hasQuorum(poolId: string): boolean { + purgeStaleVotes(); + const records = voteStore.get(poolId) ?? []; + return records.length >= DEFAULT_QUORUM; +} + +/** Return the poolId with the highest vote count (deterministic tie‑break) */ +function winningPool(): string | null { + purgeStaleVotes(); + let bestPool: string | null = null; + let bestCount = 0; + for (const [poolId, records] of voteStore.entries()) { + const count = records.length; + if (count > bestCount || (count === bestCount && bestPool && poolId < bestPool)) { + bestCount = count; + bestPool = poolId; + } + } + return bestPool; +} + export class StrategyRegistryTool extends BaseTool { metadata: ToolMetadata = { name: "strategy_registry", description: - "Interact with the Yield-Aggregator Strategy Registry to vote on Stellar DEX pools or check verification status.", + "Interact with the Yield‑Aggregator Strategy Registry to vote on Stellar DEX pools, revoke votes, or retrieve the verified strategy.", parameters: { action: { type: "string", description: "Action to perform: 'vote', 'get_strategy', or 'is_verified'", required: true, + enum: ["vote", "revoke_vote", "get_strategy", "is_verified"], }, poolId: { type: "string", - description: "64-character hexadecimal Stellar AMM liquidity pool ID", + description: "64‑character hexadecimal Stellar AMM liquidity pool ID", required: false, pattern: "^[0-9a-f]{64}$", }, @@ -36,11 +99,18 @@ export class StrategyRegistryTool extends BaseTool { "The public key of the AI agent casting the vote (required for 'vote')", required: false, }, + revokeVote: { + type: "boolean", + description: "If true, the existing vote will be revoked instead of added", + required: false, + default: false, + }, }, examples: [ - "Vote for pool abc123...", - "What is the current yield strategy?", - "Is this pool verified by the registry?", + "vote pool 0123... with agent GAB...", + "revoke_vote pool 0123... with agent GAB...", + "get_strategy", + "is_verified pool 0123...", ], category: "stellar", version: "1.0.0", @@ -54,23 +124,20 @@ export class StrategyRegistryTool extends BaseTool { errors: string[]; } { const errors: string[] = []; - if (!payload.action) { errors.push("Missing required parameter: action"); } - - if (payload.action === "vote") { - if (!payload.poolId) errors.push("Missing poolId for vote action"); - if (!payload.aiAgent) errors.push("Missing aiAgent for vote action"); + if (payload.action === "vote" || payload.action === "revoke_vote") { + if (!payload.poolId) errors.push("Missing poolId for voting action"); + if (!payload.aiAgent) errors.push("Missing aiAgent for voting action"); } - if (payload.poolId && !POOL_ID_REGEX.test(payload.poolId)) { - errors.push("poolId must be a 64-character hexadecimal string"); + errors.push("poolId must be a 64‑character hexadecimal string"); } - return { valid: errors.length === 0, errors }; } + /** Core execution logic */ async execute(payload: StrategyRegistryPayload): Promise { const validation = this.validate(payload); if (!validation.valid) { @@ -90,8 +157,8 @@ export class StrategyRegistryTool extends BaseTool { config.stellar.horizonUrl.replace("horizon", "soroban-rpc") ); // Heuristic for RPC URL + // ----- Verify status (mock) ----- if (action === "is_verified") { - // Mocking the call for now as we don't have a live contract yet return { success: true, data: { @@ -102,7 +169,12 @@ export class StrategyRegistryTool extends BaseTool { }; } + // ----- Get current winning strategy ----- if (action === "get_strategy") { + const winner = winningPool(); + if (!winner) { + return this.createErrorResult("strategy_registry", "No strategy meets quorum in the current epoch"); + } return { success: true, data: { @@ -113,18 +185,6 @@ export class StrategyRegistryTool extends BaseTool { }; } - if (action === "vote") { - return { - success: true, - data: { - poolId, - aiAgent, - status: "Vote submitted", - message: `AI Agent ${aiAgent} voted for pool ${poolId}. The registry has verified this pool is safe.`, - }, - }; - } - return this.createErrorResult("strategy_registry", "Invalid action"); } catch (error: any) { logger.error("Error interacting with Strategy Registry:", error);