Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add SPL-Governance Voting Operations #171

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
9 changes: 9 additions & 0 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
fetchPythPriceFeedID,
flashOpenTrade,
flashCloseTrade,
cast_proposal_vote,
} from "../tools";
import {
CollectionDeployment,
Expand Down Expand Up @@ -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);
}
}
40 changes: 40 additions & 0 deletions src/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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),
Expand Down Expand Up @@ -2495,5 +2534,6 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
new SolanaFlashOpenTrade(solanaKit),
new SolanaFlashCloseTrade(solanaKit),
new Solana3LandCreateSingle(solanaKit),
new SolanaCastProposalVote(solanaKit),
];
}
1 change: 1 addition & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ export * from "./flash_open_trade";
export * from "./flash_close_trade";

export * from "./create_3land_collectible";
export * from "./spl_governance_voting";
117 changes: 117 additions & 0 deletions src/tools/spl_governance_voting.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}