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 support for SPL Governance Voting Operations #188

Open
wants to merge 1 commit 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
39 changes: 39 additions & 0 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ import {
fetchPythPriceFeedID,
flashOpenTrade,
flashCloseTrade,
manageVoteDelegation,
castProposalVote,
trackVotingPower,
monitorVotingOutcomes,
} from "../tools";
import {
CollectionDeployment,
Expand Down Expand Up @@ -655,4 +659,39 @@ export class SolanaAgentKit {
): Promise<string> {
return execute_transaction(this, transactionIndex);
}

async manageVoteDelegation(
realmId: string,
governingTokenMintId: string,
governingTokenOwnerId: string,
newDelegateId: string,
): Promise<string> {
return manageVoteDelegation(
this,
realmId,
governingTokenMintId,
governingTokenOwnerId,
newDelegateId,
);
}

async castProposalVote(
realmId: string,
proposalId: string,
voteType: string,
): Promise<string> {
return castProposalVote(this, realmId, proposalId, voteType);
}

async trackVotingPower(
realmId: string,
walletId: string,
governingTokenMint: string,
): Promise<number> {
return trackVotingPower(this, realmId, walletId, governingTokenMint);
}

async monitorVotingOutcomes(proposalId: string): Promise<string> {
return monitorVotingOutcomes(this, proposalId);
}
}
153 changes: 153 additions & 0 deletions src/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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<string> {
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<string> {
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<string> {
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),
Expand Down Expand Up @@ -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),
];
}
108 changes: 108 additions & 0 deletions src/tools/cast_proposal_vote.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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}`);
}
}
4 changes: 4 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading