diff --git a/src/agent/index.ts b/src/agent/index.ts index 3563f8de..afbffec2 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -63,6 +63,7 @@ import { fetchPythPriceFeedID, flashOpenTrade, flashCloseTrade, + cast_proposal_vote, } from "../tools"; import { CollectionDeployment, @@ -603,4 +604,12 @@ export class SolanaAgentKit { ); return `Transaction: ${tx}`; } + + async castProposalVote( + realmId: string, + proposalId: string, + voteType: string, + ) { + return cast_proposal_vote(this, realmId, proposalId, voteType); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index ef282e35..84891d37 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -2435,6 +2435,45 @@ export class SolanaCloseEmptyTokenAccounts extends Tool { } } +export class SolanaCastProposalVote extends Tool { + name = "solana_cast_proposal_vote"; + description = `Vote on a created proposal with given proposalId, realmId, and vote type + + Inputs: + realmId: string, represents the realm address(of a pre-existing realm) (required), + proposalId: string, the address of the created proposal (required), + voteOption: string, the kind of vote, should be either "yes" or "no" (required), + `; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + + const getVoteAccount = await this.solanaKit.castProposalVote( + parsedInput.realmId, + parsedInput.proposalId, + parsedInput.voteType, + ); + + return JSON.stringify({ + status: getVoteAccount.status, + message: `Successfully Casted Vote on Proposal`, + signature: getVoteAccount.signature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), @@ -2495,5 +2534,6 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaFlashOpenTrade(solanaKit), new SolanaFlashCloseTrade(solanaKit), new Solana3LandCreateSingle(solanaKit), + new SolanaCastProposalVote(solanaKit), ]; } diff --git a/src/tools/index.ts b/src/tools/index.ts index 2363e3ab..a7ff4659 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -50,3 +50,4 @@ export * from "./flash_open_trade"; export * from "./flash_close_trade"; export * from "./create_3land_collectible"; +export * from "./spl_governance_voting"; diff --git a/src/tools/spl_governance_voting.ts b/src/tools/spl_governance_voting.ts new file mode 100644 index 00000000..352c75e1 --- /dev/null +++ b/src/tools/spl_governance_voting.ts @@ -0,0 +1,117 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +// import { SplGovernance } from "governance-idl-sdk"; +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 realmId Realm Address + * @param proposalId Address of created proposal on which voting happens + * @param voteType Type of vote("yes"/"no") + * @returns signature of vote cast transaction + */ + +export async function cast_proposal_vote( + agent: SolanaAgentKit, + realmId: string, + proposalId: string, + voteType: string, +) { + try { + if (!["yes", "no"].includes(voteType.toLowerCase())) { + throw new Error("Invalid voteType. Allowed values: 'yes', 'no'"); + } + + if (!PublicKey.isOnCurve(realmId) || !PublicKey.isOnCurve(proposalId)) { + throw new Error("Invalid realmId or proposalId"); + } + + const connection = agent.connection; + const governanceId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + const programVersion = await getGovernanceProgramVersion( + connection, + governanceId, + ); + const realm = new PublicKey(realmId); + const realmInfo = await getRealm(connection, realm); + const governingTokenMint = realmInfo.account.communityMint; + + const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress( + governanceId, + realm, + governingTokenMint, + agent.wallet.publicKey, + ); + + const voteRecordAddress = await getVoteRecordAddress( + governanceId, + new PublicKey(proposalId), + tokenOwnerRecordAddress, + ); + const proposal = await getProposal(connection, new PublicKey(proposalId)); + const proposalTokenOwnerRecordAddress = proposal.account.tokenOwnerRecord; + const vote = new Vote({ + voteType: voteType === "no" ? VoteKind.Deny : VoteKind.Approve, + approveChoices: [ + new VoteChoice({ + rank: 0, + weightPercentage: 100, + }), + ], + deny: voteType === "no", + veto: false, + }); + + const transaction = new Transaction(); + + await withCastVote( + transaction.instructions, + governanceId, + programVersion, + realm, + proposal.account.governance, + new PublicKey(proposalId), + proposalTokenOwnerRecordAddress, + tokenOwnerRecordAddress, + proposal.account.governingTokenMint, + voteRecordAddress, + vote, + agent.wallet.publicKey, + ); + transaction.sign(agent.wallet); + const signature = await agent.connection.sendRawTransaction( + transaction.serialize(), + { + preflightCommitment: "confirmed", + maxRetries: 3, + }, + ); + + const latestBlockhash = await agent.connection.getLatestBlockhash(); + await agent.connection.confirmTransaction({ + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }); + + return { + status: "success", + signature: signature, + }; + } catch (error: any) { + console.error(error); + throw new Error(`Unable to cast vote: ${error.message}`); + } +}