diff --git a/package.json b/package.json index c89eac44..d1eb6692 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@orca-so/whirlpools-sdk": "^0.13.12", "@pythnetwork/hermes-client": "^1.3.0", "@raydium-io/raydium-sdk-v2": "0.1.95-alpha", + "@solana/spl-governance": "^0.3.28", "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.98.0", "@sqds/multisig": "^2.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cd5d735..9867a2ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: '@raydium-io/raydium-sdk-v2': specifier: 0.1.95-alpha version: 0.1.95-alpha(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) + '@solana/spl-governance': + specifier: ^0.3.28 + version: 0.3.28(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@solana/spl-token': specifier: ^0.4.9 version: 0.4.9(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) @@ -1118,6 +1121,9 @@ packages: peerDependencies: '@solana/web3.js': ^1.50.1 + '@solana/spl-governance@0.3.28': + resolution: {integrity: sha512-CUi1hMvzId2rAtMFTlxMwOy0EmFeT0VcmiC+iQnDhRBuM8LLLvRrbTYBWZo3xIvtPQW9HfhVBoL7P/XNFIqYVQ==} + '@solana/spl-token-group@0.0.4': resolution: {integrity: sha512-7+80nrEMdUKlK37V6kOe024+T7J4nNss0F8LQ9OOPYdWCCfJmsGUzVx2W3oeizZR4IHM6N4yC9v1Xqwc3BTPWw==} engines: {node: '>=16'} @@ -1219,6 +1225,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/bn.js@4.11.6': + resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==} + '@types/bn.js@5.1.6': resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==} @@ -1593,6 +1602,9 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + borsh@0.3.1: + resolution: {integrity: sha512-gJoSTnhwLxN/i2+15Y7uprU8h3CKI+Co4YKZKvrGYUy0FwHWM20x5Sx7eU8Xv4HQqV+7rb4r3P7K1cBIQe3q8A==} + borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -5474,6 +5486,21 @@ snapshots: - supports-color - utf-8-validate + '@solana/spl-governance@0.3.28(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + dependencies: + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + axios: 1.7.9 + bignumber.js: 9.1.2 + bn.js: 5.2.1 + borsh: 0.3.1 + bs58: 4.0.1 + superstruct: 0.15.5 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - utf-8-validate + '@solana/spl-token-group@0.0.4(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs': 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22) @@ -5809,6 +5836,10 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/bn.js@4.11.6': + dependencies: + '@types/node': 22.10.5 + '@types/bn.js@5.1.6': dependencies: '@types/node': 22.10.5 @@ -6291,6 +6322,13 @@ snapshots: transitivePeerDependencies: - supports-color + borsh@0.3.1: + dependencies: + '@types/bn.js': 4.11.6 + bn.js: 5.2.1 + bs58: 4.0.1 + text-encoding-utf-8: 1.0.2 + borsh@0.7.0: dependencies: bn.js: 5.2.1 diff --git a/src/actions/index.ts b/src/actions/index.ts index e878aa1c..ded8be29 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -30,6 +30,10 @@ import launchPumpfunTokenAction from "./pumpfun/launchPumpfunToken"; import getWalletAddressAction from "./agent/getWalletAddress"; import flashOpenTradeAction from "./flash/flashOpenTrade"; import flashCloseTradeAction from "./flash/flashCloseTrade"; +import manageVoteDelegationAction from "./realm/manageVotingDelegation"; +import monitorVotingOutcomesAction from "./realm/manageVotingOutcomes"; +import trackVotingPowerAction from "./realm/trackVotingPower"; +import castGovernanceVoteAction from "./realm/castGovernanceVoteAction"; export const ACTIONS = { WALLET_ADDRESS_ACTION: getWalletAddressAction, @@ -65,6 +69,10 @@ export const ACTIONS = { LAUNCH_PUMPFUN_TOKEN_ACTION: launchPumpfunTokenAction, FLASH_OPEN_TRADE_ACTION: flashOpenTradeAction, FLASH_CLOSE_TRADE_ACTION: flashCloseTradeAction, + MANAGE_VOTING_DELEGATION: manageVoteDelegationAction, + MANAGE_VOTING_OUTCOMES: monitorVotingOutcomesAction, + TRACK_VOTING_POWER: trackVotingPowerAction, + CAST_VOTE_ACTION: castGovernanceVoteAction, }; export type { Action, ActionExample, Handler } from "../types/action"; diff --git a/src/actions/realm/castGovernanceVoteAction.ts b/src/actions/realm/castGovernanceVoteAction.ts new file mode 100644 index 00000000..3b04bacb --- /dev/null +++ b/src/actions/realm/castGovernanceVoteAction.ts @@ -0,0 +1,71 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { z } from "zod"; +import { SolanaAgentKit } from "../../agent"; +import { castProposalVote } from "../../tools/realm"; + +const castGovernanceVoteAction: Action = { + name: "CAST_GOVERNANCE_VOTE", + similes: [ + "vote on proposal", + "cast governance vote", + "submit proposal vote", + "vote on governance", + "vote on dao proposal", + ], + description: `Cast a vote on a governance proposal in a Solana DAO. + Inputs ( input is a JSON string ): + realmAccount: string, eg "7nxQB..." (required) - The public key of the realm + proposalAccount: string, eg "8x2dR..." (required) - The public key of the proposal + voteType: string, either "yes" or "no" (required) - The type of vote to cast`, + + examples: [ + [ + { + input: { + realmAccount: "7nxQB1nGrqk8WKXeFDR6ZUaQtYjV7HMsAGWgwtGHwmQU", + proposalAccount: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk", + voteType: "yes", + }, + output: { + status: "success", + signature: "2GjfL3N9E4cHp7WhDZRkx7oF2J9m3Sf5hT6zRHcVWUjp", + message: "Vote cast successfully", + }, + explanation: "Cast a yes vote on a governance proposal", + }, + ], + ], + + schema: z.object({ + realmAccount: z.string().min(32, "Invalid realm account address"), + proposalAccount: z.string().min(32, "Invalid proposal account address"), + voteType: z.enum(["yes", "no"], { + description: "Vote type must be either 'yes' or 'no'", + }), + }), + + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const signature = await castProposalVote( + agent, + new PublicKey(input.realmAccount), + new PublicKey(input.proposalAccount), + input.voteType, + ); + + return { + status: "success", + signature, + message: "Vote cast successfully", + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to cast vote: ${error.message}`, + }; + } + }, +}; + +export default castGovernanceVoteAction; \ No newline at end of file diff --git a/src/actions/realm/index.ts b/src/actions/realm/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/actions/realm/manageVotingDelegation.ts b/src/actions/realm/manageVotingDelegation.ts new file mode 100644 index 00000000..84882061 --- /dev/null +++ b/src/actions/realm/manageVotingDelegation.ts @@ -0,0 +1,81 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { z } from "zod"; +import { manageVoteDelegation } from "../../tools/realm/manage_vote_delegation"; +import { SolanaAgentKit } from "../../agent"; + +const manageVoteDelegationAction: Action = { + name: "MANAGE_VOTE_DELEGATION", + similes: [ + "delegate governance votes", + "assign voting delegate", + "transfer voting rights", + "set voting delegate", + "delegate dao voting power", + ], + description: `Delegate voting power to another wallet in a governance realm. + + Inputs ( input is a JSON string ): + realm: string, eg "7nxQB..." (required) - The public key of the realm + governingTokenMint: string, eg "8x2dR..." (required) - The mint of the governing token + delegate: string, eg "9aUn5..." (required) - The wallet address to delegate voting power to`, + + examples: [ + [ + { + input: { + realm: "7nxQB1nGrqk8WKXeFDR6ZUaQtYjV7HMsAGWgwtGHwmQU", + governingTokenMint: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk", + delegate: "9aUn5swQzUTRanaaTwmszxiv89cvFwUCjEBv1vZCoT1u", + }, + output: { + status: "success", + message: "Successfully delegated voting power", + signature: "2GjfL3N9E4cHp7WhDZRkx7oF2J9m3Sf5hT6zRHcVWUjp", + }, + explanation: "Delegate governance voting power to another wallet", + }, + ], + ], + + schema: z.object({ + realm: z.string().min(32, "Invalid realm address"), + governingTokenMint: z + .string() + .min(32, "Invalid governing token mint address"), + delegate: z.string().min(32, "Invalid delegate wallet address"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const signature = await manageVoteDelegation( + agent, + new PublicKey(input.realm), + new PublicKey(input.governingTokenMint), + new PublicKey(input.delegate), + ); + + return { + status: "success", + message: "Successfully delegated voting power", + signature, + }; + } catch (error: any) { + let errorMessage = error.message; + + // Handle specific error cases with user-friendly messages + if (error.message.includes("Account not found")) { + errorMessage = + "No token owner record found - you need to deposit tokens first"; + } else if (error.message.includes("Invalid delegate")) { + errorMessage = "The provided delegate address is invalid"; + } + + return { + status: "error", + message: errorMessage, + }; + } + }, +}; + +export default manageVoteDelegationAction; diff --git a/src/actions/realm/manageVotingOutcomes.ts b/src/actions/realm/manageVotingOutcomes.ts new file mode 100644 index 00000000..589699d1 --- /dev/null +++ b/src/actions/realm/manageVotingOutcomes.ts @@ -0,0 +1,57 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { SolanaAgentKit } from "../../agent"; +import { z } from "zod"; +import { monitorVotingOutcomes } from "../../tools/realm/monitor_voting_outcomes"; + +const monitorVotingOutcomesAction: Action = { + name: "MONITOR_VOTING_OUTCOMES", + similes: [ + "check voting results", + "view proposal outcomes", + "get vote results", + "show proposal status", + ], + description: `Monitor and retrieve the voting outcomes of a proposal within a governance realm.`, + examples: [ + [ + { + input: { + realm: "ASNJL4uXNNiNuC7XibsKw5VfB3Ce9wNsXwqGsxqrNbF4", + }, + output: { + status: "success", + proposalState: "Completed", + voteResults: { + yes: 1200, + no: 300, + abstain: 100, + }, + }, + explanation: "Get the voting results of a proposal after voting ends", + }, + ], + ], + schema: z.object({ + realm: z.string(), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + console.log("Monitoring voting outcomes for realm:", input.realm); + const outcome = await monitorVotingOutcomes( + agent, + new PublicKey(input.realm), + ); + + return { + status: "success", + proposalState: outcome.account.state.toString(), + voteResults: { + yes: outcome.account.yesVotesCount, + no: outcome.account.noVotesCount, + abstain: outcome.account.abstainVoteWeight, + }, + }; + }, +}; + +export default monitorVotingOutcomesAction; diff --git a/src/actions/realm/trackVotingPower.ts b/src/actions/realm/trackVotingPower.ts new file mode 100644 index 00000000..df0c8b32 --- /dev/null +++ b/src/actions/realm/trackVotingPower.ts @@ -0,0 +1,48 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { SolanaAgentKit } from "../../agent"; +import { z } from "zod"; +import { trackVotingPower } from "../../tools/realm/track_voting_power"; + +const trackVotingPowerAction: Action = { + name: "TRACK_VOTING_POWER", + similes: [ + "check voting power", + "view voting power", + "get governance voting power", + "show voting power", + ], + description: `Get the voting power of a given wallet in a specific governance realm. + If you provide the wallet's token address, the voting power will be calculated accordingly.`, + examples: [ + [ + { + input: { + realm: "6k4tvNjkL1shSmhFjdUzGFyQFC4ZKTpaXnKds1z9jEwF", + }, + output: { + status: "success", + votingPower: 1500, + }, + explanation: + "Get the voting power for a specific wallet and governance realm", + }, + ], + ], + schema: z.object({ + realm: z.string(), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + const votingPower = await trackVotingPower( + agent, + new PublicKey(input.realmID), + ); + + return { + status: "success", + votingPower, + }; + }, +}; + +export default trackVotingPowerAction; diff --git a/src/agent/index.ts b/src/agent/index.ts index 25037c99..343fd553 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -1,4 +1,4 @@ -import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { BN } from "@coral-xyz/anchor"; import bs58 from "bs58"; import Decimal from "decimal.js"; @@ -92,6 +92,10 @@ import { create_proposal } from "../tools/squads_multisig/create_proposal"; import { approve_proposal } from "../tools/squads_multisig/approve_proposal"; import { execute_transaction } from "../tools/squads_multisig/execute_proposal"; import { reject_proposal } from "../tools/squads_multisig/reject_proposal"; +import { monitorVotingOutcomes } from "../tools/realm/monitor_voting_outcomes"; +import { trackVotingPower } from "../tools/realm/track_voting_power"; +import { ProgramAccount, Proposal } from "@solana/spl-governance"; +import { castProposalVote, manageVoteDelegation } from "../tools/realm"; /** * Main class for interacting with Solana blockchain @@ -655,4 +659,32 @@ export class SolanaAgentKit { ): Promise { return execute_transaction(this, transactionIndex); } + + async castProposalVote( + realmID: PublicKey, + proposalID: PublicKey, + voteType: "yes" | "no", + ) { + return castProposalVote(this, realmID, proposalID, voteType); + } + + async monitorVotingOutcomes( + proposalId: PublicKey, + ): Promise> { + return monitorVotingOutcomes(this, proposalId); + } + + async trackVotingPower(tokenOwnerRecordPk: PublicKey): Promise { + return trackVotingPower(this, tokenOwnerRecordPk); + } + + async manageVoteDelegation( + realm: PublicKey, + governingTokenMint: PublicKey, + delegate: PublicKey, + ): Promise { + return manageVoteDelegation( + this, realm, governingTokenMint, delegate + ); + } } diff --git a/src/constants/index.ts b/src/constants/index.ts index 69965bf8..9b17c563 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -33,3 +33,11 @@ export const DEFAULT_OPTIONS = { export const JUP_API = "https://quote-api.jup.ag/v6"; export const JUP_REFERRAL_ADDRESS = "REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3"; + + +/** + * Governance Program Address + */ + +export const GOVERNANCE_PROGRAM_ADDRESS = + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"; diff --git a/src/langchain/index.ts b/src/langchain/index.ts index c2e5aff5..e36bebc8 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -23,6 +23,7 @@ export * from "./3land"; export * from "./tiplink"; export * from "./sns"; export * from "./lightprotocol"; +export * from "./realm"; import { SolanaAgentKit } from "../agent"; import { @@ -85,6 +86,7 @@ import { SolanaFlashCloseTrade, } from "./index"; + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), diff --git a/src/langchain/realm/cast_proposal_vote.ts b/src/langchain/realm/cast_proposal_vote.ts new file mode 100644 index 00000000..8c2a413d --- /dev/null +++ b/src/langchain/realm/cast_proposal_vote.ts @@ -0,0 +1,50 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; +import { PublicKey } from "@solana/web3.js"; + +export class SolanaCastGovernanceVoteTool extends Tool { + name = "solana_governance_vote"; + description = `Cast a vote on a governance proposal. + Inputs (input is a JSON string): + - realmAccount: string, the address eg "7nxQB..." of the realm (required) + - proposalAccount: string, the address eg "8x2dR..." of the proposal (required) + - vote: string, the type of vote, either "yes" or "no" (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { realmAccount, proposalAccount, vote } = JSON.parse(input); + + if (!["yes", "no"].includes(vote.toLowerCase())) { + throw new Error("Invalid voteType. Allowed values: 'yes', 'no'"); + } + // Validate public keys + if ( + !PublicKey.isOnCurve(realmAccount) || + !PublicKey.isOnCurve(proposalAccount) + ) { + throw new Error("Invalid realmAccount or proposalAccount"); + } + const signature = await this.solanaKit.castProposalVote( + new PublicKey(realmAccount), + new PublicKey(proposalAccount), + vote, + ); + + return JSON.stringify({ + status: "success", + message: "Vote cast successfully", + transaction: signature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } + } \ No newline at end of file diff --git a/src/langchain/realm/index.ts b/src/langchain/realm/index.ts new file mode 100644 index 00000000..b3062bd1 --- /dev/null +++ b/src/langchain/realm/index.ts @@ -0,0 +1,4 @@ +export * from "./cast_proposal_vote"; +export * from "./manage_vote_delegation"; +export * from "./monitor_voting_outcomes"; +export * from "./track_voting_power"; \ No newline at end of file diff --git a/src/langchain/realm/manage_vote_delegation.ts b/src/langchain/realm/manage_vote_delegation.ts new file mode 100644 index 00000000..bdde3225 --- /dev/null +++ b/src/langchain/realm/manage_vote_delegation.ts @@ -0,0 +1,52 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; +import { PublicKey } from "@solana/web3.js"; + +export class SolanaManageVoteDelegationTool extends Tool { + name = "delegate_vote"; + description = `Delegate voting power to another wallet. + + Input should be a JSON string containing: + + - realmAccount : string, the address eg "7nxQB..." of the realm (required) + - governingTokenMint: string, the PublicKey of the governing token mint (required) + - delegate: string, the PublicKey of the new delegate (required) `; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { realm, governingTokenMint, delegate } = JSON.parse(input); + + // Validate public keys + if ( + !PublicKey.isOnCurve(realm) || + !PublicKey.isOnCurve(governingTokenMint) || + !PublicKey.isOnCurve(delegate) + ) { + throw new Error( + "Invalid public key provided for realm, governingTokenMint, or delegate", + ); + } + const signature = await this.solanaKit.manageVoteDelegation( + new PublicKey(realm), + new PublicKey(governingTokenMint), + new PublicKey(delegate), + ); + + return JSON.stringify({ + status: "success", + message: "Voting power delegated successfully", + data: { signature }, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} \ No newline at end of file diff --git a/src/langchain/realm/monitor_voting_outcomes.ts b/src/langchain/realm/monitor_voting_outcomes.ts new file mode 100644 index 00000000..4b2c7aad --- /dev/null +++ b/src/langchain/realm/monitor_voting_outcomes.ts @@ -0,0 +1,37 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; +import { PublicKey } from "@solana/web3.js"; + +export class SolanaMonitorVotingOutcomesTool extends Tool { + name = "solana_monitor_voting_outcomes"; + description = `Monitor the voting outcomes for a given proposal. + + Inputs (input is a JSON string): + - proposalId: string, the address of the proposal (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { proposalId } = JSON.parse(input); + + const votingOutcomes = await this.solanaKit.monitorVotingOutcomes( + new PublicKey(proposalId), + ); + + return JSON.stringify({ + status: "success", + message: "Successfully monitored voting outcomes", + votingOutcomes, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } + } \ No newline at end of file diff --git a/src/langchain/realm/track_voting_power.ts b/src/langchain/realm/track_voting_power.ts new file mode 100644 index 00000000..9f161f8e --- /dev/null +++ b/src/langchain/realm/track_voting_power.ts @@ -0,0 +1,38 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; +import { PublicKey } from "@solana/web3.js"; + +export class SolanaTrackVotingPowerTool extends Tool { + name = "solana_track_voting_power"; + description = `Track the voting power of a user in a realm. + + Inputs (input is a JSON string): + - realmId: string, the address of the realm (required) + - tokenOwnerRecordPk: string, the PublicKey of the Token Owner Record (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { tokenOwnerRecordPk } = JSON.parse(input); + + const votingPower = await this.solanaKit.trackVotingPower( + new PublicKey(tokenOwnerRecordPk), + ); + + return JSON.stringify({ + status: "success", + message: "Successfully tracked voting power", + votingPower, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } + } \ No newline at end of file diff --git a/src/tools/realm/index.ts b/src/tools/realm/index.ts new file mode 100644 index 00000000..3367c56d --- /dev/null +++ b/src/tools/realm/index.ts @@ -0,0 +1,4 @@ +export * from "./spl_governance_voting"; +export * from "./manage_vote_delegation"; +export * from "./monitor_voting_outcomes"; +export * from "./track_voting_power"; \ No newline at end of file diff --git a/src/tools/realm/manage_vote_delegation.ts b/src/tools/realm/manage_vote_delegation.ts new file mode 100644 index 00000000..f6fbf5bb --- /dev/null +++ b/src/tools/realm/manage_vote_delegation.ts @@ -0,0 +1,99 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +import { SolanaAgentKit } from "../../index"; +import { + getGovernanceProgramVersion, + getTokenOwnerRecordAddress, + withSetGovernanceDelegate, +} from "@solana/spl-governance"; +import { GOVERNANCE_PROGRAM_ADDRESS } from "../../constants"; + +/** + * Delegate voting power to another wallet in a governance realm + * + * @param agent {SolanaAgentKit} The Solana Agent Kit instance + * @param realm {PublicKey} The public key of the realm + * @param governingTokenMint {PublicKey} The mint of the governing token to delegate + * @param delegate {PublicKey} The wallet address to delegate voting power to + * + * @returns {Promise} Transaction signature + * + * @throws {Error} If public keys are invalid + * @throws {Error} If delegation transaction fails + * @throws {Error} If token owner record doesn't exist + * + * @example + * const signature = await delegateVotingPower( + * agent, + * new PublicKey("realm-address"), + * new PublicKey("token-mint-address"), + * new PublicKey("delegate-address") + * ); + */ +export async function manageVoteDelegation( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, + delegate: PublicKey, +): Promise { + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey(GOVERNANCE_PROGRAM_ADDRESS); + + // Get governance program version for the connected chain + const programVersion = await getGovernanceProgramVersion( + connection, + governanceProgramId, + ); + // Get token owner record for the current wallet + const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress( + governanceProgramId, + realm, + governingTokenMint, + agent.wallet_address, + ); + + // Create transaction + const transaction = new Transaction(); + + // Add set delegate instruction + await withSetGovernanceDelegate( + transaction.instructions, + governanceProgramId, + programVersion, + realm, + governingTokenMint, + tokenOwnerRecordAddress, + agent.wallet_address, // governanceAuthority + delegate, + ); + + // Send and confirm transaction + transaction.sign(agent.wallet); + const signature = await agent.connection.sendRawTransaction( + transaction.serialize(), + { + preflightCommitment: "confirmed", + maxRetries: 3, + }, + ); + + // Wait for confirmation + const latestBlockhash = await connection.getLatestBlockhash(); + await connection.confirmTransaction({ + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }); + + return signature; + } catch (error: any) { + // Handle specific error cases + if (error.message.includes("Account not found")) { + throw new Error("No token owner record found - deposit tokens first"); + } + if (error.message.includes("Invalid delegate")) { + throw new Error("Invalid delegate address provided"); + } + throw new Error(`Failed to delegate voting power: ${error.message}`); + } +} diff --git a/src/tools/realm/monitor_voting_outcomes.ts b/src/tools/realm/monitor_voting_outcomes.ts new file mode 100644 index 00000000..d0560ab9 --- /dev/null +++ b/src/tools/realm/monitor_voting_outcomes.ts @@ -0,0 +1,22 @@ +import { PublicKey } from "@solana/web3.js"; +import { getProposal, ProgramAccount, Proposal } from "@solana/spl-governance"; +import { SolanaAgentKit } from "../../agent"; + +/** + * Monitors the voting outcome of a proposal. + * + * @param agent - SolanaAgentKit instance. + * @param proposalId - PublicKey of the proposal to monitor. + * @returns The final state of the proposal (e.g., 'Succeeded', 'Defeated'). + */ +export async function monitorVotingOutcomes( + agent: SolanaAgentKit, + proposalId: PublicKey, +): Promise> { + const proposal = await getProposal(agent.connection, proposalId); + if (!proposal) { + throw new Error("Proposal not found."); + } + // Return the status of the proposal + return proposal; +} diff --git a/src/tools/realm/spl_governance_voting.ts b/src/tools/realm/spl_governance_voting.ts new file mode 100644 index 00000000..9c35191f --- /dev/null +++ b/src/tools/realm/spl_governance_voting.ts @@ -0,0 +1,125 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +import { SolanaAgentKit } from "../../index"; +import { + getGovernanceProgramVersion, + getTokenOwnerRecordAddress, + getVoteRecordAddress, + getProposal, + Vote, + VoteChoice, + withCastVote, + getRealm, + VoteKind, +} from "@solana/spl-governance"; +import { GOVERNANCE_PROGRAM_ADDRESS } from "../../constants"; + +/** + * Cast a vote on a governance proposal + * + * @param agent {SolanaAgentKit} The Solana Agent Kit instance + * @param realmAccount {PublicKey} The public key of the realm + * @param proposalAccount {PublicKey} The public key of the proposal being voted on + * @param voteType {"yes" | "no"} Type of vote to cast + * + * @returns {Promise} Transaction signature + * + * @throws Will throw an error if the vote transaction fails + * + * @example + * const signature = await castVote( + * agent, + * new PublicKey("realm-address"), + * new PublicKey("proposal-address"), + * "yes" + * ); + */ +export async function castProposalVote( + agent: SolanaAgentKit, + realmAccount: PublicKey, + proposalAccount: PublicKey, + voteType: "yes" | "no", +): Promise { + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey(GOVERNANCE_PROGRAM_ADDRESS); + + // Get governance program version for the connected chain + const programVersion = await getGovernanceProgramVersion( + connection, + governanceProgramId, + ); + + // Fetch realm info and get governing token mint + const realmInfo = await getRealm(connection, realmAccount); + const governingTokenMint = realmInfo.account.communityMint; + + // Get voter's token owner record + const tokenOwnerRecord = await getTokenOwnerRecordAddress( + governanceProgramId, + realmAccount, + governingTokenMint, + agent.wallet_address, + ); + + // Get voter's vote record + const voteRecord = await getVoteRecordAddress( + governanceProgramId, + proposalAccount, + tokenOwnerRecord, + ); + + // Get proposal data + const proposal = await getProposal(connection, proposalAccount); + + // Construct vote object + const vote = new Vote({ + voteType: voteType === "no" ? VoteKind.Deny : VoteKind.Approve, + approveChoices: + voteType === "yes" + ? [new VoteChoice({ rank: 0, weightPercentage: 100 })] + : [], + deny: voteType === "no", + veto: false, + }); + + // Create and configure transaction + const transaction = new Transaction(); + + await withCastVote( + transaction.instructions, + governanceProgramId, + programVersion, + realmAccount, + proposal.account.governance, + proposalAccount, + proposal.account.tokenOwnerRecord, + tokenOwnerRecord, + proposal.account.governingTokenMint, + voteRecord, + vote, + agent.wallet_address, + ); + + // Sign and send transaction + transaction.sign(agent.wallet); + const signature = await agent.connection.sendRawTransaction( + transaction.serialize(), + { + preflightCommitment: "confirmed", + maxRetries: 3, + }, + ); + + // Confirm transaction + const latestBlockhash = await connection.getLatestBlockhash(); + await connection.confirmTransaction({ + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }); + + return signature; + } catch (error: any) { + throw new Error(`Failed to cast governance vote: ${error.message}`); + } +} \ No newline at end of file diff --git a/src/tools/realm/track_voting_power.ts b/src/tools/realm/track_voting_power.ts new file mode 100644 index 00000000..99b83a7c --- /dev/null +++ b/src/tools/realm/track_voting_power.ts @@ -0,0 +1,28 @@ +import { PublicKey } from "@solana/web3.js"; +import { getTokenOwnerRecord } from "@solana/spl-governance"; +import { SolanaAgentKit } from "../../agent"; + +/** + * Tracks the voting power of a user in a governance realm. + * + * @param agent - SolanaAgentKit instance. + * @param tokenOwnerRecordPk - PublicKey of the user's Token Owner Record. + * @returns The voting power of the user. + */ +export async function trackVotingPower( + agent: SolanaAgentKit, + tokenOwnerRecordPk: PublicKey, +): Promise { + // Fetch the Token Owner Record + const tokenOwnerRecord = await getTokenOwnerRecord( + agent.connection, + tokenOwnerRecordPk, + ); + + if (!tokenOwnerRecord) { + throw new Error("Token Owner Record not found."); + } + + // Return the voting power (governing token deposit amount) + return tokenOwnerRecord.account.governingTokenDepositAmount.toNumber(); +}