From c8e6f73811ebc2b5a68042df4f65072f5015108d Mon Sep 17 00:00:00 2001 From: RudraKc Date: Sat, 11 Jan 2025 19:21:09 +0530 Subject: [PATCH] feat: Add support for SPL Governance Voting Operations --- src/agent/index.ts | 39 +++++++ src/langchain/index.ts | 153 +++++++++++++++++++++++++++ src/tools/cast_proposal_vote.ts | 108 +++++++++++++++++++ src/tools/index.ts | 4 + src/tools/manage_vote_delegation.ts | 76 +++++++++++++ src/tools/monitor_voting_outcomes.ts | 24 +++++ src/tools/track_voting_power.ts | 50 +++++++++ 7 files changed, 454 insertions(+) create mode 100644 src/tools/cast_proposal_vote.ts create mode 100644 src/tools/manage_vote_delegation.ts create mode 100644 src/tools/monitor_voting_outcomes.ts create mode 100644 src/tools/track_voting_power.ts diff --git a/src/agent/index.ts b/src/agent/index.ts index 56517037..3083d038 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -63,6 +63,10 @@ import { fetchPythPriceFeedID, flashOpenTrade, flashCloseTrade, + manageVoteDelegation, + castProposalVote, + trackVotingPower, + monitorVotingOutcomes, } from "../tools"; import { CollectionDeployment, @@ -655,4 +659,39 @@ export class SolanaAgentKit { ): Promise { return execute_transaction(this, transactionIndex); } + + async manageVoteDelegation( + realmId: string, + governingTokenMintId: string, + governingTokenOwnerId: string, + newDelegateId: string, + ): Promise { + return manageVoteDelegation( + this, + realmId, + governingTokenMintId, + governingTokenOwnerId, + newDelegateId, + ); + } + + async castProposalVote( + realmId: string, + proposalId: string, + voteType: string, + ): Promise { + return castProposalVote(this, realmId, proposalId, voteType); + } + + async trackVotingPower( + realmId: string, + walletId: string, + governingTokenMint: string, + ): Promise { + return trackVotingPower(this, realmId, walletId, governingTokenMint); + } + + async monitorVotingOutcomes(proposalId: string): Promise { + return monitorVotingOutcomes(this, proposalId); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index e442206a..acaab68f 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -2688,6 +2688,155 @@ export class SolanaExecuteProposal2by2Multisig extends Tool { } } +export class SolanaCastProposalVoteTool extends Tool { + name = "cast_proposal_vote"; + description = `Cast a vote on a governance proposal within a realm on Solana. + + Inputs (JSON string): + - realmId: string (Realm Address) + - proposalId: string (Proposal Address) + - voteType: string ("yes" or "no")`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { realmId, proposalId, voteType } = JSON.parse(input); + + const signature = await this.solanaKit.castProposalVote( + realmId, + proposalId, + voteType, + ); + + return JSON.stringify({ + status: "success", + message: "Vote cast successfully", + signature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + }); + } + } +} + +export class SolanaTrackVotingPowerTool extends Tool { + name = "track_voting_power"; + description = `Track the voting power of a specific wallet address in a realm on Solana. + + Inputs (JSON string): + - realmId: string (Public key of the realm) + - walletId: string (Public key of the wallet) + - governingTokenMint: string (Public key of the governing token mint)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { realmId, walletId, governingTokenMint } = JSON.parse(input); + + const votingPower = await this.solanaKit.trackVotingPower( + realmId, + walletId, + governingTokenMint, + ); + + return JSON.stringify({ + status: "success", + message: "Voting power fetched successfully", + votingPower, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + }); + } + } +} + +export class SolanaMonitorVotingOutcomesTool extends Tool { + name = "monitor_voting_outcomes"; + description = `Monitor the voting outcome of a proposal on Solana. + + Inputs (JSON string): + - proposalId: string (Public key of the proposal)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { proposalId } = JSON.parse(input); + + const state = await this.solanaKit.monitorVotingOutcomes(proposalId); + + return JSON.stringify({ + status: "success", + message: "Proposal state fetched successfully", + state, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + }); + } + } +} + +export class SolanaManageVoteDelegationTool extends Tool { + name = "manage_vote_delegation"; + description = `Set a governance delegate for a realm and token owner on Solana. + + Inputs (JSON string): + - realmId: string (Public key of the realm) + - governingTokenMintId: string (Public key of the governing token mint) + - governingTokenOwnerId: string (Public key of the governing token owner) + - newDelegateId: string (Public key of the new delegate)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { + realmId, + governingTokenMintId, + governingTokenOwnerId, + newDelegateId, + } = JSON.parse(input); + + const signature = await this.solanaKit.manageVoteDelegation( + realmId, + governingTokenMintId, + governingTokenOwnerId, + newDelegateId, + ); + + return JSON.stringify({ + status: "success", + message: "Governance delegate set successfully", + signature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + }); + } + } +} + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), @@ -2755,5 +2904,9 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaApproveProposal2by2Multisig(solanaKit), new SolanaRejectProposal2by2Multisig(solanaKit), new SolanaExecuteProposal2by2Multisig(solanaKit), + new SolanaCastProposalVoteTool(solanaKit), + new SolanaTrackVotingPowerTool(solanaKit), + new SolanaMonitorVotingOutcomesTool(solanaKit), + new SolanaManageVoteDelegationTool(solanaKit), ]; } diff --git a/src/tools/cast_proposal_vote.ts b/src/tools/cast_proposal_vote.ts new file mode 100644 index 00000000..db8165d9 --- /dev/null +++ b/src/tools/cast_proposal_vote.ts @@ -0,0 +1,108 @@ +import { + PublicKey, + Transaction, + Signer, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + getGovernanceProgramVersion, + getTokenOwnerRecordAddress, + getVoteRecordAddress, + getProposal, + Vote, + VoteChoice, + withCastVote, + getRealm, + VoteKind, +} from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Cast a vote on a given proposal. + * + * @param agent The SolanaAgentKit instance. + * @param realmId The public key of the realm as a string. + * @param proposalId The public key of the proposal as a string. + * @param voteType The type of vote ("yes" or "no"). + * @returns The transaction signature. + */ +export async function castProposalVote( + agent: SolanaAgentKit, + realmId: string, + proposalId: string, + voteType: string, +): Promise { + if (!["yes", "no"].includes(voteType.toLowerCase())) { + throw new Error("Invalid voteType. Allowed values: 'yes', 'no'."); + } + + const realmPublicKey = new PublicKey(realmId); + const proposalPublicKey = new PublicKey(proposalId); + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + try { + const connection = agent.connection; + const programVersion = await getGovernanceProgramVersion( + connection, + governanceProgramId, + ); + const realmInfo = await getRealm(connection, realmPublicKey); + const governingTokenMint = realmInfo.account.communityMint; + + const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress( + governanceProgramId, + realmPublicKey, + governingTokenMint, + agent.wallet.publicKey, + ); + + const voteRecordAddress = await getVoteRecordAddress( + governanceProgramId, + proposalPublicKey, + tokenOwnerRecordAddress, + ); + + const proposal = await getProposal(connection, proposalPublicKey); + const proposalTokenOwnerRecordAddress = proposal.account.tokenOwnerRecord; + + const vote = new Vote({ + voteType: + voteType.toLowerCase() === "no" ? VoteKind.Deny : VoteKind.Approve, + approveChoices: [new VoteChoice({ rank: 0, weightPercentage: 100 })], + deny: voteType.toLowerCase() === "no", + veto: false, + }); + + const transaction = new Transaction(); + await withCastVote( + transaction.instructions, + governanceProgramId, + programVersion, + realmPublicKey, + proposal.account.governance, + proposalPublicKey, + proposalTokenOwnerRecordAddress, + tokenOwnerRecordAddress, + proposal.account.governingTokenMint, + voteRecordAddress, + vote, + agent.wallet.publicKey, + ); + + // Send and confirm the transaction + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [agent.wallet as Signer], + { + preflightCommitment: "confirmed", + }, + ); + + return signature; + } catch (error: any) { + throw new Error(`Unable to cast vote: ${error.message}`); + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 2363e3ab..d0d851d8 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -50,3 +50,7 @@ export * from "./flash_open_trade"; export * from "./flash_close_trade"; export * from "./create_3land_collectible"; +export * from "./manage_vote_delegation"; +export * from "./cast_proposal_vote"; +export * from "./track_voting_power"; +export * from "./monitor_voting_outcomes"; diff --git a/src/tools/manage_vote_delegation.ts b/src/tools/manage_vote_delegation.ts new file mode 100644 index 00000000..b835c8e7 --- /dev/null +++ b/src/tools/manage_vote_delegation.ts @@ -0,0 +1,76 @@ +import { + PublicKey, + Transaction, + Signer, + sendAndConfirmTransaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { + getGovernanceProgramVersion, + withSetGovernanceDelegate, +} from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Set a governance delegate for a given realm and token owner. + * + * @param agent The SolanaAgentKit instance. + * @param realmId The public key of the realm as a string. + * @param governingTokenMintId The public key of the governing token mint as a string. + * @param governingTokenOwnerId The public key of the governing token owner as a string. + * @param newDelegateId The public key of the new delegate as a string. + * @returns The transaction signature. + */ +export async function manageVoteDelegation( + agent: SolanaAgentKit, + realmId: string, + governingTokenMintId: string, + governingTokenOwnerId: string, + newDelegateId: string, +): Promise { + const realmPublicKey = new PublicKey(realmId); + const governingTokenMint = new PublicKey(governingTokenMintId); + const governingTokenOwner = new PublicKey(governingTokenOwnerId); + const newDelegate = new PublicKey(newDelegateId); + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + try { + const connection = agent.connection; + const programVersion = await getGovernanceProgramVersion( + connection, + governanceProgramId, + ); + + const transaction = new Transaction(); + const instructions: TransactionInstruction[] = []; + + await withSetGovernanceDelegate( + instructions, + governanceProgramId, + programVersion, + realmPublicKey, + governingTokenMint, + governingTokenOwner, + agent.wallet.publicKey, + newDelegate, + ); + + transaction.add(...instructions); + + // Send and confirm the transaction + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [agent.wallet as Signer], + { + preflightCommitment: "confirmed", + }, + ); + + return signature; + } catch (error: any) { + throw new Error(`Failed to set governance delegate: ${error.message}`); + } +} diff --git a/src/tools/monitor_voting_outcomes.ts b/src/tools/monitor_voting_outcomes.ts new file mode 100644 index 00000000..471b3764 --- /dev/null +++ b/src/tools/monitor_voting_outcomes.ts @@ -0,0 +1,24 @@ +import { PublicKey } from "@solana/web3.js"; +import { getProposal, ProposalState } from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Monitor the voting outcome of a proposal. + * + * @param agent The SolanaAgentKit instance. + * @param proposalId The public key of the proposal as a string. + * @returns The current state of the proposal. + */ +export async function monitorVotingOutcomes( + agent: SolanaAgentKit, + proposalId: string, +): Promise { + const proposalPublicKey = new PublicKey(proposalId); + + try { + const proposal = await getProposal(agent.connection, proposalPublicKey); + return ProposalState[proposal.account.state]; + } catch (error: any) { + throw new Error(`Unable to monitor voting outcomes: ${error.message}`); + } +} diff --git a/src/tools/track_voting_power.ts b/src/tools/track_voting_power.ts new file mode 100644 index 00000000..e1497490 --- /dev/null +++ b/src/tools/track_voting_power.ts @@ -0,0 +1,50 @@ +import { PublicKey } from "@solana/web3.js"; +import { + getTokenOwnerRecordAddress, + getTokenOwnerRecord, +} from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Track the voting power of a specific wallet address in a realm on Solana. + * + * @param agent The SolanaAgentKit instance. + * @param realmId The public key of the realm as a string. + * @param walletId The public key of the wallet as a string. + * @param governingTokenMint The public key of the governing token mint. + * @returns The voting power of the specified wallet in the realm. + */ +export async function trackVotingPower( + agent: SolanaAgentKit, + realmId: string, + walletId: string, + governingTokenMint: string, +): Promise { + const realmPublicKey = new PublicKey(realmId); + const walletPublicKey = new PublicKey(walletId); + const governingTokenMintKey = new PublicKey(governingTokenMint); + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + try { + // Get the TokenOwnerRecord address + const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress( + governanceProgramId, + realmPublicKey, + governingTokenMintKey, + walletPublicKey, + ); + + // Fetch the TokenOwnerRecord + const tokenOwnerRecord = await getTokenOwnerRecord( + agent.connection, + tokenOwnerRecordAddress, + ); + + // Return the voting power + return tokenOwnerRecord.account.governingTokenDepositAmount.toNumber(); + } catch (error: any) { + throw new Error(`Unable to track voting power: ${error.message}`); + } +}