Skip to content
Open
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
114 changes: 87 additions & 27 deletions src/Agents/tools/strategyRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> {
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<string, VoteRecord[]> = 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<StrategyRegistryPayload> {
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 YieldAggregator 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: "64character hexadecimal Stellar AMM liquidity pool ID",
required: false,
pattern: "^[0-9a-f]{64}$",
},
Expand All @@ -36,11 +99,18 @@ export class StrategyRegistryTool extends BaseTool<StrategyRegistryPayload> {
"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",
Expand All @@ -54,23 +124,20 @@ export class StrategyRegistryTool extends BaseTool<StrategyRegistryPayload> {
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 64character hexadecimal string");
}

return { valid: errors.length === 0, errors };
}

/** Core execution logic */
async execute(payload: StrategyRegistryPayload): Promise<ToolResult> {
const validation = this.validate(payload);
if (!validation.valid) {
Expand All @@ -90,8 +157,8 @@ export class StrategyRegistryTool extends BaseTool<StrategyRegistryPayload> {
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: {
Expand All @@ -102,7 +169,12 @@ export class StrategyRegistryTool extends BaseTool<StrategyRegistryPayload> {
};
}

// ----- 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: {
Expand All @@ -113,18 +185,6 @@ export class StrategyRegistryTool extends BaseTool<StrategyRegistryPayload> {
};
}

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);
Expand Down