From 10f995df7f1aea6c89d64d0f2a8e7b6a76460968 Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Thu, 9 Jan 2025 22:45:26 +0530 Subject: [PATCH 1/4] Added init changes --- package.json | 1 + pnpm-lock.yaml | 54 ++++++++ src/actions/governance.ts | 203 +++++++++++++++++++++++++++++ src/actions/index.ts | 10 ++ src/agent/index.ts | 82 ++++++++++++ src/langchain/index.ts | 208 +++++++++++++++++++++++++----- src/tools/governance.ts | 265 ++++++++++++++++++++++++++++++++++++++ src/tools/index.ts | 1 + 8 files changed, 794 insertions(+), 30 deletions(-) create mode 100644 src/actions/governance.ts create mode 100644 src/tools/governance.ts diff --git a/package.json b/package.json index 70d54c7a..4550cad7 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", "@tensor-oss/tensorswap-sdk": "^4.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55c2dcae..09f3c4db 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.9)(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.9)(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.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) @@ -110,6 +113,9 @@ importers: form-data: specifier: ^4.0.1 version: 4.0.1 + governance-idl-sdk: + specifier: ^0.0.4 + version: 0.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) langchain: specifier: ^0.3.8 version: 0.3.9(@langchain/core@0.3.27(openai@4.77.3(zod@3.24.1)))(@langchain/groq@0.1.2(@langchain/core@0.3.27(openai@4.77.3(zod@3.24.1))))(axios@1.7.9)(openai@4.77.3(zod@3.24.1)) @@ -964,6 +970,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'} @@ -1061,6 +1070,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==} @@ -1435,6 +1447,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==} @@ -2163,6 +2178,9 @@ packages: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} + governance-idl-sdk@0.0.4: + resolution: {integrity: sha512-90B5lZBxEnraiK74jHWIYbMec7Y0aQEyPz/MF7KeRCGc2ImcIa6xwWvscVxyZhtU7dys9FBJaUN/EZj9TET32Q==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -5220,6 +5238,21 @@ snapshots: - supports-color - utf-8-validate + '@solana/spl-governance@0.3.28(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(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.9)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs': 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22) @@ -5535,6 +5568,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 @@ -6017,6 +6054,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 @@ -6862,6 +6906,16 @@ snapshots: p-cancelable: 2.1.1 responselike: 2.0.1 + governance-idl-sdk@0.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@coral-xyz/anchor': 0.29.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + bn.js: 5.2.1 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + graceful-fs@4.2.11: optional: true diff --git a/src/actions/governance.ts b/src/actions/governance.ts new file mode 100644 index 00000000..b8f558bf --- /dev/null +++ b/src/actions/governance.ts @@ -0,0 +1,203 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../types/action"; +import { SolanaAgentKit } from "../agent"; +import { z } from "zod"; +import { + castVote, + getVotingPower, + delegateVotingPower, + removeDelegation, + getVotingOutcome, +} from "../tools/governance"; + +export const castVoteAction: Action = { + name: "CAST_VOTE", + similes: ["vote", "cast ballot", "vote on proposal"], + description: "Cast a vote on an active governance proposal", + examples: [ + [ + { + input: { + proposal: "4HxrP3R6A6GcUv62VHG331gwJKNhrqHKF438oRztzz2r", + vote: "yes", + comment: "I support this proposal", + }, + output: { + status: "success", + signature: "2ZE7Rz...", + message: "Vote cast successfully", + }, + explanation: "Cast a yes vote with a comment", + }, + ], + ], + schema: z.object({ + proposal: z.string().min(32, "Invalid proposal address"), + vote: z.enum(["yes", "no", "abstain"]), + comment: z.string().optional(), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const signature = await castVote( + agent, + new PublicKey(input.proposal), + input.vote, + input.comment, + ); + + return { + status: "success", + signature, + message: "Vote cast successfully", + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to cast vote: ${error.message}`, + }; + } + }, +}; + +export const getVotingPowerAction: Action = { + name: "GET_VOTING_POWER", + similes: ["check voting power", "get voting weight", "view governance power"], + description: "Get current voting power in a realm", + examples: [ + [ + { + input: { + realm: "7nxQB...", + governingTokenMint: "EPjF...", + }, + output: { + status: "success", + votingPower: 1000, + delegatedPower: 500, + totalPower: 1500, + }, + explanation: "Check voting power including delegations", + }, + ], + ], + schema: z.object({ + realm: z.string().min(32, "Invalid realm address"), + governingTokenMint: z.string().min(32, "Invalid token mint address"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const power = await getVotingPower( + agent, + new PublicKey(input.realm), + new PublicKey(input.governingTokenMint), + ); + + return { + status: "success", + ...power, + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to get voting power: ${error.message}`, + }; + } + }, +}; + +export const delegateVotingPowerAction: Action = { + name: "DELEGATE_VOTING_POWER", + similes: ["delegate votes", "transfer voting power", "assign voting rights"], + description: "Delegate voting power to another wallet", + examples: [ + [ + { + input: { + realm: "7nxQB...", + governingTokenMint: "EPjF...", + delegate: "8x2dR...", + }, + output: { + status: "success", + signature: "2ZE7Rz...", + message: "Voting power delegated successfully", + }, + explanation: "Delegate voting power to another wallet", + }, + ], + ], + schema: z.object({ + realm: z.string().min(32, "Invalid realm address"), + governingTokenMint: z.string().min(32, "Invalid token mint address"), + delegate: z.string().min(32, "Invalid delegate address"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const signature = await delegateVotingPower( + agent, + new PublicKey(input.realm), + new PublicKey(input.governingTokenMint), + new PublicKey(input.delegate), + ); + + return { + status: "success", + signature, + message: "Voting power delegated successfully", + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to delegate voting power: ${error.message}`, + }; + } + }, +}; + +export const getVotingOutcomeAction: Action = { + name: "GET_VOTING_OUTCOME", + similes: ["check vote results", "view proposal outcome", "get vote counts"], + description: "Get the current outcome of a governance proposal vote", + examples: [ + [ + { + input: { + proposal: "4HxrP3R6A6GcUv62VHG331gwJKNhrqHKF438oRztzz2r", + }, + output: { + status: "success", + outcome: { + status: "Voting", + yesVotes: 1000000, + noVotes: 500000, + abstainVotes: 100000, + isFinalized: false, + votingEndTime: 1672531200, + }, + }, + explanation: "Get detailed voting results for a proposal", + }, + ], + ], + schema: z.object({ + proposal: z.string().min(32, "Invalid proposal address"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const outcome = await getVotingOutcome( + agent, + new PublicKey(input.proposal), + ); + + return { + status: "success", + outcome, + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to get voting outcome: ${error.message}`, + }; + } + }, +}; diff --git a/src/actions/index.ts b/src/actions/index.ts index c9742098..899d93e7 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -30,6 +30,12 @@ import launchPumpfunTokenAction from "./launchPumpfunToken"; import getWalletAddressAction from "./getWalletAddress"; import flashOpenTradeAction from "./flashOpenTrade"; import flashCloseTradeAction from "./flashCloseTrade"; +import { + castVoteAction, + getVotingPowerAction, + delegateVotingPowerAction, + getVotingOutcomeAction, +} from "./governance"; export const ACTIONS = { WALLET_ADDRESS_ACTION: getWalletAddressAction, @@ -65,6 +71,10 @@ export const ACTIONS = { LAUNCH_PUMPFUN_TOKEN_ACTION: launchPumpfunTokenAction, FLASH_OPEN_TRADE_ACTION: flashOpenTradeAction, FLASH_CLOSE_TRADE_ACTION: flashCloseTradeAction, + CAST_VOTE_ACTION: castVoteAction, + GET_VOTING_POWER_ACTION: getVotingPowerAction, + DELEGATE_VOTING_POWER_ACTION: delegateVotingPowerAction, + GET_VOTING_OUTCOME_ACTION: getVotingOutcomeAction, }; export type { Action, ActionExample, Handler } from "../types/action"; diff --git a/src/agent/index.ts b/src/agent/index.ts index 3563f8de..126d87a2 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -63,6 +63,7 @@ import { fetchPythPriceFeedID, flashOpenTrade, flashCloseTrade, + castGovernanceVote, } from "../tools"; import { CollectionDeployment, @@ -603,4 +604,85 @@ export class SolanaAgentKit { ); return `Transaction: ${tx}`; } + + async castGovernanceVote( + realmAccount: PublicKey, + proposalAccount: PublicKey, + voteType: "yes" | "no", + ): Promise { + return await castGovernanceVote( + this, + realmAccount, + proposalAccount, + voteType, + ); + } + + async getVotingPower( + realm: PublicKey, + governingTokenMint: PublicKey, + ): Promise<{ + votingPower: number; + delegatedPower: number; + totalPower: number; + }> { + const governance = new SplGovernance(this.connection); + const tokenHolding = await governance.getTokenOwnerRecordByOwner( + realm, + governingTokenMint, + this.wallet_address, + ); + + if (!tokenHolding) { + return { votingPower: 0, delegatedPower: 0, totalPower: 0 }; + } + + return { + votingPower: tokenHolding.governingTokenDepositAmount.toNumber(), + delegatedPower: tokenHolding.totalDelegatedVoterWeight.toNumber(), + totalPower: tokenHolding.governingTokenDepositAmount + .add(tokenHolding.totalDelegatedVoterWeight) + .toNumber(), + }; + } + + async delegateVotingPower( + realm: PublicKey, + governingTokenMint: PublicKey, + delegate: PublicKey, + ): Promise { + const governance = new SplGovernance(this.connection); + const instruction = await governance.setGovernanceDelegate( + realm, + governingTokenMint, + this.wallet_address, + delegate, + ); + return await this.connection.sendTransaction(instruction, [this.wallet]); + } + + async getVotingOutcome(proposal: PublicKey): Promise<{ + status: string; + yesVotes: number; + noVotes: number; + abstainVotes: number; + isFinalized: boolean; + votingEndTime: number; + }> { + const governance = new SplGovernance(this.connection); + const proposalData = await governance.getProposalByPubkey(proposal); + + if (!proposalData) { + throw new Error("Proposal not found"); + } + + return { + status: proposalData.state, + yesVotes: proposalData.getYesVoteCount().toNumber(), + noVotes: proposalData.getNoVoteCount().toNumber(), + abstainVotes: proposalData.getAbstainVoteCount().toNumber(), + isFinalized: proposalData.isVoteFinalized(), + votingEndTime: proposalData.votingEndTime.toNumber(), + }; + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index ef282e35..c4276cdb 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -287,13 +287,13 @@ export class SolanaPerpCloseTradeTool extends Tool { const tx = parsedInput.side === "long" ? await this.solanaKit.closePerpTradeLong({ - price: parsedInput.price, - tradeMint: new PublicKey(parsedInput.tradeMint), - }) + price: parsedInput.price, + tradeMint: new PublicKey(parsedInput.tradeMint), + }) : await this.solanaKit.closePerpTradeShort({ - price: parsedInput.price, - tradeMint: new PublicKey(parsedInput.tradeMint), - }); + price: parsedInput.price, + tradeMint: new PublicKey(parsedInput.tradeMint), + }); return JSON.stringify({ status: "success", @@ -337,21 +337,21 @@ export class SolanaPerpOpenTradeTool extends Tool { const tx = parsedInput.side === "long" ? await this.solanaKit.openPerpTradeLong({ - price: parsedInput.price, - collateralAmount: parsedInput.collateralAmount, - collateralMint: new PublicKey(parsedInput.collateralMint), - leverage: parsedInput.leverage, - tradeMint: new PublicKey(parsedInput.tradeMint), - slippage: parsedInput.slippage, - }) + price: parsedInput.price, + collateralAmount: parsedInput.collateralAmount, + collateralMint: new PublicKey(parsedInput.collateralMint), + leverage: parsedInput.leverage, + tradeMint: new PublicKey(parsedInput.tradeMint), + slippage: parsedInput.slippage, + }) : await this.solanaKit.openPerpTradeLong({ - price: parsedInput.price, - collateralAmount: parsedInput.collateralAmount, - collateralMint: new PublicKey(parsedInput.collateralMint), - leverage: parsedInput.leverage, - tradeMint: new PublicKey(parsedInput.tradeMint), - slippage: parsedInput.slippage, - }); + price: parsedInput.price, + collateralAmount: parsedInput.collateralAmount, + collateralMint: new PublicKey(parsedInput.collateralMint), + leverage: parsedInput.leverage, + tradeMint: new PublicKey(parsedInput.tradeMint), + slippage: parsedInput.slippage, + }); return JSON.stringify({ status: "success", @@ -807,23 +807,23 @@ export class SolanaFlashOpenTrade extends Tool { if (!Object.keys(marketTokenMap).includes(parsedInput.token)) { throw new Error( "Token must be one of " + - Object.keys(marketTokenMap).join(", ") + - ", received: " + - parsedInput.token + - "\n" + - "Please check https://beast.flash.trade/ for the list of supported tokens", + Object.keys(marketTokenMap).join(", ") + + ", received: " + + parsedInput.token + + "\n" + + "Please check https://beast.flash.trade/ for the list of supported tokens", ); } if (!["long", "short"].includes(parsedInput.type)) { throw new Error( 'Type must be either "long" or "short", received: ' + - parsedInput.type, + parsedInput.type, ); } if (!parsedInput.collateral || parsedInput.collateral <= 0) { throw new Error( "Collateral amount must be positive, received: " + - parsedInput.collateral, + parsedInput.collateral, ); } if (!parsedInput.leverage || parsedInput.leverage <= 0) { @@ -2044,9 +2044,9 @@ export class SolanaRockPaperScissorsTool extends Tool { const result = await this.solanaKit.rockPaperScissors( Number(parsedInput['"amount"']), parsedInput['"choice"'].replace(/^"|"$/g, "") as - | "rock" - | "paper" - | "scissors", + | "rock" + | "paper" + | "scissors", ); return JSON.stringify({ @@ -2435,6 +2435,150 @@ export class SolanaCloseEmptyTokenAccounts extends Tool { } } +export class CastGovernanceVoteTool extends Tool { + name = "solana_governance_vote"; + description = `Cast a vote on a governance proposal. + + Inputs ( input is a JSON string ): + realmAccount: string, eg "7nxQB..." (required) + proposalAccount: string, eg "8x2dR..." (required) + vote: string, 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.castGovernanceVote( + 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", + }); + } + } +} + +export class GetVotingPowerTool extends Tool { + name = "get_voting_power"; + description = + "Get current voting power in a realm. Input should be a JSON string containing: realm (address) and governingTokenMint (address)."; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { realm, governingTokenMint } = JSON.parse(input); + const votingPower = await this.solanaKit.getVotingPower( + new PublicKey(realm), + new PublicKey(governingTokenMint), + ); + + return JSON.stringify({ + status: "success", + message: "Voting power fetched successfully", + data: votingPower, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + +export class DelegateVoteTool extends Tool { + name = "delegate_vote"; + description = + "Delegate voting power to another wallet. Input should be a JSON string containing: realm (address), governingTokenMint (address), and delegate (address)."; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { realm, governingTokenMint, delegate } = JSON.parse(input); + const signature = await this.solanaKit.delegateVotingPower( + 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", + }); + } + } +} + +export class GetVotingOutcomeTool extends Tool { + name = "get_voting_outcome"; + description = + "Get the current outcome of a governance proposal. Input should be a JSON string containing: proposal (address)."; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { proposal } = JSON.parse(input); + const outcome = await this.solanaKit.getVotingOutcome( + new PublicKey(proposal), + ); + + return JSON.stringify({ + status: "success", + message: "Voting outcome fetched successfully", + data: outcome, + }); + } 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 +2639,9 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaFlashOpenTrade(solanaKit), new SolanaFlashCloseTrade(solanaKit), new Solana3LandCreateSingle(solanaKit), + new CastVoteTool(solanaKit), + new GetVotingPowerTool(solanaKit), + new DelegateVoteTool(solanaKit), + new GetVotingOutcomeTool(solanaKit), ]; } diff --git a/src/tools/governance.ts b/src/tools/governance.ts new file mode 100644 index 00000000..b0f85b9b --- /dev/null +++ b/src/tools/governance.ts @@ -0,0 +1,265 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +import { SolanaAgentKit } from "../index"; +import { + getGovernanceProgramVersion, + getTokenOwnerRecordAddress, + getVoteRecordAddress, + getProposal, + Vote, + VoteChoice, + withCastVote, + getRealm, + VoteKind, + getTokenOwnerRecordsByOwner, +} from "@solana/spl-governance"; +import { BN } from "@coral-xyz/anchor"; + +/** + * 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 castGovernanceVote( + agent: SolanaAgentKit, + realmAccount: PublicKey, + proposalAccount: PublicKey, + voteType: "yes" | "no", +): Promise { + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + // 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}`); + } +} + +/** + * Get current voting power for a wallet in a realm + */ +export async function getVotingPower( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, +): Promise<{ + votingPower: number; + delegatedPower: number; + totalPower: number; +}> { + try { + + const connection = agent.connection; + const GOVERNANCE_PROGRAM_ID = + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"; + const programId = new PublicKey(GOVERNANCE_PROGRAM_ID); + const ownerRecordsbyOwner = await getTokenOwnerRecordsByOwner( + connection, + programId, + agent.wallet_address, + ); + + // Get token holding data + const tokenHolding = await governance.getTokenOwnerRecordByOwner( + realm, + governingTokenMint, + agent.wallet_address, + ); + + if (!tokenHolding) { + return { votingPower: 0, delegatedPower: 0, totalPower: 0 }; + } + + return { + votingPower: tokenHolding.governingTokenDepositAmount.toNumber(), + delegatedPower: tokenHolding.totalDelegatedVoterWeight.toNumber(), + totalPower: tokenHolding.governingTokenDepositAmount + .add(tokenHolding.totalDelegatedVoterWeight) + .toNumber(), + }; + } catch (error: any) { + throw new Error(`Failed to get voting power: ${error.message}`); + } +} + +/** + * Delegate voting power to another wallet + */ +export async function delegateVotingPower( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, + delegate: PublicKey, +): Promise { + try { + const governance = new SplGovernance(agent.connection); + + const instruction = await governance.setGovernanceDelegate( + realm, + governingTokenMint, + agent.wallet_address, + delegate, + ); + + const signature = await agent.connection.sendTransaction(instruction, [ + agent.wallet, + ]); + return signature; + } catch (error: any) { + throw new Error(`Failed to delegate voting power: ${error.message}`); + } +} + +/** + * Remove voting power delegation + */ +export async function removeDelegation( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, +): Promise { + try { + const governance = new SplGovernance(agent.connection); + + const instruction = await governance.setGovernanceDelegate( + realm, + governingTokenMint, + agent.wallet_address, + null, // Passing null removes delegation + ); + + const signature = await agent.connection.sendTransaction(instruction, [ + agent.wallet, + ]); + return signature; + } catch (error: any) { + throw new Error(`Failed to remove delegation: ${error.message}`); + } +} + +/** + * Get voting outcome for a specific proposal + */ +export async function getVotingOutcome( + agent: SolanaAgentKit, + proposal: PublicKey, +): Promise<{ + status: string; + yesVotes: number; + noVotes: number; + abstainVotes: number; + isFinalized: boolean; + votingEndTime: number; +}> { + try { + const governance = new SplGovernance(agent.connection); + const proposalData = await governance.getProposalByPubkey(proposal); + + if (!proposalData) { + throw new Error("Proposal not found"); + } + + return { + status: proposalData.state, + yesVotes: proposalData.getYesVoteCount().toNumber(), + noVotes: proposalData.getNoVoteCount().toNumber(), + abstainVotes: proposalData.getAbstainVoteCount().toNumber(), + isFinalized: proposalData.isVoteFinalized(), + votingEndTime: proposalData.votingEndTime.toNumber(), + }; + } catch (error: any) { + throw new Error(`Failed to get voting outcome: ${error.message}`); + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 2363e3ab..c30957c0 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 "./governance"; From 510927b34329d908803238239be046440151d7ae Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sat, 11 Jan 2025 09:47:31 +0530 Subject: [PATCH 2/4] Initial commit --- .husky/pre-commit | 1 + pnpm-lock.yaml | 285 +++++++++++----------- src/agent/index.ts | 24 +- src/langchain/index.ts | 60 ++--- src/tools/governance.ts | 265 --------------------- src/tools/realm/governance.ts | 432 ++++++++++++++++++++++++++++++++++ 6 files changed, 615 insertions(+), 452 deletions(-) delete mode 100644 src/tools/governance.ts create mode 100644 src/tools/realm/governance.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index e69de29b..d42474c6 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -0,0 +1 @@ +tsc && lint-staged \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c4bc95c..05cb7312 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,10 @@ importers: dependencies: '@3land/listings-sdk': specifier: ^0.0.4 - version: 0.0.4(@types/node@22.10.5)(arweave@1.15.5)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) + version: 0.0.4(@types/node@22.10.5)(arweave@1.15.5)(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) '@ai-sdk/openai': specifier: ^1.0.11 - version: 1.0.13(zod@3.24.1) + version: 1.0.11(zod@3.24.1) '@bonfida/spl-name-service': specifier: ^3.0.7 version: 3.0.7(@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) @@ -76,7 +76,7 @@ importers: 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.9)(utf-8-validate@5.0.10) + 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) @@ -112,13 +112,10 @@ importers: version: 16.4.7 flash-sdk: specifier: ^2.24.3 - version: 2.24.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) + version: 2.24.3(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) form-data: specifier: ^4.0.1 version: 4.0.1 - governance-idl-sdk: - specifier: ^0.0.4 - version: 0.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) langchain: specifier: ^0.3.8 version: 0.3.9(@langchain/core@0.3.26(openai@4.77.0(zod@3.24.1)))(@langchain/groq@0.1.2(@langchain/core@0.3.26(openai@4.77.0(zod@3.24.1))))(axios@1.7.9)(openai@4.77.0(zod@3.24.1)) @@ -140,7 +137,7 @@ importers: version: 5.0.1 '@types/node': specifier: ^22.10.2 - version: 22.10.2 + version: 22.10.5 '@typescript-eslint/eslint-plugin': specifier: ^8.18.2 version: 8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) @@ -167,7 +164,7 @@ importers: version: 3.4.2 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.10.2)(typescript@5.7.2) + version: 10.9.2(@types/node@22.10.5)(typescript@5.7.2) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -861,6 +858,9 @@ packages: '@shikijs/vscode-textmate@10.0.1': resolution: {integrity: sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==} + '@shikijs/vscode-textmate@9.3.1': + resolution: {integrity: sha512-79QfK1393x9Ho60QFyLti+QfdJzRQCVLFb97kOIV7Eo9vQU/roINgk7m24uv0a7AUvN//RDH36FLjjK48v0s9g==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -1045,6 +1045,10 @@ packages: '@solana/web3.js@1.98.0': resolution: {integrity: sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA==} + '@sqds/multisig@2.1.3': + resolution: {integrity: sha512-WOiL7La+RSiJsz7jVO85yhSiiSvNMUthiWuLPeWVOoD6IYa34BEAzanF1RdXRWGglSbRFYCTkyr+Ay1WmXmSRQ==} + engines: {node: '>=14'} + '@supercharge/promise-pool@3.2.0': resolution: {integrity: sha512-pj0cAALblTZBPtMltWOlZTQSLT07jIaFNeM8TWoJD1cQMgDB9mcMlVMoetiB35OzNJpqQ2b+QEtwiR9f20mADg==} engines: {node: '>=8'} @@ -2136,12 +2140,8 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} - get-intrinsic@1.2.7: - resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + get-intrinsic@1.2.6: + resolution: {integrity: sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==} engines: {node: '>= 0.4'} get-stream@5.2.0: @@ -2189,9 +2189,6 @@ packages: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} - governance-idl-sdk@0.0.4: - resolution: {integrity: sha512-90B5lZBxEnraiK74jHWIYbMec7Y0aQEyPz/MF7KeRCGc2ImcIa6xwWvscVxyZhtU7dys9FBJaUN/EZj9TET32Q==} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2302,6 +2299,9 @@ packages: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2365,10 +2365,6 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - is-retry-allowed@2.2.0: resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} engines: {node: '>=10'} @@ -3078,8 +3074,8 @@ packages: regex-utilities@2.3.0: resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - regex@5.1.1: - resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} + regex@5.0.2: + resolution: {integrity: sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==} resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -3679,14 +3675,14 @@ packages: snapshots: - '@3land/listings-sdk@0.0.4(@types/node@22.10.5)(arweave@1.15.5)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': + '@3land/listings-sdk@0.0.4(@types/node@22.10.5)(arweave@1.15.5)(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': dependencies: - '@coral-xyz/borsh': 0.30.1(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@irys/sdk': 0.2.11(arweave@1.15.5)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@coral-xyz/borsh': 0.30.1(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@irys/sdk': 0.2.11(arweave@1.15.5)(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@metaplex-foundation/beet': 0.7.2 - '@metaplex-foundation/mpl-token-metadata': 2.13.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) - '@project-serum/anchor': 0.26.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@metaplex-foundation/mpl-token-metadata': 2.13.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) + '@project-serum/anchor': 0.26.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn: 1.0.5 bn.js: 5.2.1 bs58: 6.0.0 @@ -3761,8 +3757,8 @@ snapshots: dependencies: '@aptos-labs/aptos-cli': 1.0.2 '@aptos-labs/aptos-client': 0.1.1 - '@noble/curves': 1.8.0 - '@noble/hashes': 1.7.0 + '@noble/curves': 1.7.0 + '@noble/hashes': 1.6.1 '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 eventemitter3: 5.0.1 @@ -3856,16 +3852,16 @@ snapshots: - encoding - utf-8-validate - '@coral-xyz/anchor@0.27.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@coral-xyz/anchor@0.27.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: - '@coral-xyz/borsh': 0.27.0(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@coral-xyz/borsh': 0.27.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) base64-js: 1.5.1 bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 camelcase: 6.3.0 - cross-fetch: 3.2.0 + cross-fetch: 3.1.8 crypto-hash: 1.3.0 eventemitter3: 4.0.7 js-sha256: 0.9.0 @@ -3878,7 +3874,7 @@ snapshots: - encoding - utf-8-validate - '@coral-xyz/anchor@0.29.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@noble/hashes': 1.6.1 @@ -3905,27 +3901,27 @@ snapshots: bn.js: 5.2.1 buffer-layout: 1.2.2 - '@coral-xyz/borsh@0.27.0(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coral-xyz/borsh@0.27.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 buffer-layout: 1.2.2 - '@coral-xyz/borsh@0.28.0(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coral-xyz/borsh@0.28.0(@solana/web3.js@1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 buffer-layout: 1.2.2 - '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 buffer-layout: 1.2.2 - '@coral-xyz/borsh@0.30.1(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coral-xyz/borsh@0.30.1(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 buffer-layout: 1.2.2 @@ -4140,7 +4136,7 @@ snapshots: dependencies: '@ethersproject/logger': 5.7.0 - '@ethersproject/providers@5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@ethersproject/providers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/abstract-provider': 5.7.0 '@ethersproject/abstract-signer': 5.7.0 @@ -4161,7 +4157,7 @@ snapshots: '@ethersproject/transactions': 5.7.0 '@ethersproject/web': 5.7.1 bech32: 1.1.4 - ws: 7.4.6(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.4.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -4289,22 +4285,22 @@ snapshots: transitivePeerDependencies: - debug - '@irys/sdk@0.2.11(arweave@1.15.5)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@irys/sdk@0.2.11(arweave@1.15.5)(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@aptos-labs/ts-sdk': 1.33.1 '@ethersproject/bignumber': 5.7.0 '@ethersproject/contracts': 5.7.0 - '@ethersproject/providers': 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@ethersproject/providers': 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@ethersproject/wallet': 5.7.0 '@irys/query': 0.0.8 '@near-js/crypto': 0.0.3 '@near-js/keystores-browser': 0.0.3 '@near-js/providers': 0.0.4 '@near-js/transactions': 0.1.1 - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@supercharge/promise-pool': 3.2.0 algosdk: 1.24.1 - arbundles: 0.11.2(arweave@1.15.5)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + arbundles: 0.11.2(arweave@1.15.5)(bufferutil@4.0.8)(utf-8-validate@5.0.10) async-retry: 1.3.3 axios: 1.7.9 base64url: 3.0.1 @@ -4796,7 +4792,7 @@ snapshots: '@noble/curves@1.7.0': dependencies: - '@noble/hashes': 1.7.0 + '@noble/hashes': 1.6.0 '@noble/ed25519@1.7.3': {} @@ -4857,16 +4853,16 @@ snapshots: '@pkgr/core@0.1.1': {} - '@project-serum/anchor@0.26.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@project-serum/anchor@0.26.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: - '@coral-xyz/borsh': 0.26.0(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@coral-xyz/borsh': 0.26.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) base64-js: 1.5.1 bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 camelcase: 6.3.0 - cross-fetch: 3.2.0 + cross-fetch: 3.1.8 crypto-hash: 1.3.0 eventemitter3: 4.0.7 js-sha256: 0.9.0 @@ -4879,11 +4875,11 @@ snapshots: - encoding - utf-8-validate - '@pythnetwork/client@2.22.0(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@pythnetwork/client@2.22.0(@solana/web3.js@1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: - '@coral-xyz/anchor': 0.29.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@coral-xyz/borsh': 0.28.0(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@coral-xyz/anchor': 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@coral-xyz/borsh': 0.28.0(@solana/web3.js@1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil @@ -4898,15 +4894,15 @@ snapshots: transitivePeerDependencies: - axios - '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@pythnetwork/price-service-sdk': 1.8.0 '@types/ws': 8.5.13 axios: 1.7.9 axios-retry: 3.9.1 - isomorphic-ws: 4.0.1(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + isomorphic-ws: 4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) ts-log: 2.2.7 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - debug @@ -4924,7 +4920,7 @@ snapshots: '@randlabs/communication-bridge': 1.0.1 optional: true - '@raydium-io/raydium-sdk-v2@0.1.95-alpha(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': + '@raydium-io/raydium-sdk-v2@0.1.95-alpha(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 '@solana/spl-token': 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) @@ -5015,9 +5011,11 @@ snapshots: '@shikijs/vscode-textmate@10.0.1': {} + '@shikijs/vscode-textmate@9.3.1': {} + '@sindresorhus/is@4.6.0': {} - '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -5251,6 +5249,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) @@ -5284,15 +5297,15 @@ snapshots: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)': + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5)': + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -5321,12 +5334,12 @@ snapshots: - encoding - utf-8-validate - '@solana/spl-token@0.3.11(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': + '@solana/spl-token@0.3.11(@solana/web3.js@1.95.8(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)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/web3.js': 1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil @@ -5335,7 +5348,7 @@ snapshots: - typescript - utf-8-validate - '@solana/spl-token@0.3.11(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5)(utf-8-validate@5.0.10)': + '@solana/spl-token@0.3.11(@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@4.9.5)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -5418,7 +5431,7 @@ snapshots: '@noble/curves': 1.7.0 '@noble/hashes': 1.6.1 '@solana/buffer-layout': 4.0.1 - agentkeepalive: 4.5.0 + agentkeepalive: 4.6.0 bigint-buffer: 1.1.5 bn.js: 5.2.1 borsh: 0.7.0 @@ -5434,11 +5447,11 @@ snapshots: - encoding - utf-8-validate - '@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@solana/web3.js@1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.26.0 - '@noble/curves': 1.8.0 - '@noble/hashes': 1.7.0 + '@noble/curves': 1.7.0 + '@noble/hashes': 1.6.1 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.6.0 bigint-buffer: 1.1.5 @@ -5447,7 +5460,7 @@ snapshots: bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.1.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + jayson: 4.1.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) node-fetch: 2.7.0 rpc-websockets: 9.0.4 superstruct: 2.0.2 @@ -5456,13 +5469,13 @@ snapshots: - encoding - utf-8-validate - '@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.26.0 '@noble/curves': 1.7.0 '@noble/hashes': 1.6.1 '@solana/buffer-layout': 4.0.1 - agentkeepalive: 4.5.0 + agentkeepalive: 4.6.0 bigint-buffer: 1.1.5 bn.js: 5.2.1 borsh: 0.7.0 @@ -5478,6 +5491,26 @@ snapshots: - encoding - utf-8-validate + '@sqds/multisig@2.1.3(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': + dependencies: + '@metaplex-foundation/beet': 0.7.1 + '@metaplex-foundation/beet-solana': 0.4.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@metaplex-foundation/cusper': 0.0.2 + '@solana/spl-token': 0.3.11(@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) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@types/bn.js': 5.1.6 + assert: 2.1.0 + bn.js: 5.2.1 + buffer: 6.0.3 + invariant: 2.2.4 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - supports-color + - typescript + - utf-8-validate + '@supercharge/promise-pool@3.2.0': {} '@swc/helpers@0.5.15': @@ -5488,7 +5521,7 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tensor-hq/tensor-common@8.3.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': + '@tensor-hq/tensor-common@8.3.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': dependencies: '@coral-xyz/anchor': 0.26.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@metaplex-foundation/mpl-auction-house': 2.5.1(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) @@ -5566,13 +5599,13 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@types/bn.js@5.1.6': + '@types/bn.js@4.11.6': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@types/bn.js@5.1.6': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@types/body-parser@1.19.5': dependencies: @@ -5592,7 +5625,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@types/deep-eql@4.0.2': {} @@ -5602,7 +5635,7 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@types/qs': 6.9.17 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -5636,7 +5669,7 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.5 form-data: 4.0.1 '@types/node@11.11.6': {} @@ -5674,12 +5707,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@types/send': 0.17.4 '@types/unist@3.0.3': {} @@ -5690,11 +5723,11 @@ snapshots: '@types/ws@7.4.7': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@types/ws@8.5.13': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.5 '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)': dependencies: @@ -5836,7 +5869,7 @@ snapshots: dependencies: algo-msgpack-with-bigint: 2.1.1 buffer: 6.0.3 - cross-fetch: 3.2.0 + cross-fetch: 3.1.8 hi-base32: 0.5.1 js-sha256: 0.9.0 js-sha3: 0.8.0 @@ -5869,11 +5902,11 @@ snapshots: ansicolors@0.3.2: {} - arbundles@0.11.2(arweave@1.15.5)(bufferutil@4.0.9)(utf-8-validate@5.0.10): + arbundles@0.11.2(arweave@1.15.5)(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@ethersproject/bytes': 5.7.0 '@ethersproject/hash': 5.7.0 - '@ethersproject/providers': 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@ethersproject/providers': 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@ethersproject/signing-key': 5.7.0 '@ethersproject/transactions': 5.7.0 '@ethersproject/wallet': 5.7.0 @@ -5923,7 +5956,7 @@ snapshots: asn1.js@5.4.1: dependencies: - bn.js: 4.11.6 + bn.js: 4.12.1 inherits: 2.0.4 minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 @@ -6744,13 +6777,13 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flash-sdk@2.24.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10): + flash-sdk@2.24.3(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10): dependencies: - '@coral-xyz/anchor': 0.27.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@pythnetwork/client': 2.22.0(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@pythnetwork/price-service-client': 1.9.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/spl-token': 0.3.11(@solana/web3.js@1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@coral-xyz/anchor': 0.27.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@pythnetwork/client': 2.22.0(@solana/web3.js@1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@pythnetwork/price-service-client': 1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/spl-token': 0.3.11(@solana/web3.js@1.95.8(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) + '@solana/web3.js': 1.95.8(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@types/node': 20.17.11 bignumber.js: 9.1.2 bs58: 5.0.0 @@ -6838,11 +6871,6 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.0.0 - get-stream@5.2.0: dependencies: pump: 3.0.2 @@ -6906,16 +6934,6 @@ snapshots: p-cancelable: 2.1.1 responselike: 2.0.1 - governance-idl-sdk@0.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10): - dependencies: - '@coral-xyz/anchor': 0.29.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - bn.js: 5.2.1 - transitivePeerDependencies: - - bufferutil - - encoding - - utf-8-validate - graceful-fs@4.2.11: optional: true @@ -6931,7 +6949,7 @@ snapshots: '@types/node': 18.19.68 '@types/node-fetch': 2.6.12 abort-controller: 3.0.0 - agentkeepalive: 4.5.0 + agentkeepalive: 4.6.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.7.0 @@ -7065,6 +7083,10 @@ snapshots: through: 2.3.8 wrap-ansi: 6.2.0 + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} ipaddr.js@2.2.0: {} @@ -7109,13 +7131,6 @@ snapshots: is-path-inside@3.0.3: {} - is-regex@1.2.1: - dependencies: - call-bound: 1.0.3 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - is-retry-allowed@2.2.0: {} is-stream@3.0.0: {} @@ -7132,11 +7147,11 @@ snapshots: isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) - isomorphic-ws@4.0.1(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + isomorphic-ws@4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) jackspeak@3.4.3: dependencies: @@ -7592,7 +7607,7 @@ snapshots: '@types/node': 18.19.68 '@types/node-fetch': 2.6.12 abort-controller: 3.0.0 - agentkeepalive: 4.5.0 + agentkeepalive: 4.6.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.7.0 @@ -7856,12 +7871,6 @@ snapshots: safe-buffer@5.2.1: {} - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.3 - es-errors: 1.3.0 - is-regex: 1.2.1 - safer-buffer@2.1.2: {} scrypt-js@3.0.1: {} @@ -8155,7 +8164,7 @@ snapshots: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.10.2 + '@types/node': 22.10.5 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -8377,12 +8386,12 @@ snapshots: wrappy@1.0.2: {} - ws@7.4.6(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@7.4.6(bufferutil@4.0.8)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.0.9 + bufferutil: 4.0.8 utf-8-validate: 5.0.10 - ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.0.8 utf-8-validate: 5.0.10 diff --git a/src/agent/index.ts b/src/agent/index.ts index 256259a3..8ad65b57 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -64,6 +64,7 @@ import { flashOpenTrade, flashCloseTrade, castGovernanceVote, + getVotingPower, } from "../tools"; import { CollectionDeployment, @@ -676,26 +677,11 @@ export class SolanaAgentKit { ): Promise<{ votingPower: number; delegatedPower: number; - totalPower: number; + totalVotesCount: number; + unrelinquishedVotesCount: number; + outstandingProposalCount: number; }> { - const governance = new SplGovernance(this.connection); - const tokenHolding = await governance.getTokenOwnerRecordByOwner( - realm, - governingTokenMint, - this.wallet_address, - ); - - if (!tokenHolding) { - return { votingPower: 0, delegatedPower: 0, totalPower: 0 }; - } - - return { - votingPower: tokenHolding.governingTokenDepositAmount.toNumber(), - delegatedPower: tokenHolding.totalDelegatedVoterWeight.toNumber(), - totalPower: tokenHolding.governingTokenDepositAmount - .add(tokenHolding.totalDelegatedVoterWeight) - .toNumber(), - }; + return await getVotingPower(this, realm, governingTokenMint); } async delegateVotingPower( diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 0ac9877c..ace13c6d 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -287,13 +287,13 @@ export class SolanaPerpCloseTradeTool extends Tool { const tx = parsedInput.side === "long" ? await this.solanaKit.closePerpTradeLong({ - price: parsedInput.price, - tradeMint: new PublicKey(parsedInput.tradeMint), - }) + price: parsedInput.price, + tradeMint: new PublicKey(parsedInput.tradeMint), + }) : await this.solanaKit.closePerpTradeShort({ - price: parsedInput.price, - tradeMint: new PublicKey(parsedInput.tradeMint), - }); + price: parsedInput.price, + tradeMint: new PublicKey(parsedInput.tradeMint), + }); return JSON.stringify({ status: "success", @@ -337,21 +337,21 @@ export class SolanaPerpOpenTradeTool extends Tool { const tx = parsedInput.side === "long" ? await this.solanaKit.openPerpTradeLong({ - price: parsedInput.price, - collateralAmount: parsedInput.collateralAmount, - collateralMint: new PublicKey(parsedInput.collateralMint), - leverage: parsedInput.leverage, - tradeMint: new PublicKey(parsedInput.tradeMint), - slippage: parsedInput.slippage, - }) + price: parsedInput.price, + collateralAmount: parsedInput.collateralAmount, + collateralMint: new PublicKey(parsedInput.collateralMint), + leverage: parsedInput.leverage, + tradeMint: new PublicKey(parsedInput.tradeMint), + slippage: parsedInput.slippage, + }) : await this.solanaKit.openPerpTradeLong({ - price: parsedInput.price, - collateralAmount: parsedInput.collateralAmount, - collateralMint: new PublicKey(parsedInput.collateralMint), - leverage: parsedInput.leverage, - tradeMint: new PublicKey(parsedInput.tradeMint), - slippage: parsedInput.slippage, - }); + price: parsedInput.price, + collateralAmount: parsedInput.collateralAmount, + collateralMint: new PublicKey(parsedInput.collateralMint), + leverage: parsedInput.leverage, + tradeMint: new PublicKey(parsedInput.tradeMint), + slippage: parsedInput.slippage, + }); return JSON.stringify({ status: "success", @@ -807,23 +807,23 @@ export class SolanaFlashOpenTrade extends Tool { if (!Object.keys(marketTokenMap).includes(parsedInput.token)) { throw new Error( "Token must be one of " + - Object.keys(marketTokenMap).join(", ") + - ", received: " + - parsedInput.token + - "\n" + - "Please check https://beast.flash.trade/ for the list of supported tokens", + Object.keys(marketTokenMap).join(", ") + + ", received: " + + parsedInput.token + + "\n" + + "Please check https://beast.flash.trade/ for the list of supported tokens", ); } if (!["long", "short"].includes(parsedInput.type)) { throw new Error( 'Type must be either "long" or "short", received: ' + - parsedInput.type, + parsedInput.type, ); } if (!parsedInput.collateral || parsedInput.collateral <= 0) { throw new Error( "Collateral amount must be positive, received: " + - parsedInput.collateral, + parsedInput.collateral, ); } if (!parsedInput.leverage || parsedInput.leverage <= 0) { @@ -2044,9 +2044,9 @@ export class SolanaRockPaperScissorsTool extends Tool { const result = await this.solanaKit.rockPaperScissors( Number(parsedInput['"amount"']), parsedInput['"choice"'].replace(/^"|"$/g, "") as - | "rock" - | "paper" - | "scissors", + | "rock" + | "paper" + | "scissors", ); return JSON.stringify({ diff --git a/src/tools/governance.ts b/src/tools/governance.ts deleted file mode 100644 index b0f85b9b..00000000 --- a/src/tools/governance.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { PublicKey, Transaction } from "@solana/web3.js"; -import { SolanaAgentKit } from "../index"; -import { - getGovernanceProgramVersion, - getTokenOwnerRecordAddress, - getVoteRecordAddress, - getProposal, - Vote, - VoteChoice, - withCastVote, - getRealm, - VoteKind, - getTokenOwnerRecordsByOwner, -} from "@solana/spl-governance"; -import { BN } from "@coral-xyz/anchor"; - -/** - * 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 castGovernanceVote( - agent: SolanaAgentKit, - realmAccount: PublicKey, - proposalAccount: PublicKey, - voteType: "yes" | "no", -): Promise { - try { - const connection = agent.connection; - const governanceProgramId = new PublicKey( - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", - ); - - // 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}`); - } -} - -/** - * Get current voting power for a wallet in a realm - */ -export async function getVotingPower( - agent: SolanaAgentKit, - realm: PublicKey, - governingTokenMint: PublicKey, -): Promise<{ - votingPower: number; - delegatedPower: number; - totalPower: number; -}> { - try { - - const connection = agent.connection; - const GOVERNANCE_PROGRAM_ID = - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"; - const programId = new PublicKey(GOVERNANCE_PROGRAM_ID); - const ownerRecordsbyOwner = await getTokenOwnerRecordsByOwner( - connection, - programId, - agent.wallet_address, - ); - - // Get token holding data - const tokenHolding = await governance.getTokenOwnerRecordByOwner( - realm, - governingTokenMint, - agent.wallet_address, - ); - - if (!tokenHolding) { - return { votingPower: 0, delegatedPower: 0, totalPower: 0 }; - } - - return { - votingPower: tokenHolding.governingTokenDepositAmount.toNumber(), - delegatedPower: tokenHolding.totalDelegatedVoterWeight.toNumber(), - totalPower: tokenHolding.governingTokenDepositAmount - .add(tokenHolding.totalDelegatedVoterWeight) - .toNumber(), - }; - } catch (error: any) { - throw new Error(`Failed to get voting power: ${error.message}`); - } -} - -/** - * Delegate voting power to another wallet - */ -export async function delegateVotingPower( - agent: SolanaAgentKit, - realm: PublicKey, - governingTokenMint: PublicKey, - delegate: PublicKey, -): Promise { - try { - const governance = new SplGovernance(agent.connection); - - const instruction = await governance.setGovernanceDelegate( - realm, - governingTokenMint, - agent.wallet_address, - delegate, - ); - - const signature = await agent.connection.sendTransaction(instruction, [ - agent.wallet, - ]); - return signature; - } catch (error: any) { - throw new Error(`Failed to delegate voting power: ${error.message}`); - } -} - -/** - * Remove voting power delegation - */ -export async function removeDelegation( - agent: SolanaAgentKit, - realm: PublicKey, - governingTokenMint: PublicKey, -): Promise { - try { - const governance = new SplGovernance(agent.connection); - - const instruction = await governance.setGovernanceDelegate( - realm, - governingTokenMint, - agent.wallet_address, - null, // Passing null removes delegation - ); - - const signature = await agent.connection.sendTransaction(instruction, [ - agent.wallet, - ]); - return signature; - } catch (error: any) { - throw new Error(`Failed to remove delegation: ${error.message}`); - } -} - -/** - * Get voting outcome for a specific proposal - */ -export async function getVotingOutcome( - agent: SolanaAgentKit, - proposal: PublicKey, -): Promise<{ - status: string; - yesVotes: number; - noVotes: number; - abstainVotes: number; - isFinalized: boolean; - votingEndTime: number; -}> { - try { - const governance = new SplGovernance(agent.connection); - const proposalData = await governance.getProposalByPubkey(proposal); - - if (!proposalData) { - throw new Error("Proposal not found"); - } - - return { - status: proposalData.state, - yesVotes: proposalData.getYesVoteCount().toNumber(), - noVotes: proposalData.getNoVoteCount().toNumber(), - abstainVotes: proposalData.getAbstainVoteCount().toNumber(), - isFinalized: proposalData.isVoteFinalized(), - votingEndTime: proposalData.votingEndTime.toNumber(), - }; - } catch (error: any) { - throw new Error(`Failed to get voting outcome: ${error.message}`); - } -} diff --git a/src/tools/realm/governance.ts b/src/tools/realm/governance.ts new file mode 100644 index 00000000..6899f56f --- /dev/null +++ b/src/tools/realm/governance.ts @@ -0,0 +1,432 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +import { SolanaAgentKit } from "../../index"; +import { + getGovernanceProgramVersion, + getTokenOwnerRecordAddress, + getVoteRecordAddress, + getProposal, + Vote, + VoteChoice, + withCastVote, + getRealm, + VoteKind, + getTokenOwnerRecordsByOwner, + withSetGovernanceDelegate, +} from "@solana/spl-governance"; +import { BN } from "@coral-xyz/anchor"; + +/** + * 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 castGovernanceVote( + agent: SolanaAgentKit, + realmAccount: PublicKey, + proposalAccount: PublicKey, + voteType: "yes" | "no", +): Promise { + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + // 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}`); + } +} + +/** + * Get current voting power for a wallet in a realm including delegated power + * + * @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 check power for + * + * @returns {Promise} Object containing voting power details: + * - votingPower: Direct voting power from token deposits + * - delegatedPower: Additional voting power from delegations + * - totalVotesCount: Total number of votes cast + * - unrelinquishedVotesCount: Number of active votes + * - outstandingProposalCount: Number of outstanding proposals + * + * @throws {Error} If public keys are invalid + * @throws {Error} If fetching voting power fails + * + * @example + * const votingPower = await getVotingPower( + * agent, + * new PublicKey("realm-address"), + * new PublicKey("token-mint-address") + * ); + */ +export async function getVotingPower( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, +): Promise<{ + votingPower: number; + delegatedPower: number; + totalVotesCount: number; + unrelinquishedVotesCount: number; + outstandingProposalCount: number; +}> { + // Validate public keys + if ( + !PublicKey.isOnCurve(realm.toBytes()) || + !PublicKey.isOnCurve(governingTokenMint.toBytes()) + ) { + throw new Error("Invalid realm or governingTokenMint address"); + } + + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + // Get all token owner records for this wallet + const tokenOwnerRecords = await getTokenOwnerRecordsByOwner( + connection, + governanceProgramId, + agent.wallet_address, + ); + + // Find the record for the specific token mint we're interested in + const relevantRecord = tokenOwnerRecords.find((record) => + record.account.governingTokenMint.equals(governingTokenMint), + ); + + if (!relevantRecord) { + return { + votingPower: 0, + delegatedPower: 0, + totalVotesCount: 0, + unrelinquishedVotesCount: 0, + outstandingProposalCount: 0, + }; + } + + return { + votingPower: + relevantRecord.account.governingTokenDepositAmount.toNumber(), + // Calculate delegated power based on presence of governance delegate + delegatedPower: relevantRecord.account.governanceDelegate + ? relevantRecord.account.governingTokenDepositAmount.toNumber() + : 0, + totalVotesCount: relevantRecord.account.totalVotesCount, + unrelinquishedVotesCount: relevantRecord.account.unrelinquishedVotesCount, + outstandingProposalCount: relevantRecord.account.outstandingProposalCount, + }; + } catch (error: any) { + throw new Error(`Failed to get voting power: ${error.message}`); + } +} + +/** + * 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 delegateVotingPower( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, + delegate: PublicKey, +): Promise { + // Validate public keys + // if ( + // !PublicKey.isOnCurve(realm.toBytes()) || + // !PublicKey.isOnCurve(governingTokenMint.toBytes()) || + // !PublicKey.isOnCurve(delegate.toBytes()) + // ) { + // throw new Error( + // "Invalid public key provided for realm, governingTokenMint, or delegate", + // ); + // } + + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + // 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}`); + } +} + +/** + * Remove voting power delegation + * + * @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 + * + * @returns {Promise} Transaction signature + * + * @example + * const signature = await removeDelegation( + * agent, + * new PublicKey("realm-address"), + * new PublicKey("token-mint-address") + * ); + */ +export async function removeDelegation( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, +): Promise { + // Can use the same function with null delegate to remove delegation + return delegateVotingPower( + agent, + realm, + governingTokenMint, + PublicKey.default, // Pass default/null public key to remove delegation + ); +} + +/** + * Get detailed voting outcome for a specific governance proposal + * + * @param agent {SolanaAgentKit} The Solana Agent Kit instance + * @param proposalAccount {PublicKey} The public key of the proposal account + * + * @returns {Promise} Object containing: + * - state: Current proposal state + * - yesVotes: Number of approve votes + * - noVotes: Number of deny votes + * - abstainVotes: Number of abstain votes + * - vetoVotes: Weight of veto votes + * - isVoteFinalized: Whether voting has concluded + * - name: Proposal name + * - description: Link to proposal description + * - votingStartedAt: Timestamp when voting started + * - votingCompletedAt: Timestamp when voting completed + * - signatoriesCount: Number of required signatories + * - signatoriesSignedOff: Number of signatories who signed + * + * @throws {Error} If proposal account is invalid + * @throws {Error} If proposal is not found + * + * @example + * const outcome = await getVotingOutcome( + * agent, + * new PublicKey("proposal-address") + * ); + */ +export async function getVotingOutcome( + agent: SolanaAgentKit, + proposalAccount: PublicKey, +): Promise<{ + state: string; + yesVotes: string; + noVotes: string; + abstainVotes: string; + vetoVotes: string; + isVoteFinalized: boolean; + name: string; + description: string; + votingStartedAt: number | null; + votingCompletedAt: number | null; + signatoriesCount: number; + signatoriesSignedOff: number; +}> { + // Validate proposal account + if (!PublicKey.isOnCurve(proposalAccount.toBytes())) { + throw new Error("Invalid proposal account address"); + } + + try { + const connection = agent.connection; + + // Get proposal data using supported API + const proposalData = await getProposal(connection, proposalAccount); + + if (!proposalData) { + throw new Error("Proposal not found"); + } + + const proposal = proposalData.account; + + return { + state: proposal.state.toString(), + yesVotes: proposal.getYesVoteCount().toString(), + noVotes: proposal.getNoVoteCount().toString(), + abstainVotes: proposal.abstainVoteWeight?.toString() || "0", + vetoVotes: proposal.vetoVoteWeight.toString(), + isVoteFinalized: proposal.isVoteFinalized(), + name: proposal.name, + description: proposal.descriptionLink, + votingStartedAt: proposal.votingAt?.toNumber() || null, + votingCompletedAt: proposal.votingCompletedAt?.toNumber() || null, + signatoriesCount: proposal.signatoriesCount, + signatoriesSignedOff: proposal.signatoriesSignedOffCount, + }; + } catch (error: any) { + throw new Error(`Failed to get voting outcome: ${error.message}`); + } +} From 0bdf7d277f25031063a2172c60e14cdbcbd973ca Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sat, 11 Jan 2025 21:07:58 +0530 Subject: [PATCH 3/4] Added separate functions --- src/tools/realm/caste_vote.ts | 126 +++++++ src/tools/realm/delegate_voting_power.ts | 141 ++++++++ src/tools/realm/get_voting_outcome.ts | 85 +++++ src/tools/realm/get_voting_power.ts | 90 +++++ src/tools/realm/governance.ts | 432 ----------------------- src/utils/keypair.ts | 18 +- 6 files changed, 455 insertions(+), 437 deletions(-) create mode 100644 src/tools/realm/caste_vote.ts create mode 100644 src/tools/realm/delegate_voting_power.ts create mode 100644 src/tools/realm/get_voting_outcome.ts create mode 100644 src/tools/realm/get_voting_power.ts delete mode 100644 src/tools/realm/governance.ts diff --git a/src/tools/realm/caste_vote.ts b/src/tools/realm/caste_vote.ts new file mode 100644 index 00000000..7b87a0f7 --- /dev/null +++ b/src/tools/realm/caste_vote.ts @@ -0,0 +1,126 @@ +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"; + +/** + * 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 castGovernanceVote( + agent: SolanaAgentKit, + realmAccount: PublicKey, + proposalAccount: PublicKey, + voteType: "yes" | "no", +): Promise { + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + // 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}`); + } +} diff --git a/src/tools/realm/delegate_voting_power.ts b/src/tools/realm/delegate_voting_power.ts new file mode 100644 index 00000000..611f0622 --- /dev/null +++ b/src/tools/realm/delegate_voting_power.ts @@ -0,0 +1,141 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +import { SolanaAgentKit } from "../../index"; +import { + getGovernanceProgramVersion, + getTokenOwnerRecordAddress, + withSetGovernanceDelegate, +} from "@solana/spl-governance"; + +/** + * 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 delegateVotingPower( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, + delegate: PublicKey, +): Promise { + // Validate public keys + // if ( + // !PublicKey.isOnCurve(realm.toBytes()) || + // !PublicKey.isOnCurve(governingTokenMint.toBytes()) || + // !PublicKey.isOnCurve(delegate.toBytes()) + // ) { + // throw new Error( + // "Invalid public key provided for realm, governingTokenMint, or delegate", + // ); + // } + + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + // 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}`); + } +} + +/** + * Remove voting power delegation + * + * @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 + * + * @returns {Promise} Transaction signature + * + * @example + * const signature = await removeDelegation( + * agent, + * new PublicKey("realm-address"), + * new PublicKey("token-mint-address") + * ); + */ +export async function removeDelegation( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, +): Promise { + // Can use the same function with null delegate to remove delegation + return delegateVotingPower( + agent, + realm, + governingTokenMint, + PublicKey.default, // Pass default/null public key to remove delegation + ); +} diff --git a/src/tools/realm/get_voting_outcome.ts b/src/tools/realm/get_voting_outcome.ts new file mode 100644 index 00000000..9fa7ad5e --- /dev/null +++ b/src/tools/realm/get_voting_outcome.ts @@ -0,0 +1,85 @@ +import { PublicKey } from "@solana/web3.js"; +import { SolanaAgentKit } from "../../index"; +import { getProposal } from "@solana/spl-governance"; + +/** + * Get detailed voting outcome for a specific governance proposal + * + * @param agent {SolanaAgentKit} The Solana Agent Kit instance + * @param proposalAccount {PublicKey} The public key of the proposal account + * + * @returns {Promise} Object containing: + * - state: Current proposal state + * - yesVotes: Number of approve votes + * - noVotes: Number of deny votes + * - abstainVotes: Number of abstain votes + * - vetoVotes: Weight of veto votes + * - isVoteFinalized: Whether voting has concluded + * - name: Proposal name + * - description: Link to proposal description + * - votingStartedAt: Timestamp when voting started + * - votingCompletedAt: Timestamp when voting completed + * - signatoriesCount: Number of required signatories + * - signatoriesSignedOff: Number of signatories who signed + * + * @throws {Error} If proposal account is invalid + * @throws {Error} If proposal is not found + * + * @example + * const outcome = await getVotingOutcome( + * agent, + * new PublicKey("proposal-address") + * ); + */ +export async function getVotingOutcome( + agent: SolanaAgentKit, + proposalAccount: PublicKey, +): Promise<{ + state: string; + yesVotes: string; + noVotes: string; + abstainVotes: string; + vetoVotes: string; + isVoteFinalized: boolean; + name: string; + description: string; + votingStartedAt: number | null; + votingCompletedAt: number | null; + signatoriesCount: number; + signatoriesSignedOff: number; +}> { + // Validate proposal account + if (!PublicKey.isOnCurve(proposalAccount.toBytes())) { + throw new Error("Invalid proposal account address"); + } + + try { + const connection = agent.connection; + + // Get proposal data using supported API + const proposalData = await getProposal(connection, proposalAccount); + + if (!proposalData) { + throw new Error("Proposal not found"); + } + + const proposal = proposalData.account; + + return { + state: proposal.state.toString(), + yesVotes: proposal.getYesVoteCount().toString(), + noVotes: proposal.getNoVoteCount().toString(), + abstainVotes: proposal.abstainVoteWeight?.toString() || "0", + vetoVotes: proposal.vetoVoteWeight.toString(), + isVoteFinalized: proposal.isVoteFinalized(), + name: proposal.name, + description: proposal.descriptionLink, + votingStartedAt: proposal.votingAt?.toNumber() || null, + votingCompletedAt: proposal.votingCompletedAt?.toNumber() || null, + signatoriesCount: proposal.signatoriesCount, + signatoriesSignedOff: proposal.signatoriesSignedOffCount, + }; + } catch (error: any) { + throw new Error(`Failed to get voting outcome: ${error.message}`); + } +} diff --git a/src/tools/realm/get_voting_power.ts b/src/tools/realm/get_voting_power.ts new file mode 100644 index 00000000..50d77164 --- /dev/null +++ b/src/tools/realm/get_voting_power.ts @@ -0,0 +1,90 @@ +import { PublicKey } from "@solana/web3.js"; +import { SolanaAgentKit } from "../../index"; +import { getTokenOwnerRecordsByOwner } from "@solana/spl-governance"; + +/** + * Get current voting power for a wallet in a realm including delegated power + * + * @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 check power for + * + * @returns {Promise} Object containing voting power details: + * - votingPower: Direct voting power from token deposits + * - delegatedPower: Additional voting power from delegations + * - totalVotesCount: Total number of votes cast + * - unrelinquishedVotesCount: Number of active votes + * - outstandingProposalCount: Number of outstanding proposals + * + * @throws {Error} If public keys are invalid + * @throws {Error} If fetching voting power fails + * + * @example + * const votingPower = await getVotingPower( + * agent, + * new PublicKey("realm-address"), + * new PublicKey("token-mint-address") + * ); + */ +export async function getVotingPower( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, +): Promise<{ + votingPower: number; + delegatedPower: number; + totalVotesCount: number; + unrelinquishedVotesCount: number; + outstandingProposalCount: number; +}> { + // Validate public keys + if ( + !PublicKey.isOnCurve(realm.toBytes()) || + !PublicKey.isOnCurve(governingTokenMint.toBytes()) + ) { + throw new Error("Invalid realm or governingTokenMint address"); + } + + try { + const connection = agent.connection; + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + // Get all token owner records for this wallet + const tokenOwnerRecords = await getTokenOwnerRecordsByOwner( + connection, + governanceProgramId, + agent.wallet_address, + ); + + // Find the record for the specific token mint we're interested in + const relevantRecord = tokenOwnerRecords.find((record) => + record.account.governingTokenMint.equals(governingTokenMint), + ); + + if (!relevantRecord) { + return { + votingPower: 0, + delegatedPower: 0, + totalVotesCount: 0, + unrelinquishedVotesCount: 0, + outstandingProposalCount: 0, + }; + } + + return { + votingPower: + relevantRecord.account.governingTokenDepositAmount.toNumber(), + // Calculate delegated power based on presence of governance delegate + delegatedPower: relevantRecord.account.governanceDelegate + ? relevantRecord.account.governingTokenDepositAmount.toNumber() + : 0, + totalVotesCount: relevantRecord.account.totalVotesCount, + unrelinquishedVotesCount: relevantRecord.account.unrelinquishedVotesCount, + outstandingProposalCount: relevantRecord.account.outstandingProposalCount, + }; + } catch (error: any) { + throw new Error(`Failed to get voting power: ${error.message}`); + } +} diff --git a/src/tools/realm/governance.ts b/src/tools/realm/governance.ts deleted file mode 100644 index 6899f56f..00000000 --- a/src/tools/realm/governance.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { PublicKey, Transaction } from "@solana/web3.js"; -import { SolanaAgentKit } from "../../index"; -import { - getGovernanceProgramVersion, - getTokenOwnerRecordAddress, - getVoteRecordAddress, - getProposal, - Vote, - VoteChoice, - withCastVote, - getRealm, - VoteKind, - getTokenOwnerRecordsByOwner, - withSetGovernanceDelegate, -} from "@solana/spl-governance"; -import { BN } from "@coral-xyz/anchor"; - -/** - * 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 castGovernanceVote( - agent: SolanaAgentKit, - realmAccount: PublicKey, - proposalAccount: PublicKey, - voteType: "yes" | "no", -): Promise { - try { - const connection = agent.connection; - const governanceProgramId = new PublicKey( - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", - ); - - // 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}`); - } -} - -/** - * Get current voting power for a wallet in a realm including delegated power - * - * @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 check power for - * - * @returns {Promise} Object containing voting power details: - * - votingPower: Direct voting power from token deposits - * - delegatedPower: Additional voting power from delegations - * - totalVotesCount: Total number of votes cast - * - unrelinquishedVotesCount: Number of active votes - * - outstandingProposalCount: Number of outstanding proposals - * - * @throws {Error} If public keys are invalid - * @throws {Error} If fetching voting power fails - * - * @example - * const votingPower = await getVotingPower( - * agent, - * new PublicKey("realm-address"), - * new PublicKey("token-mint-address") - * ); - */ -export async function getVotingPower( - agent: SolanaAgentKit, - realm: PublicKey, - governingTokenMint: PublicKey, -): Promise<{ - votingPower: number; - delegatedPower: number; - totalVotesCount: number; - unrelinquishedVotesCount: number; - outstandingProposalCount: number; -}> { - // Validate public keys - if ( - !PublicKey.isOnCurve(realm.toBytes()) || - !PublicKey.isOnCurve(governingTokenMint.toBytes()) - ) { - throw new Error("Invalid realm or governingTokenMint address"); - } - - try { - const connection = agent.connection; - const governanceProgramId = new PublicKey( - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", - ); - - // Get all token owner records for this wallet - const tokenOwnerRecords = await getTokenOwnerRecordsByOwner( - connection, - governanceProgramId, - agent.wallet_address, - ); - - // Find the record for the specific token mint we're interested in - const relevantRecord = tokenOwnerRecords.find((record) => - record.account.governingTokenMint.equals(governingTokenMint), - ); - - if (!relevantRecord) { - return { - votingPower: 0, - delegatedPower: 0, - totalVotesCount: 0, - unrelinquishedVotesCount: 0, - outstandingProposalCount: 0, - }; - } - - return { - votingPower: - relevantRecord.account.governingTokenDepositAmount.toNumber(), - // Calculate delegated power based on presence of governance delegate - delegatedPower: relevantRecord.account.governanceDelegate - ? relevantRecord.account.governingTokenDepositAmount.toNumber() - : 0, - totalVotesCount: relevantRecord.account.totalVotesCount, - unrelinquishedVotesCount: relevantRecord.account.unrelinquishedVotesCount, - outstandingProposalCount: relevantRecord.account.outstandingProposalCount, - }; - } catch (error: any) { - throw new Error(`Failed to get voting power: ${error.message}`); - } -} - -/** - * 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 delegateVotingPower( - agent: SolanaAgentKit, - realm: PublicKey, - governingTokenMint: PublicKey, - delegate: PublicKey, -): Promise { - // Validate public keys - // if ( - // !PublicKey.isOnCurve(realm.toBytes()) || - // !PublicKey.isOnCurve(governingTokenMint.toBytes()) || - // !PublicKey.isOnCurve(delegate.toBytes()) - // ) { - // throw new Error( - // "Invalid public key provided for realm, governingTokenMint, or delegate", - // ); - // } - - try { - const connection = agent.connection; - const governanceProgramId = new PublicKey( - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", - ); - - // 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}`); - } -} - -/** - * Remove voting power delegation - * - * @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 - * - * @returns {Promise} Transaction signature - * - * @example - * const signature = await removeDelegation( - * agent, - * new PublicKey("realm-address"), - * new PublicKey("token-mint-address") - * ); - */ -export async function removeDelegation( - agent: SolanaAgentKit, - realm: PublicKey, - governingTokenMint: PublicKey, -): Promise { - // Can use the same function with null delegate to remove delegation - return delegateVotingPower( - agent, - realm, - governingTokenMint, - PublicKey.default, // Pass default/null public key to remove delegation - ); -} - -/** - * Get detailed voting outcome for a specific governance proposal - * - * @param agent {SolanaAgentKit} The Solana Agent Kit instance - * @param proposalAccount {PublicKey} The public key of the proposal account - * - * @returns {Promise} Object containing: - * - state: Current proposal state - * - yesVotes: Number of approve votes - * - noVotes: Number of deny votes - * - abstainVotes: Number of abstain votes - * - vetoVotes: Weight of veto votes - * - isVoteFinalized: Whether voting has concluded - * - name: Proposal name - * - description: Link to proposal description - * - votingStartedAt: Timestamp when voting started - * - votingCompletedAt: Timestamp when voting completed - * - signatoriesCount: Number of required signatories - * - signatoriesSignedOff: Number of signatories who signed - * - * @throws {Error} If proposal account is invalid - * @throws {Error} If proposal is not found - * - * @example - * const outcome = await getVotingOutcome( - * agent, - * new PublicKey("proposal-address") - * ); - */ -export async function getVotingOutcome( - agent: SolanaAgentKit, - proposalAccount: PublicKey, -): Promise<{ - state: string; - yesVotes: string; - noVotes: string; - abstainVotes: string; - vetoVotes: string; - isVoteFinalized: boolean; - name: string; - description: string; - votingStartedAt: number | null; - votingCompletedAt: number | null; - signatoriesCount: number; - signatoriesSignedOff: number; -}> { - // Validate proposal account - if (!PublicKey.isOnCurve(proposalAccount.toBytes())) { - throw new Error("Invalid proposal account address"); - } - - try { - const connection = agent.connection; - - // Get proposal data using supported API - const proposalData = await getProposal(connection, proposalAccount); - - if (!proposalData) { - throw new Error("Proposal not found"); - } - - const proposal = proposalData.account; - - return { - state: proposal.state.toString(), - yesVotes: proposal.getYesVoteCount().toString(), - noVotes: proposal.getNoVoteCount().toString(), - abstainVotes: proposal.abstainVoteWeight?.toString() || "0", - vetoVotes: proposal.vetoVoteWeight.toString(), - isVoteFinalized: proposal.isVoteFinalized(), - name: proposal.name, - description: proposal.descriptionLink, - votingStartedAt: proposal.votingAt?.toNumber() || null, - votingCompletedAt: proposal.votingCompletedAt?.toNumber() || null, - signatoriesCount: proposal.signatoriesCount, - signatoriesSignedOff: proposal.signatoriesSignedOffCount, - }; - } catch (error: any) { - throw new Error(`Failed to get voting outcome: ${error.message}`); - } -} diff --git a/src/utils/keypair.ts b/src/utils/keypair.ts index 25d1a0e7..1b62f1f4 100644 --- a/src/utils/keypair.ts +++ b/src/utils/keypair.ts @@ -1,4 +1,9 @@ -import { Keypair, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js"; +import { + Keypair, + PublicKey, + Transaction, + VersionedTransaction, +} from "@solana/web3.js"; import bs58 from "bs58"; export const keypair = Keypair.generate(); @@ -6,7 +11,6 @@ export const keypair = Keypair.generate(); console.log(keypair.publicKey.toString()); console.log(bs58.encode(keypair.secretKey)); - export class Wallet { private _signer: Keypair; @@ -14,7 +18,9 @@ export class Wallet { this._signer = signer; } - async signTransaction(tx: T): Promise { + async signTransaction( + tx: T, + ): Promise { if (tx instanceof Transaction) { tx.sign(this._signer); } else if (tx instanceof VersionedTransaction) { @@ -25,11 +31,13 @@ export class Wallet { return tx; } - async signAllTransactions(txs: T[]): Promise { + async signAllTransactions( + txs: T[], + ): Promise { return Promise.all(txs.map((tx) => this.signTransaction(tx))); } get publicKey(): PublicKey { return this._signer.publicKey; } -} \ No newline at end of file +} From 071d4cc6e1b2b154764328e81991469a8e067d7e Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Sun, 12 Jan 2025 00:48:02 +0530 Subject: [PATCH 4/4] Added voting governance features --- src/actions/governance.ts | 203 ----------------------- src/actions/index.ts | 12 +- src/actions/realm/castVote.ts | 72 ++++++++ src/actions/realm/delegateVotingPower.ts | 82 +++++++++ src/actions/realm/getVotingOutcome.ts | 91 ++++++++++ src/actions/realm/getVotingPower.ts | 90 ++++++++++ src/agent/index.ts | 56 ++----- src/constants/index.ts | 7 + src/langchain/index.ts | 72 ++++++-- src/tools/index.ts | 2 +- src/tools/realm/caste_vote.ts | 5 +- src/tools/realm/delegate_voting_power.ts | 46 +---- src/tools/realm/get_voting_outcome.ts | 51 +----- src/tools/realm/get_voting_power.ts | 26 +-- src/tools/realm/index.ts | 4 + src/types/index.ts | 8 + 16 files changed, 437 insertions(+), 390 deletions(-) delete mode 100644 src/actions/governance.ts create mode 100644 src/actions/realm/castVote.ts create mode 100644 src/actions/realm/delegateVotingPower.ts create mode 100644 src/actions/realm/getVotingOutcome.ts create mode 100644 src/actions/realm/getVotingPower.ts create mode 100644 src/tools/realm/index.ts diff --git a/src/actions/governance.ts b/src/actions/governance.ts deleted file mode 100644 index b8f558bf..00000000 --- a/src/actions/governance.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; -import { Action } from "../types/action"; -import { SolanaAgentKit } from "../agent"; -import { z } from "zod"; -import { - castVote, - getVotingPower, - delegateVotingPower, - removeDelegation, - getVotingOutcome, -} from "../tools/governance"; - -export const castVoteAction: Action = { - name: "CAST_VOTE", - similes: ["vote", "cast ballot", "vote on proposal"], - description: "Cast a vote on an active governance proposal", - examples: [ - [ - { - input: { - proposal: "4HxrP3R6A6GcUv62VHG331gwJKNhrqHKF438oRztzz2r", - vote: "yes", - comment: "I support this proposal", - }, - output: { - status: "success", - signature: "2ZE7Rz...", - message: "Vote cast successfully", - }, - explanation: "Cast a yes vote with a comment", - }, - ], - ], - schema: z.object({ - proposal: z.string().min(32, "Invalid proposal address"), - vote: z.enum(["yes", "no", "abstain"]), - comment: z.string().optional(), - }), - handler: async (agent: SolanaAgentKit, input: Record) => { - try { - const signature = await castVote( - agent, - new PublicKey(input.proposal), - input.vote, - input.comment, - ); - - return { - status: "success", - signature, - message: "Vote cast successfully", - }; - } catch (error: any) { - return { - status: "error", - message: `Failed to cast vote: ${error.message}`, - }; - } - }, -}; - -export const getVotingPowerAction: Action = { - name: "GET_VOTING_POWER", - similes: ["check voting power", "get voting weight", "view governance power"], - description: "Get current voting power in a realm", - examples: [ - [ - { - input: { - realm: "7nxQB...", - governingTokenMint: "EPjF...", - }, - output: { - status: "success", - votingPower: 1000, - delegatedPower: 500, - totalPower: 1500, - }, - explanation: "Check voting power including delegations", - }, - ], - ], - schema: z.object({ - realm: z.string().min(32, "Invalid realm address"), - governingTokenMint: z.string().min(32, "Invalid token mint address"), - }), - handler: async (agent: SolanaAgentKit, input: Record) => { - try { - const power = await getVotingPower( - agent, - new PublicKey(input.realm), - new PublicKey(input.governingTokenMint), - ); - - return { - status: "success", - ...power, - }; - } catch (error: any) { - return { - status: "error", - message: `Failed to get voting power: ${error.message}`, - }; - } - }, -}; - -export const delegateVotingPowerAction: Action = { - name: "DELEGATE_VOTING_POWER", - similes: ["delegate votes", "transfer voting power", "assign voting rights"], - description: "Delegate voting power to another wallet", - examples: [ - [ - { - input: { - realm: "7nxQB...", - governingTokenMint: "EPjF...", - delegate: "8x2dR...", - }, - output: { - status: "success", - signature: "2ZE7Rz...", - message: "Voting power delegated successfully", - }, - explanation: "Delegate voting power to another wallet", - }, - ], - ], - schema: z.object({ - realm: z.string().min(32, "Invalid realm address"), - governingTokenMint: z.string().min(32, "Invalid token mint address"), - delegate: z.string().min(32, "Invalid delegate address"), - }), - handler: async (agent: SolanaAgentKit, input: Record) => { - try { - const signature = await delegateVotingPower( - agent, - new PublicKey(input.realm), - new PublicKey(input.governingTokenMint), - new PublicKey(input.delegate), - ); - - return { - status: "success", - signature, - message: "Voting power delegated successfully", - }; - } catch (error: any) { - return { - status: "error", - message: `Failed to delegate voting power: ${error.message}`, - }; - } - }, -}; - -export const getVotingOutcomeAction: Action = { - name: "GET_VOTING_OUTCOME", - similes: ["check vote results", "view proposal outcome", "get vote counts"], - description: "Get the current outcome of a governance proposal vote", - examples: [ - [ - { - input: { - proposal: "4HxrP3R6A6GcUv62VHG331gwJKNhrqHKF438oRztzz2r", - }, - output: { - status: "success", - outcome: { - status: "Voting", - yesVotes: 1000000, - noVotes: 500000, - abstainVotes: 100000, - isFinalized: false, - votingEndTime: 1672531200, - }, - }, - explanation: "Get detailed voting results for a proposal", - }, - ], - ], - schema: z.object({ - proposal: z.string().min(32, "Invalid proposal address"), - }), - handler: async (agent: SolanaAgentKit, input: Record) => { - try { - const outcome = await getVotingOutcome( - agent, - new PublicKey(input.proposal), - ); - - return { - status: "success", - outcome, - }; - } catch (error: any) { - return { - status: "error", - message: `Failed to get voting outcome: ${error.message}`, - }; - } - }, -}; diff --git a/src/actions/index.ts b/src/actions/index.ts index 899d93e7..12d32033 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -30,12 +30,10 @@ import launchPumpfunTokenAction from "./launchPumpfunToken"; import getWalletAddressAction from "./getWalletAddress"; import flashOpenTradeAction from "./flashOpenTrade"; import flashCloseTradeAction from "./flashCloseTrade"; -import { - castVoteAction, - getVotingPowerAction, - delegateVotingPowerAction, - getVotingOutcomeAction, -} from "./governance"; +import castGovernanceVoteAction from "./realm/castVote"; +import delegateVotingPowerAction from "./realm/getVotingPower"; +import getVotingOutcomeAction from "./realm/getVotingOutcome"; +import getVotingPowerAction from "./realm/delegateVotingPower"; export const ACTIONS = { WALLET_ADDRESS_ACTION: getWalletAddressAction, @@ -71,7 +69,7 @@ export const ACTIONS = { LAUNCH_PUMPFUN_TOKEN_ACTION: launchPumpfunTokenAction, FLASH_OPEN_TRADE_ACTION: flashOpenTradeAction, FLASH_CLOSE_TRADE_ACTION: flashCloseTradeAction, - CAST_VOTE_ACTION: castVoteAction, + CAST_VOTE_ACTION: castGovernanceVoteAction, GET_VOTING_POWER_ACTION: getVotingPowerAction, DELEGATE_VOTING_POWER_ACTION: delegateVotingPowerAction, GET_VOTING_OUTCOME_ACTION: getVotingOutcomeAction, diff --git a/src/actions/realm/castVote.ts b/src/actions/realm/castVote.ts new file mode 100644 index 00000000..1c41762e --- /dev/null +++ b/src/actions/realm/castVote.ts @@ -0,0 +1,72 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { z } from "zod"; +import { castGovernanceVote } from "../../tools"; +import { SolanaAgentKit } from "../../agent"; + +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 castGovernanceVote( + 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; diff --git a/src/actions/realm/delegateVotingPower.ts b/src/actions/realm/delegateVotingPower.ts new file mode 100644 index 00000000..55c0b84e --- /dev/null +++ b/src/actions/realm/delegateVotingPower.ts @@ -0,0 +1,82 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { z } from "zod"; +import { delegateVotingPower } from "../../tools"; +import { SolanaAgentKit } from "../../agent"; + +const delegateVotingPowerAction: Action = { + name: "DELEGATE_GOVERNANCE_VOTING", + 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 delegateVotingPower( + 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 delegateVotingPowerAction; diff --git a/src/actions/realm/getVotingOutcome.ts b/src/actions/realm/getVotingOutcome.ts new file mode 100644 index 00000000..98d720b5 --- /dev/null +++ b/src/actions/realm/getVotingOutcome.ts @@ -0,0 +1,91 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { z } from "zod"; +import { getVotingOutcome } from "../../tools"; +import { SolanaAgentKit } from "../../agent"; + +const getVotingOutcomeAction: Action = { + name: "GET_GOVERNANCE_VOTING_OUTCOME", + similes: [ + "check proposal votes", + "get proposal results", + "view voting outcome", + "check governance votes", + "get dao vote status", + ], + description: `Get the voting outcome and details for a governance proposal. + + Inputs ( input is a JSON string ): + proposalAccount: string, eg "8x2dR..." (required) - The public key of the proposal account to check`, + + examples: [ + [ + { + input: { + proposalAccount: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk", + }, + output: { + status: "success", + message: "Successfully retrieved proposal voting outcome", + data: { + state: "Voting", + name: "Treasury Transfer Proposal", + yesVotes: "1000000", + noVotes: "500000", + description: "https://example.com/proposal", + isVoteFinalized: false, + votingStartedAt: 1678901234, + votingCompletedAt: null, + signatoriesRequired: 3, + signatoriesSigned: 3, + }, + }, + explanation: "Get voting outcome for a governance proposal", + }, + ], + ], + + schema: z.object({ + proposalAccount: z.string().min(32, "Invalid proposal account address"), + }), + + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const proposal = await getVotingOutcome( + agent, + new PublicKey(input.proposalAccount), + ); + + // Format the response with relevant proposal data + return { + status: "success", + message: "Successfully retrieved proposal voting outcome", + data: { + state: proposal.state.toString(), + name: proposal.name, + yesVotes: proposal.yesVotesCount.toString(), + noVotes: proposal.noVotesCount.toString(), + description: proposal.descriptionLink, + isVoteFinalized: proposal.isVoteFinalized(), + votingStartedAt: proposal.votingAt?.toNumber() || null, + votingCompletedAt: proposal.votingCompletedAt?.toNumber() || null, + signatoriesRequired: proposal.signatoriesCount, + signatoriesSigned: proposal.signatoriesSignedOffCount, + }, + }; + } catch (error: any) { + let errorMessage = error.message; + + if (error.message.includes("Account not found")) { + errorMessage = "Proposal not found - please check the proposal address"; + } + + return { + status: "error", + message: errorMessage, + }; + } + }, +}; + +export default getVotingOutcomeAction; diff --git a/src/actions/realm/getVotingPower.ts b/src/actions/realm/getVotingPower.ts new file mode 100644 index 00000000..16e2835c --- /dev/null +++ b/src/actions/realm/getVotingPower.ts @@ -0,0 +1,90 @@ +import { PublicKey } from "@solana/web3.js"; +import { Action } from "../../types/action"; +import { z } from "zod"; +import { getVotingPower } from "../../tools"; +import { SolanaAgentKit } from "../../agent"; + +const getVotingPowerAction: Action = { + name: "GET_GOVERNANCE_VOTING_POWER", + similes: [ + "check voting power", + "view governance power", + "get dao voting weight", + "check vote delegation power", + "view voting rights", + ], + description: `Get current voting power and delegation details for a 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 to check power for`, + + examples: [ + [ + { + input: { + realm: "7nxQB1nGrqk8WKXeFDR6ZUaQtYjV7HMsAGWgwtGHwmQU", + governingTokenMint: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk", + }, + output: { + status: "success", + message: "Successfully retrieved voting power information", + data: { + votingPower: 1000000, + delegatedPower: 500000, + totalVotesCount: 5, + unrelinquishedVotesCount: 2, + outstandingProposalCount: 1, + }, + }, + explanation: + "Get current voting power and delegation details for a wallet", + }, + ], + ], + + schema: z.object({ + realm: z.string().min(32, "Invalid realm address"), + governingTokenMint: z + .string() + .min(32, "Invalid governing token mint address"), + }), + + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const votingPowerInfo = await getVotingPower( + agent, + new PublicKey(input.realm), + new PublicKey(input.governingTokenMint), + ); + + return { + status: "success", + message: "Successfully retrieved voting power information", + data: { + votingPower: votingPowerInfo.votingPower, + delegatedPower: votingPowerInfo.delegatedPower, + totalVotesCount: votingPowerInfo.totalVotesCount, + unrelinquishedVotesCount: votingPowerInfo.unrelinquishedVotesCount, + outstandingProposalCount: votingPowerInfo.outstandingProposalCount, + }, + }; + } catch (error: any) { + let errorMessage = error.message; + + // Handle specific error cases + if (error.message.includes("Account not found")) { + errorMessage = "No voting record found for this wallet"; + } else if (error.message.includes("Invalid mint")) { + errorMessage = "Invalid governing token mint provided"; + } + + return { + status: "error", + message: errorMessage, + }; + } + }, +}; + +export default getVotingPowerAction; diff --git a/src/agent/index.ts b/src/agent/index.ts index 8ad65b57..0cb25f18 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -3,7 +3,7 @@ import { BN } from "@coral-xyz/anchor"; import bs58 from "bs58"; import Decimal from "decimal.js"; import { DEFAULT_OPTIONS } from "../constants"; -import { Config, TokenCheck } from "../types"; +import { Config, TokenCheck, VotingPowerInfo } from "../types"; import { deploy_collection, deploy_token, @@ -65,6 +65,8 @@ import { flashCloseTrade, castGovernanceVote, getVotingPower, + delegateVotingPower, + getVotingOutcome, } from "../tools"; import { CollectionDeployment, @@ -94,6 +96,7 @@ 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 { Proposal } from "@solana/spl-governance"; /** * Main class for interacting with Solana blockchain @@ -663,25 +666,14 @@ export class SolanaAgentKit { proposalAccount: PublicKey, voteType: "yes" | "no", ): Promise { - return await castGovernanceVote( - this, - realmAccount, - proposalAccount, - voteType, - ); + return castGovernanceVote(this, realmAccount, proposalAccount, voteType); } async getVotingPower( realm: PublicKey, governingTokenMint: PublicKey, - ): Promise<{ - votingPower: number; - delegatedPower: number; - totalVotesCount: number; - unrelinquishedVotesCount: number; - outstandingProposalCount: number; - }> { - return await getVotingPower(this, realm, governingTokenMint); + ): Promise { + return getVotingPower(this, realm, governingTokenMint); } async delegateVotingPower( @@ -689,38 +681,10 @@ export class SolanaAgentKit { governingTokenMint: PublicKey, delegate: PublicKey, ): Promise { - const governance = new SplGovernance(this.connection); - const instruction = await governance.setGovernanceDelegate( - realm, - governingTokenMint, - this.wallet_address, - delegate, - ); - return await this.connection.sendTransaction(instruction, [this.wallet]); + return delegateVotingPower(this, realm, governingTokenMint, delegate); } - async getVotingOutcome(proposal: PublicKey): Promise<{ - status: string; - yesVotes: number; - noVotes: number; - abstainVotes: number; - isFinalized: boolean; - votingEndTime: number; - }> { - const governance = new SplGovernance(this.connection); - const proposalData = await governance.getProposalByPubkey(proposal); - - if (!proposalData) { - throw new Error("Proposal not found"); - } - - return { - status: proposalData.state, - yesVotes: proposalData.getYesVoteCount().toNumber(), - noVotes: proposalData.getNoVoteCount().toNumber(), - abstainVotes: proposalData.getAbstainVoteCount().toNumber(), - isFinalized: proposalData.isVoteFinalized(), - votingEndTime: proposalData.votingEndTime.toNumber(), - }; + async getVotingOutcome(proposal: PublicKey): Promise { + return getVotingOutcome(this, proposal); } } diff --git a/src/constants/index.ts b/src/constants/index.ts index 69965bf8..51045a8d 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -33,3 +33,10 @@ 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 ace13c6d..b98306c8 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -2688,14 +2688,14 @@ export class SolanaExecuteProposal2by2Multisig extends Tool { } } -export class CastGovernanceVoteTool extends Tool { +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, eg "7nxQB..." (required) - proposalAccount: string, eg "8x2dR..." (required) - vote: string, either "yes" or "no" (required)`; + 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(); @@ -2736,10 +2736,13 @@ export class CastGovernanceVoteTool extends Tool { } } -export class GetVotingPowerTool extends Tool { +export class SolanaGetVotingPowerTool extends Tool { name = "get_voting_power"; - description = - "Get current voting power in a realm. Input should be a JSON string containing: realm (address) and governingTokenMint (address)."; + description = `Get current voting power in a realm. + + Inputs (input is a JSON string): + - realmAccount: string, the address eg "7nxQB..." of the realm (required) + - governingTokenMint: string, the PublicKey of the Governing Token Mint (required)`; constructor(private solanaKit: SolanaAgentKit) { super(); @@ -2748,6 +2751,16 @@ export class GetVotingPowerTool extends Tool { protected async _call(input: string): Promise { try { const { realm, governingTokenMint } = JSON.parse(input); + + if ( + !PublicKey.isOnCurve(realm) || + !PublicKey.isOnCurve(governingTokenMint) + ) { + throw new Error( + "Invalid public key provided for realm or governingTokenMint", + ); + } + const votingPower = await this.solanaKit.getVotingPower( new PublicKey(realm), new PublicKey(governingTokenMint), @@ -2768,10 +2781,15 @@ export class GetVotingPowerTool extends Tool { } } -export class DelegateVoteTool extends Tool { +export class SolanaDelegateVoteTool extends Tool { name = "delegate_vote"; - description = - "Delegate voting power to another wallet. Input should be a JSON string containing: realm (address), governingTokenMint (address), and delegate (address)."; + 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(); @@ -2780,6 +2798,17 @@ export class DelegateVoteTool extends Tool { 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.delegateVotingPower( new PublicKey(realm), new PublicKey(governingTokenMint), @@ -2801,10 +2830,12 @@ export class DelegateVoteTool extends Tool { } } -export class GetVotingOutcomeTool extends Tool { +export class SolanaGetVotingOutcomeTool extends Tool { name = "get_voting_outcome"; - description = - "Get the current outcome of a governance proposal. Input should be a JSON string containing: proposal (address)."; + description = `Get the current outcome of a governance proposal. + + Input should be a JSON string containing: + - proposalAccount: string, the address of the proposal (required)`; constructor(private solanaKit: SolanaAgentKit) { super(); @@ -2813,6 +2844,11 @@ export class GetVotingOutcomeTool extends Tool { protected async _call(input: string): Promise { try { const { proposal } = JSON.parse(input); + + if (!PublicKey.isOnCurve(proposal)) { + throw new Error("Invalid public key provided for proposal"); + } + const outcome = await this.solanaKit.getVotingOutcome( new PublicKey(proposal), ); @@ -2899,9 +2935,9 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaApproveProposal2by2Multisig(solanaKit), new SolanaRejectProposal2by2Multisig(solanaKit), new SolanaExecuteProposal2by2Multisig(solanaKit), - new CastVoteTool(solanaKit), - new GetVotingPowerTool(solanaKit), - new DelegateVoteTool(solanaKit), - new GetVotingOutcomeTool(solanaKit), + new SolanaCastGovernanceVoteTool(solanaKit), + new SolanaGetVotingPowerTool(solanaKit), + new SolanaDelegateVoteTool(solanaKit), + new SolanaGetVotingOutcomeTool(solanaKit), ]; } diff --git a/src/tools/index.ts b/src/tools/index.ts index c30957c0..6ef46fae 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -50,4 +50,4 @@ export * from "./flash_open_trade"; export * from "./flash_close_trade"; export * from "./create_3land_collectible"; -export * from "./governance"; +export * from "./realm"; diff --git a/src/tools/realm/caste_vote.ts b/src/tools/realm/caste_vote.ts index 7b87a0f7..b6b005df 100644 --- a/src/tools/realm/caste_vote.ts +++ b/src/tools/realm/caste_vote.ts @@ -11,6 +11,7 @@ import { getRealm, VoteKind, } from "@solana/spl-governance"; +import { GOVERNANCE_PROGRAM_ADDRESS } from "../../constants"; /** * Cast a vote on a governance proposal @@ -40,9 +41,7 @@ export async function castGovernanceVote( ): Promise { try { const connection = agent.connection; - const governanceProgramId = new PublicKey( - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", - ); + const governanceProgramId = new PublicKey(GOVERNANCE_PROGRAM_ADDRESS); // Get governance program version for the connected chain const programVersion = await getGovernanceProgramVersion( diff --git a/src/tools/realm/delegate_voting_power.ts b/src/tools/realm/delegate_voting_power.ts index 611f0622..08913929 100644 --- a/src/tools/realm/delegate_voting_power.ts +++ b/src/tools/realm/delegate_voting_power.ts @@ -5,6 +5,7 @@ import { getTokenOwnerRecordAddress, withSetGovernanceDelegate, } from "@solana/spl-governance"; +import { GOVERNANCE_PROGRAM_ADDRESS } from "../../constants"; /** * Delegate voting power to another wallet in a governance realm @@ -34,22 +35,9 @@ export async function delegateVotingPower( governingTokenMint: PublicKey, delegate: PublicKey, ): Promise { - // Validate public keys - // if ( - // !PublicKey.isOnCurve(realm.toBytes()) || - // !PublicKey.isOnCurve(governingTokenMint.toBytes()) || - // !PublicKey.isOnCurve(delegate.toBytes()) - // ) { - // throw new Error( - // "Invalid public key provided for realm, governingTokenMint, or delegate", - // ); - // } - try { const connection = agent.connection; - const governanceProgramId = new PublicKey( - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", - ); + const governanceProgramId = new PublicKey(GOVERNANCE_PROGRAM_ADDRESS); // Get governance program version for the connected chain const programVersion = await getGovernanceProgramVersion( @@ -109,33 +97,3 @@ export async function delegateVotingPower( throw new Error(`Failed to delegate voting power: ${error.message}`); } } - -/** - * Remove voting power delegation - * - * @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 - * - * @returns {Promise} Transaction signature - * - * @example - * const signature = await removeDelegation( - * agent, - * new PublicKey("realm-address"), - * new PublicKey("token-mint-address") - * ); - */ -export async function removeDelegation( - agent: SolanaAgentKit, - realm: PublicKey, - governingTokenMint: PublicKey, -): Promise { - // Can use the same function with null delegate to remove delegation - return delegateVotingPower( - agent, - realm, - governingTokenMint, - PublicKey.default, // Pass default/null public key to remove delegation - ); -} diff --git a/src/tools/realm/get_voting_outcome.ts b/src/tools/realm/get_voting_outcome.ts index 9fa7ad5e..03887907 100644 --- a/src/tools/realm/get_voting_outcome.ts +++ b/src/tools/realm/get_voting_outcome.ts @@ -1,6 +1,6 @@ import { PublicKey } from "@solana/web3.js"; import { SolanaAgentKit } from "../../index"; -import { getProposal } from "@solana/spl-governance"; +import { getProposal, Proposal } from "@solana/spl-governance"; /** * Get detailed voting outcome for a specific governance proposal @@ -8,19 +8,7 @@ import { getProposal } from "@solana/spl-governance"; * @param agent {SolanaAgentKit} The Solana Agent Kit instance * @param proposalAccount {PublicKey} The public key of the proposal account * - * @returns {Promise} Object containing: - * - state: Current proposal state - * - yesVotes: Number of approve votes - * - noVotes: Number of deny votes - * - abstainVotes: Number of abstain votes - * - vetoVotes: Weight of veto votes - * - isVoteFinalized: Whether voting has concluded - * - name: Proposal name - * - description: Link to proposal description - * - votingStartedAt: Timestamp when voting started - * - votingCompletedAt: Timestamp when voting completed - * - signatoriesCount: Number of required signatories - * - signatoriesSignedOff: Number of signatories who signed + * @returns {Promise} * * @throws {Error} If proposal account is invalid * @throws {Error} If proposal is not found @@ -34,25 +22,7 @@ import { getProposal } from "@solana/spl-governance"; export async function getVotingOutcome( agent: SolanaAgentKit, proposalAccount: PublicKey, -): Promise<{ - state: string; - yesVotes: string; - noVotes: string; - abstainVotes: string; - vetoVotes: string; - isVoteFinalized: boolean; - name: string; - description: string; - votingStartedAt: number | null; - votingCompletedAt: number | null; - signatoriesCount: number; - signatoriesSignedOff: number; -}> { - // Validate proposal account - if (!PublicKey.isOnCurve(proposalAccount.toBytes())) { - throw new Error("Invalid proposal account address"); - } - +): Promise { try { const connection = agent.connection; @@ -65,20 +35,7 @@ export async function getVotingOutcome( const proposal = proposalData.account; - return { - state: proposal.state.toString(), - yesVotes: proposal.getYesVoteCount().toString(), - noVotes: proposal.getNoVoteCount().toString(), - abstainVotes: proposal.abstainVoteWeight?.toString() || "0", - vetoVotes: proposal.vetoVoteWeight.toString(), - isVoteFinalized: proposal.isVoteFinalized(), - name: proposal.name, - description: proposal.descriptionLink, - votingStartedAt: proposal.votingAt?.toNumber() || null, - votingCompletedAt: proposal.votingCompletedAt?.toNumber() || null, - signatoriesCount: proposal.signatoriesCount, - signatoriesSignedOff: proposal.signatoriesSignedOffCount, - }; + return proposal; } catch (error: any) { throw new Error(`Failed to get voting outcome: ${error.message}`); } diff --git a/src/tools/realm/get_voting_power.ts b/src/tools/realm/get_voting_power.ts index 50d77164..d37840c5 100644 --- a/src/tools/realm/get_voting_power.ts +++ b/src/tools/realm/get_voting_power.ts @@ -1,6 +1,7 @@ import { PublicKey } from "@solana/web3.js"; -import { SolanaAgentKit } from "../../index"; +import { SolanaAgentKit, VotingPowerInfo } from "../../index"; import { getTokenOwnerRecordsByOwner } from "@solana/spl-governance"; +import { GOVERNANCE_PROGRAM_ADDRESS } from "../../constants"; /** * Get current voting power for a wallet in a realm including delegated power @@ -9,7 +10,7 @@ import { getTokenOwnerRecordsByOwner } from "@solana/spl-governance"; * @param realm {PublicKey} The public key of the realm * @param governingTokenMint {PublicKey} The mint of the governing token to check power for * - * @returns {Promise} Object containing voting power details: + * @returns {Promise} VotingPowerInfo containing voting power details: * - votingPower: Direct voting power from token deposits * - delegatedPower: Additional voting power from delegations * - totalVotesCount: Total number of votes cast @@ -30,26 +31,10 @@ export async function getVotingPower( agent: SolanaAgentKit, realm: PublicKey, governingTokenMint: PublicKey, -): Promise<{ - votingPower: number; - delegatedPower: number; - totalVotesCount: number; - unrelinquishedVotesCount: number; - outstandingProposalCount: number; -}> { - // Validate public keys - if ( - !PublicKey.isOnCurve(realm.toBytes()) || - !PublicKey.isOnCurve(governingTokenMint.toBytes()) - ) { - throw new Error("Invalid realm or governingTokenMint address"); - } - +): Promise { try { const connection = agent.connection; - const governanceProgramId = new PublicKey( - "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", - ); + const governanceProgramId = new PublicKey(GOVERNANCE_PROGRAM_ADDRESS); // Get all token owner records for this wallet const tokenOwnerRecords = await getTokenOwnerRecordsByOwner( @@ -76,7 +61,6 @@ export async function getVotingPower( return { votingPower: relevantRecord.account.governingTokenDepositAmount.toNumber(), - // Calculate delegated power based on presence of governance delegate delegatedPower: relevantRecord.account.governanceDelegate ? relevantRecord.account.governingTokenDepositAmount.toNumber() : 0, diff --git a/src/tools/realm/index.ts b/src/tools/realm/index.ts new file mode 100644 index 00000000..ad368a3a --- /dev/null +++ b/src/tools/realm/index.ts @@ -0,0 +1,4 @@ +export * from "./caste_vote"; +export * from "./delegate_voting_power"; +export * from "./get_voting_outcome"; +export * from "./get_voting_power"; diff --git a/src/types/index.ts b/src/types/index.ts index 01ac152a..0d0cb617 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -237,3 +237,11 @@ export interface FlashCloseTradeParams { token: string; side: "long" | "short"; } + +export interface VotingPowerInfo { + votingPower: number; + delegatedPower: number; + totalVotesCount: number; + unrelinquishedVotesCount: number; + outstandingProposalCount: number; +}