From 4af66d2b08334144ae281d0b0fabb8e581465e30 Mon Sep 17 00:00:00 2001 From: RudraKc Date: Sat, 11 Jan 2025 06:13:23 +0530 Subject: [PATCH 1/2] feat: Add SPL-Governance Treasury Operations --- .../execute_Approved_Treasury_Actions.ts | 62 +++++++++++++ src/tools/monitor_treasury_balances.ts | 78 +++++++++++++++++ src/tools/propose_transaction.ts | 87 +++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 src/tools/execute_Approved_Treasury_Actions.ts create mode 100644 src/tools/monitor_treasury_balances.ts create mode 100644 src/tools/propose_transaction.ts diff --git a/src/tools/execute_Approved_Treasury_Actions.ts b/src/tools/execute_Approved_Treasury_Actions.ts new file mode 100644 index 00000000..41003f27 --- /dev/null +++ b/src/tools/execute_Approved_Treasury_Actions.ts @@ -0,0 +1,62 @@ +import { + PublicKey, + Transaction, + sendAndConfirmTransaction, + TransactionInstruction, + Signer, +} from "@solana/web3.js"; +import { + InstructionData, + withExecuteTransaction, +} from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Execute a transaction from a proposal. + * + * @param connection The connection to use to send the transaction. + * @param programId The program id of the governance program. + * @param programVersion The version of the governance program. + * @param governance The account of the governance. + * @param proposal The account of the proposal. + * @param transactionAddress The address of the transaction. + * @param transactionInstructions The instructions of the transaction. + * @param executor The executor of the transaction. + * @returns The signature of the transaction. + */ +export async function execute_approved_treasury_actions( + agent: SolanaAgentKit, + programId: PublicKey, + programVersion: number, + governance: PublicKey, + proposal: PublicKey, + transactionAddress: PublicKey, + transactionInstructions: InstructionData[], + executor: Signer, +) { + try { + const connection = agent.connection; + const transaction = new Transaction(); + const instructions: TransactionInstruction[] = []; + + // Prepare the transaction instructions + await withExecuteTransaction( + instructions, + programId, + programVersion, + governance, + proposal, + transactionAddress, + transactionInstructions, + ); + + // Add the instructions to the transaction + transaction.add(...instructions); + + // Send and confirm the transaction + return await sendAndConfirmTransaction(connection, transaction, [executor]); + } catch (error) { + console.error("Failed to execute transaction:", error); + throw error; + } +} diff --git a/src/tools/monitor_treasury_balances.ts b/src/tools/monitor_treasury_balances.ts new file mode 100644 index 00000000..cb4b6f5f --- /dev/null +++ b/src/tools/monitor_treasury_balances.ts @@ -0,0 +1,78 @@ +import { PublicKey } from "@solana/web3.js"; +import { + getGovernanceAccounts, + getNativeTreasuryAddress, + Governance, +} from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Monitors the balances of all treasuries associated with a governance. + * + * @param agent The {@link SolanaAgentKit} instance. + * @param governancePubkey The public key of the governance. + * @returns The balances of all treasuries associated with the governance. + * Each balance object contains the `account` public key, the `solBalance` in SOL, + * and the `splTokens` balance of SPL tokens associated with the treasury. + * The `splTokens` property is an array of objects with `mint` and `balance` properties. + * The `mint` property is the mint address of the SPL token, and the `balance` property is the balance of the SPL token in UI units. + */ +export async function monitor_treasury_balances( + agent: SolanaAgentKit, + governancePubkey: PublicKey, +): Promise< + { + account: PublicKey; + solBalance: number; + splTokens: { mint: string; balance: number }[]; + }[] +> { + try { + const connection = agent.connection; + // Fetch all governance accounts + const governanceAccounts = await getGovernanceAccounts( + connection, + governancePubkey, + Governance, + ); + + const balances = []; + + // Iterate over governance accounts to fetch treasury balances + for (const governance of governanceAccounts) { + // Compute the native treasury address + const treasuryAddress = await getNativeTreasuryAddress( + governancePubkey, + governance.pubkey, + ); + + // Fetch the SOL balance of the treasury + const solBalance = (await connection.getBalance(treasuryAddress)) / 1e9; + + // Fetch SPL token balances + const tokenAccounts = await connection.getParsedTokenAccountsByOwner( + treasuryAddress, + { + programId: new PublicKey( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ), + }, + ); + + const splTokens = tokenAccounts.value.map((tokenAccount) => { + const tokenAmount = tokenAccount.account.data.parsed.info.tokenAmount; + return { + mint: tokenAccount.account.data.parsed.info.mint, + balance: tokenAmount.uiAmount, + }; + }); + + balances.push({ account: treasuryAddress, solBalance, splTokens }); + } + + return balances; + } catch (error) { + console.error("Failed to monitor treasury balances:", error); + throw error; + } +} diff --git a/src/tools/propose_transaction.ts b/src/tools/propose_transaction.ts new file mode 100644 index 00000000..e498e71a --- /dev/null +++ b/src/tools/propose_transaction.ts @@ -0,0 +1,87 @@ +import { + PublicKey, + Transaction, + sendAndConfirmTransaction, + TransactionInstruction, + Signer, +} from "@solana/web3.js"; +import { withCreateProposal, VoteType } from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Propose a transaction to the Solana governance program. + * + * @param connection The Solana RPC connection + * @param programId The program ID of the governance program + * @param programVersion The program version of the governance program + * @param realm The realm public key + * @param governance The governance public key + * @param tokenOwnerRecord The token owner record public key + * @param governingTokenMint The governing token mint public key + * @param governanceAuthority The governance authority public key + * @param payer The payer wallet + * @param name The proposal name + * @param descriptionLink The proposal description link + * @param options The proposal options + * @param useDenyOption Whether to use the deny option (default: true) + * @param proposalIndex The proposal index (default: undefined) + * @param voterWeightRecord The voter weight record public key (default: undefined) + * @returns The proposal public key + * @throws Error if the proposal fails + */ + +export async function propose_transaction( + agent: SolanaAgentKit, + programId: PublicKey, + programVersion: number, + realm: PublicKey, + governance: PublicKey, + tokenOwnerRecord: PublicKey, + governingTokenMint: PublicKey, + governanceAuthority: PublicKey, + payer: Signer, + name: string, + descriptionLink: string, + options: string[], + voteType: VoteType, + useDenyOption: boolean = true, + proposalIndex?: number, + voterWeightRecord?: PublicKey, +) { + try { + const connection = agent.connection; + const transaction = new Transaction(); + const instructions: TransactionInstruction[] = []; + + // Create the proposal + const proposalPublicKey = await withCreateProposal( + instructions, + programId, + programVersion, + realm, + governance, + tokenOwnerRecord, + name, + descriptionLink, + governingTokenMint, + governanceAuthority, + proposalIndex, + voteType, + options, + useDenyOption, + payer.publicKey, + voterWeightRecord, + ); + + // Add the instructions to the transaction + transaction.add(...instructions); + + // Send and confirm the transaction + await sendAndConfirmTransaction(connection, transaction, [payer]); + + return proposalPublicKey; + } catch (error) { + console.error("Failed to propose transaction:", error); + throw error; + } +} From 1e85fa4a55734ab0a5f484be6f0869433918e294 Mon Sep 17 00:00:00 2001 From: RudraKc Date: Sat, 11 Jan 2025 21:12:44 +0530 Subject: [PATCH 2/2] fix: added remaining file changes --- src/agent/index.ts | 48 ++++++ src/langchain/index.ts | 139 ++++++++++++++++++ .../execute_Approved_Treasury_Actions.ts | 72 +++++---- src/tools/index.ts | 3 + src/tools/propose_transaction.ts | 139 ++++++++++++------ 5 files changed, 324 insertions(+), 77 deletions(-) diff --git a/src/agent/index.ts b/src/agent/index.ts index 56517037..e4497a3e 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -63,6 +63,9 @@ import { fetchPythPriceFeedID, flashOpenTrade, flashCloseTrade, + monitor_treasury_balances, + proposeTransaction, + executeApprovedTreasuryActions, } from "../tools"; import { CollectionDeployment, @@ -655,4 +658,49 @@ export class SolanaAgentKit { ): Promise { return execute_transaction(this, transactionIndex); } + + async monitorTreasuryBalances(governancePubkey: PublicKey): Promise { + const res = monitor_treasury_balances(this, governancePubkey); + return JSON.stringify(res); + } + async proposeTransaction( + realmId: PublicKey, + governanceId: PublicKey, + name: string, + descriptionLink: string, + options: string[], + voteType: string, + choiceType: string = "FullWeight", + useDenyOption: boolean = true, + ): Promise { + const res = proposeTransaction( + this, + realmId.toString(), + governanceId.toString(), + name, + descriptionLink, + options, + voteType, + choiceType, + useDenyOption, + ); + return res.toString(); + } + + async executeApprovedTreasuryActions( + realmId: string, + governanceId: string, + proposalId: string, + transactionAddress: string, + transactionInstructions: any[], + ): Promise { + return executeApprovedTreasuryActions( + this, + realmId, + governanceId, + proposalId, + transactionAddress, + transactionInstructions, + ); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index e442206a..ef655794 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -2688,6 +2688,142 @@ export class SolanaExecuteProposal2by2Multisig extends Tool { } } +export class SolanaProposeTransactionTool extends Tool { + name = "propose_transaction"; + description = `Propose a transaction in a Solana DAO governance program. + + Inputs (JSON string): + - realmId: string, the public key of the realm. + - governanceId: string, the public key of the governance account. + - name: string, the name of the proposal. + - descriptionLink: string, a link to the description of the proposal. + - options: string[], the options for the proposal (e.g., "yes", "no"). + - voteType: string, the type of vote ("single" or "multi"). + - choiceType: string, for multi-choice votes ("FullWeight" or "Weighted"). + - useDenyOption: boolean (optional), whether to include a deny option.`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { + realmId, + governanceId, + name, + descriptionLink, + options, + voteType, + choiceType, + useDenyOption, + } = JSON.parse(input); + + const proposalPublicKey = await this.solanaKit.proposeTransaction( + realmId, + governanceId, + name, + descriptionLink, + options, + voteType, + choiceType, + useDenyOption, + ); + + return JSON.stringify({ + status: "success", + proposalPublicKey, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "PROPOSE_TRANSACTION_ERROR", + }); + } + } +} + +export class SolanaMonitorTreasuryBalancesTool extends Tool { + name = "monitor_treasury_balances"; + description = `Monitor the balances of treasuries associated with a Solana governance program. + + Inputs (JSON string): + - governancePubkey: string, the public key of the governance account.`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { governancePubkey } = JSON.parse(input); + + const balances = await this.solanaKit.monitorTreasuryBalances( + new PublicKey(governancePubkey), + ); + + return JSON.stringify({ + status: "success", + balances, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "MONITOR_TREASURY_BALANCES_ERROR", + }); + } + } +} + +export class SolanaExecuteApprovedTreasuryActionsTool extends Tool { + name = "execute_approved_treasury_actions"; + description = `Execute an approved transaction from a proposal in a Solana DAO governance program. + + Inputs (JSON string): + - realmId: string, the public key of the realm. + - governanceId: string, the public key of the governance account. + - proposalId: string, the public key of the proposal. + - transactionAddress: string, the public key of the transaction. + - transactionInstructions: InstructionData[], the instructions for the transaction.`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const { + realmId, + governanceId, + proposalId, + transactionAddress, + transactionInstructions, + } = JSON.parse(input); + + const signature = await this.solanaKit.executeApprovedTreasuryActions( + realmId, + governanceId, + proposalId, + transactionAddress, + transactionInstructions, + ); + + return JSON.stringify({ + status: "success", + signature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "EXECUTE_APPROVED_TREASURY_ACTIONS_ERROR", + }); + } + } +} + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), @@ -2755,5 +2891,8 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaApproveProposal2by2Multisig(solanaKit), new SolanaRejectProposal2by2Multisig(solanaKit), new SolanaExecuteProposal2by2Multisig(solanaKit), + new SolanaProposeTransactionTool(solanaKit), + new SolanaMonitorTreasuryBalancesTool(solanaKit), + new SolanaExecuteApprovedTreasuryActionsTool(solanaKit), ]; } diff --git a/src/tools/execute_Approved_Treasury_Actions.ts b/src/tools/execute_Approved_Treasury_Actions.ts index 41003f27..ff210d7e 100644 --- a/src/tools/execute_Approved_Treasury_Actions.ts +++ b/src/tools/execute_Approved_Treasury_Actions.ts @@ -3,60 +3,74 @@ import { Transaction, sendAndConfirmTransaction, TransactionInstruction, - Signer, } from "@solana/web3.js"; import { InstructionData, withExecuteTransaction, + getGovernanceProgramVersion, } from "@solana/spl-governance"; import { SolanaAgentKit } from "../agent"; /** - * Execute a transaction from a proposal. + * Execute a transaction from an approved proposal. * - * @param connection The connection to use to send the transaction. - * @param programId The program id of the governance program. - * @param programVersion The version of the governance program. - * @param governance The account of the governance. - * @param proposal The account of the proposal. - * @param transactionAddress The address of the transaction. + * @param agent The SolanaAgentKit instance. + * @param realmId The public key of the realm as a string. + * @param governanceId The public key of the governance as a string. + * @param proposalId The public key of the proposal as a string. + * @param transactionAddress The public key of the transaction as a string. * @param transactionInstructions The instructions of the transaction. - * @param executor The executor of the transaction. - * @returns The signature of the transaction. + * @returns The signature of the transaction. */ -export async function execute_approved_treasury_actions( +export async function executeApprovedTreasuryActions( agent: SolanaAgentKit, - programId: PublicKey, - programVersion: number, - governance: PublicKey, - proposal: PublicKey, - transactionAddress: PublicKey, + realmId: string, + governanceId: string, + proposalId: string, + transactionAddress: string, transactionInstructions: InstructionData[], - executor: Signer, -) { +): Promise { + const connection = agent.connection; + const realmPublicKey = new PublicKey(realmId); + const governancePublicKey = new PublicKey(governanceId); + const proposalPublicKey = new PublicKey(proposalId); + const transactionPublicKey = new PublicKey(transactionAddress); + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + try { - const connection = agent.connection; + // Fetch the program version + const programVersion = await getGovernanceProgramVersion( + connection, + governanceProgramId, + ); + + // Prepare transaction and instructions const transaction = new Transaction(); const instructions: TransactionInstruction[] = []; - // Prepare the transaction instructions await withExecuteTransaction( instructions, - programId, + governanceProgramId, programVersion, - governance, - proposal, - transactionAddress, + governancePublicKey, + proposalPublicKey, + transactionPublicKey, transactionInstructions, ); - // Add the instructions to the transaction transaction.add(...instructions); // Send and confirm the transaction - return await sendAndConfirmTransaction(connection, transaction, [executor]); - } catch (error) { - console.error("Failed to execute transaction:", error); - throw error; + const signature = await sendAndConfirmTransaction(connection, transaction, [ + agent.wallet, + ]); + + return signature; + } catch (error: any) { + throw new Error( + `Failed to execute approved treasury actions: ${error.message}`, + ); } } diff --git a/src/tools/index.ts b/src/tools/index.ts index 2363e3ab..4083e8d0 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -50,3 +50,6 @@ export * from "./flash_open_trade"; export * from "./flash_close_trade"; export * from "./create_3land_collectible"; +export * from "./execute_Approved_Treasury_Actions"; +export * from "./monitor_treasury_balances"; +export * from "./propose_transaction"; diff --git a/src/tools/propose_transaction.ts b/src/tools/propose_transaction.ts index e498e71a..631f5d7e 100644 --- a/src/tools/propose_transaction.ts +++ b/src/tools/propose_transaction.ts @@ -5,83 +5,126 @@ import { TransactionInstruction, Signer, } from "@solana/web3.js"; -import { withCreateProposal, VoteType } from "@solana/spl-governance"; +import { + withCreateProposal, + VoteType, + getGovernanceProgramVersion, + getRealm, + getTokenOwnerRecordAddress, + MultiChoiceType, +} from "@solana/spl-governance"; import { SolanaAgentKit } from "../agent"; /** * Propose a transaction to the Solana governance program. * - * @param connection The Solana RPC connection - * @param programId The program ID of the governance program - * @param programVersion The program version of the governance program - * @param realm The realm public key - * @param governance The governance public key - * @param tokenOwnerRecord The token owner record public key - * @param governingTokenMint The governing token mint public key - * @param governanceAuthority The governance authority public key - * @param payer The payer wallet - * @param name The proposal name - * @param descriptionLink The proposal description link - * @param options The proposal options - * @param useDenyOption Whether to use the deny option (default: true) - * @param proposalIndex The proposal index (default: undefined) - * @param voterWeightRecord The voter weight record public key (default: undefined) - * @returns The proposal public key - * @throws Error if the proposal fails + * @param agent The SolanaAgentKit instance. + * @param realmId The public key of the realm as a string. + * @param governanceId The public key of the governance as a string. + * @param name The proposal name. + * @param descriptionLink The proposal description link. + * @param options The proposal options. + * @param voteType The type of vote ("single" or "multi"). + * @param choiceType The type of multi-choice voting ("FullWeight" or "Weighted") for multi-choice votes. + * @param useDenyOption Whether to use the deny option (default: true). + * @returns The public key of the created proposal. */ - -export async function propose_transaction( +export async function proposeTransaction( agent: SolanaAgentKit, - programId: PublicKey, - programVersion: number, - realm: PublicKey, - governance: PublicKey, - tokenOwnerRecord: PublicKey, - governingTokenMint: PublicKey, - governanceAuthority: PublicKey, - payer: Signer, + realmId: string, + governanceId: string, name: string, descriptionLink: string, options: string[], - voteType: VoteType, + voteType: string, + choiceType: string = "FullWeight", useDenyOption: boolean = true, - proposalIndex?: number, - voterWeightRecord?: PublicKey, -) { +): Promise { + const connection = agent.connection; + const realmPublicKey = new PublicKey(realmId); + const governancePublicKey = new PublicKey(governanceId); + const governanceProgramId = new PublicKey( + "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", + ); + + let mappedVoteType: VoteType; + if (voteType.toLowerCase() === "single") { + mappedVoteType = VoteType.SINGLE_CHOICE; + } else if (voteType.toLowerCase() === "multi") { + const choiceTypeMapping: { [key: string]: MultiChoiceType } = { + fullweight: MultiChoiceType.FullWeight, + weighted: MultiChoiceType.Weighted, + }; + + const mappedChoiceType = choiceTypeMapping[choiceType.toLowerCase()]; + if (!mappedChoiceType) { + throw new Error( + `Invalid choiceType '${choiceType}'. Allowed values are 'FullWeight' or 'Weighted' for multi-choice votes.`, + ); + } + + mappedVoteType = VoteType.MULTI_CHOICE( + mappedChoiceType, + 1, + options.length, + 1, + ); + } else { + throw new Error( + `Invalid voteType '${voteType}'. Allowed values are 'single' or 'multi'.`, + ); + } + try { - const connection = agent.connection; + // Fetch the program version + const programVersion = await getGovernanceProgramVersion( + connection, + governanceProgramId, + ); + + // Fetch realm and token information + const realm = await getRealm(connection, realmPublicKey); + const governingTokenMint = realm.account.communityMint; + + // Get the token owner record + const tokenOwnerRecordAddress = await getTokenOwnerRecordAddress( + governanceProgramId, + realmPublicKey, + governingTokenMint, + agent.wallet.publicKey, + ); + + // Create the proposal const transaction = new Transaction(); const instructions: TransactionInstruction[] = []; - // Create the proposal const proposalPublicKey = await withCreateProposal( instructions, - programId, + governanceProgramId, programVersion, - realm, - governance, - tokenOwnerRecord, + realmPublicKey, + governancePublicKey, + tokenOwnerRecordAddress, name, descriptionLink, governingTokenMint, - governanceAuthority, - proposalIndex, - voteType, + agent.wallet.publicKey, + undefined, + mappedVoteType, options, useDenyOption, - payer.publicKey, - voterWeightRecord, + agent.wallet.publicKey, ); - // Add the instructions to the transaction transaction.add(...instructions); // Send and confirm the transaction - await sendAndConfirmTransaction(connection, transaction, [payer]); + await sendAndConfirmTransaction(connection, transaction, [ + agent.wallet as Signer, + ]); return proposalPublicKey; - } catch (error) { - console.error("Failed to propose transaction:", error); - throw error; + } catch (error: any) { + throw new Error(`Failed to propose transaction: ${error.message}`); } }