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 new file mode 100644 index 00000000..ff210d7e --- /dev/null +++ b/src/tools/execute_Approved_Treasury_Actions.ts @@ -0,0 +1,76 @@ +import { + PublicKey, + Transaction, + sendAndConfirmTransaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { + InstructionData, + withExecuteTransaction, + getGovernanceProgramVersion, +} from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Execute a transaction from an approved proposal. + * + * @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. + * @returns The signature of the transaction. + */ +export async function executeApprovedTreasuryActions( + agent: SolanaAgentKit, + realmId: string, + governanceId: string, + proposalId: string, + transactionAddress: string, + transactionInstructions: InstructionData[], +): 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 { + // Fetch the program version + const programVersion = await getGovernanceProgramVersion( + connection, + governanceProgramId, + ); + + // Prepare transaction and instructions + const transaction = new Transaction(); + const instructions: TransactionInstruction[] = []; + + await withExecuteTransaction( + instructions, + governanceProgramId, + programVersion, + governancePublicKey, + proposalPublicKey, + transactionPublicKey, + transactionInstructions, + ); + + transaction.add(...instructions); + + // Send and confirm the transaction + 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/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..631f5d7e --- /dev/null +++ b/src/tools/propose_transaction.ts @@ -0,0 +1,130 @@ +import { + PublicKey, + Transaction, + sendAndConfirmTransaction, + TransactionInstruction, + Signer, +} from "@solana/web3.js"; +import { + withCreateProposal, + VoteType, + getGovernanceProgramVersion, + getRealm, + getTokenOwnerRecordAddress, + MultiChoiceType, +} from "@solana/spl-governance"; +import { SolanaAgentKit } from "../agent"; + +/** + * Propose a transaction to the Solana governance program. + * + * @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 proposeTransaction( + agent: SolanaAgentKit, + realmId: string, + governanceId: string, + name: string, + descriptionLink: string, + options: string[], + voteType: string, + choiceType: string = "FullWeight", + useDenyOption: boolean = true, +): 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 { + // 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[] = []; + + const proposalPublicKey = await withCreateProposal( + instructions, + governanceProgramId, + programVersion, + realmPublicKey, + governancePublicKey, + tokenOwnerRecordAddress, + name, + descriptionLink, + governingTokenMint, + agent.wallet.publicKey, + undefined, + mappedVoteType, + options, + useDenyOption, + agent.wallet.publicKey, + ); + + transaction.add(...instructions); + + // Send and confirm the transaction + await sendAndConfirmTransaction(connection, transaction, [ + agent.wallet as Signer, + ]); + + return proposalPublicKey; + } catch (error: any) { + throw new Error(`Failed to propose transaction: ${error.message}`); + } +}