diff --git a/package.json b/package.json index d7ee5466..cdbd0efd 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "ethers": "^6.13.5", "flash-sdk": "^2.24.3", "form-data": "^4.0.1", + "governance-idl-sdk": "^0.0.4", "langchain": "^0.3.8", "openai": "^4.77.0", "tiktoken": "^1.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfe78d3f..3dca43ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,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)) @@ -2963,6 +2966,9 @@ packages: resolution: {integrity: sha512-1qd54GLxvVgzuidFmw9ze9umxS3rzhdBH6Wt6BTYrTQUXTN01vGGYXwzLzYLowNx8HBH3/c7kRyvx90fh13i7Q==} engines: {node: '>=0.10.0 <7'} + 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==} @@ -9922,6 +9928,16 @@ snapshots: unzip-response: 1.0.2 url-parse-lax: 1.0.0 + 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/index.ts b/src/actions/index.ts index 736a1aa9..b0d34d2f 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -109,6 +109,13 @@ import getCoingeckoTokenPriceDataAction from "./coingecko/getCoingeckoTokenPrice import getCoingeckoTopGainersAction from "./coingecko/getCoingeckoTopGainers"; import getCoingeckoTrendingPoolsAction from "./coingecko/getCoingeckoTrendingPools"; import getCoingeckoTrendingTokensAction from "./coingecko/getCoingeckoTrendingTokens"; +import castVoteAction from "./realm-governance/cast-vote"; +import getRealmInfoAction from "./realm-governance/realm-info"; +import createRealmAction from "./realm-governance/create-realm"; +import createProposalAction from "./realm-governance/create-proposal"; +import getVoterHistoryAction from "./realm-governance/voter-history"; +import getTokenOwnerRecordAction from "./realm-governance/owner-record"; + export const ACTIONS = { GET_INFO_ACTION: getInfoAction, @@ -226,6 +233,13 @@ export const ACTIONS = { GET_COINGECKO_TOP_GAINERS_ACTION: getCoingeckoTopGainersAction, GET_COINGECKO_TRENDING_POOLS_ACTION: getCoingeckoTrendingPoolsAction, GET_COINGECKO_TRENDING_TOKENS_ACTION: getCoingeckoTrendingTokensAction, + CAST_VOTE_ACTION: castVoteAction, + GET_REALM_INFO_ACTION: getRealmInfoAction, + CREATE_REALM_ACTION: createRealmAction, + CREATE_PROPOSAL_ACTION: createProposalAction, + GET_VOTER_HISTORY_ACTION: getVoterHistoryAction, + GET_TOKEN_OWNER_RECORD_ACTION: getTokenOwnerRecordAction, + }; export type { Action, ActionExample, Handler } from "../types/action"; diff --git a/src/actions/realm-governance/cast-vote.ts b/src/actions/realm-governance/cast-vote.ts new file mode 100644 index 00000000..ffc6bf1b --- /dev/null +++ b/src/actions/realm-governance/cast-vote.ts @@ -0,0 +1,69 @@ +import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; +import { SolanaAgentKit } from "../../agent"; +import { Action, VoteConfig } from "../../types"; + +const castVoteAction: Action = { + name: "SPL_CAST_VOTE", + similes: [ + "vote on proposal", + "cast dao vote", + "submit governance vote", + "vote on dao motion", + "participate in dao governance", + "cast ballot on proposal", + ], + description: "Cast a vote on an existing proposal in a DAO", + examples: [ + [ + { + input: { + proposal: "2ZE7Rz...", + tokenOwnerRecord: "8gYZR...", + }, + output: { + status: "success", + voteRecordAddress: "5PmxV...", + message: "Successfully cast vote on proposal", + }, + explanation: "Cast a vote on an existing proposal", + }, + ], + ], + schema: z.object({ + proposal: z.string().min(1).describe("Address of the proposal to vote on"), + tokenOwnerRecord: z + .string() + .min(1) + .describe("Token owner record address for voting"), + + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + + const voteConfig: VoteConfig = { + proposal: new PublicKey(input.proposal), + tokenOwnerRecord: new PublicKey(input.tokenOwnerRecord), + realm: new PublicKey("11111111111111111111111111111111"), + choice: 0, + governingTokenMint: new PublicKey("11111111111111111111111111111111"), + governance: new PublicKey("11111111111111111111111111111111") + }; + + const result = await agent.castVote(voteConfig); + + return { + status: "success", + voteRecordAddress: result.toString(), + message: "Successfully cast vote on proposal", + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to cast vote: ${error.message}`, + }; + } + }, +}; + +export default castVoteAction; \ No newline at end of file diff --git a/src/actions/realm-governance/create-proposal.ts b/src/actions/realm-governance/create-proposal.ts new file mode 100644 index 00000000..61c085ee --- /dev/null +++ b/src/actions/realm-governance/create-proposal.ts @@ -0,0 +1,75 @@ +import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; +import { SolanaAgentKit } from "../../agent"; +import { Action } from "../../types"; + +const createProposalAction: Action = { + name: "SPL_CREATE_PROPOSAL", + similes: [ + "create dao proposal", + "submit governance proposal", + "initiate dao vote", + "propose dao action", + "start governance vote", + "submit dao motion", + ], + description: + "Create a new proposal in a DAO realm for community or council voting", + examples: [ + [ + { + input: { + realm: "7nxQB...", + name: "Treasury Funding Allocation", + description: "Proposal to allocate 1000 tokens to the development team", + governingTokenMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + voteType: "single-choice", + }, + output: { + status: "success", + proposalAddress: "2ZE7Rz...", + message: "Successfully created proposal for community voting", + }, + explanation: + "Create a single-choice proposal for community members to vote on", + }, + ], + ], + schema: z.object({ + realm: z.string().min(1).describe("Address of the DAO realm"), + name: z.string().min(1).describe("Name of the proposal"), + description: z.string().min(1).describe("Description of the proposal"), + governingTokenMint: z + .string() + .min(1) + .describe("Token mint address for voting (community or council)"), + voteType: z + .enum(["single-choice", "multiple-choice"]) + .describe("Type of voting mechanism for the proposal"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const result = await agent.createProposal(new PublicKey(input.realm), { + name: input.name, + description: input.description, + governingTokenMint: new PublicKey(input.governingTokenMint), + voteType: input.voteType, + options: ["Approve", "Deny"], + }); + + return { + status: "success", + proposalAddress: result.toString(), + message: `Successfully created proposal: ${input.name}`, + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to create proposal: ${error.message}`, + }; + } + }, +}; + + +export default createProposalAction; \ No newline at end of file diff --git a/src/actions/realm-governance/create-realm.ts b/src/actions/realm-governance/create-realm.ts new file mode 100644 index 00000000..7ed5b222 --- /dev/null +++ b/src/actions/realm-governance/create-realm.ts @@ -0,0 +1,77 @@ +import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; +import { SolanaAgentKit } from "../../agent"; +import { Action } from "../../types"; + +const createRealmAction: Action = { + name: "SPL_CREATE_REALM", + similes: [ + "create dao realm", + "initialize governance realm", + "setup dao", + "create governance entity", + "establish dao space", + "initialize dao organization", + ], + description: + "Create a new DAO realm with specified configuration for on-chain governance", + examples: [ + [ + { + input: { + name: "My Community DAO", + communityMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + minCommunityTokens: 1000, + councilMint: "So11111111111111111111111111111111111111112", + }, + output: { + status: "success", + realmAddress: "7nxQB...", + message: "Successfully created DAO realm", + }, + explanation: + "Create a new DAO realm with community and council tokens for governance", + }, + ], + ], + schema: z.object({ + name: z.string().min(1).describe("Name of the DAO realm"), + communityMint: z + .string() + .min(1) + .describe("Address of the community token mint"), + minCommunityTokens: z + .number() + .positive() + .describe("Minimum community tokens required to create governance"), + councilMint: z + .string() + .optional() + .describe("Optional address of the council token mint"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const result = await agent.createRealm({ + name: input.name, + communityMint: new PublicKey(input.communityMint), + minCommunityTokensToCreateGovernance: input.minCommunityTokens, + councilMint: input.councilMint + ? new PublicKey(input.councilMint) + : undefined, + }); + + return { + status: "success", + realmAddress: result.toString(), + message: `Successfully created realm: ${input.name}`, + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to create realm: ${error.message}`, + }; + } + }, +}; + +export default createRealmAction; \ No newline at end of file diff --git a/src/actions/realm-governance/owner-record.ts b/src/actions/realm-governance/owner-record.ts new file mode 100644 index 00000000..c064ed2f --- /dev/null +++ b/src/actions/realm-governance/owner-record.ts @@ -0,0 +1,71 @@ +import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; +import { SolanaAgentKit } from "../../agent"; +import { Action } from "../../types"; + +const getTokenOwnerRecordAction: Action = { + name: "SPL_GET_TOKEN_OWNER_RECORD", + similes: [ + "check dao membership status", + "get governance token holdings", + "view dao member record", + "check voting power", + "verify dao participation rights", + "view governance token record", + ], + description: "Get token owner record for a member in a DAO realm", + examples: [ + [ + { + input: { + realm: "7nxQB...", + governingTokenMint: "EPjF...", + governingTokenOwner: "DqYm...", + }, + output: { + status: "success", + tokenOwnerRecord: { + governingTokenOwner: "DqYm...", + tokenBalance: 5000, + // other member data + }, + message: "Successfully retrieved token owner record", + }, + explanation: "Retrieve a member's voting power and participation record", + }, + ], + ], + schema: z.object({ + realm: z.string().min(1).describe("Address of the DAO realm"), + governingTokenMint: z + .string() + .min(1) + .describe("Token mint address for voting (community or council)"), + governingTokenOwner: z + .string() + .min(1) + .describe("Address of the token owner/member"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const result = await agent.getTokenOwnerRecord( + new PublicKey(input.realm), + new PublicKey(input.governingTokenMint), + new PublicKey(input.governingTokenOwner) + ); + + return { + status: "success", + tokenOwnerRecord: result, + message: "Successfully retrieved token owner record", + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to get token owner record: ${error.message}`, + }; + } + }, +}; + +export default getTokenOwnerRecordAction; \ No newline at end of file diff --git a/src/actions/realm-governance/realm-info.ts b/src/actions/realm-governance/realm-info.ts new file mode 100644 index 00000000..274dbe58 --- /dev/null +++ b/src/actions/realm-governance/realm-info.ts @@ -0,0 +1,59 @@ +import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; +import { SolanaAgentKit } from "../../agent"; +import { Action } from "../../types"; +import { e } from "@raydium-io/raydium-sdk-v2/lib/api-0eb57ba2"; + +const getRealmInfoAction: Action = { + name: "SPL_GET_REALM_INFO", + similes: [ + "view dao information", + "check realm status", + "get dao details", + "retrieve governance realm info", + "fetch dao configuration", + "lookup realm data", + ], + description: "Get detailed information about a DAO realm", + examples: [ + [ + { + input: { + realm: "7nxQB...", + }, + output: { + status: "success", + realmInfo: { + name: "My Community DAO", + communityMint: "EPjF...", + councilMint: "So11...", + // other realm data + }, + message: "Successfully retrieved realm information", + }, + explanation: "Retrieve configuration details for a DAO realm", + }, + ], + ], + schema: z.object({ + realm: z.string().min(1).describe("Address of the DAO realm"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const result = await agent.getRealm(new PublicKey(input.realm)); + + return { + status: "success", + realmInfo: result, + message: "Successfully retrieved realm information", + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to get realm info: ${error.message}`, + }; + } + }, +}; + +export default getRealmInfoAction; \ No newline at end of file diff --git a/src/actions/realm-governance/voter-history.ts b/src/actions/realm-governance/voter-history.ts new file mode 100644 index 00000000..5c902b6e --- /dev/null +++ b/src/actions/realm-governance/voter-history.ts @@ -0,0 +1,61 @@ +import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; +import { SolanaAgentKit } from "../../agent"; +import { Action } from "../../types"; + +const getVoterHistoryAction: Action = { + name: "SPL_GET_VOTER_HISTORY", + similes: [ + "check voting history", + "view past votes", + "retrieve voter record", + "get dao participation history", + "review governance activity", + "check vote records", + ], + description: "Get voting history for a specific voter across proposals", + examples: [ + [ + { + input: { + voter: "DqYm...", + }, + output: { + status: "success", + voterHistory: [ + { + proposal: "2ZE7Rz...", + vote: "Approve", + timestamp: 1672531200, + }, + + ], + message: "Successfully retrieved voter history", + }, + explanation: "Retrieve a complete voting record for a DAO participant", + }, + ], + ], + schema: z.object({ + voter: z.string().min(1).describe("Address of the voter"), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + try { + const result = await agent.getVoterHistory(new PublicKey(input.voter)); + + return { + status: "success", + voterHistory: result, + message: "Successfully retrieved voter history", + }; + } catch (error: any) { + return { + status: "error", + message: `Failed to get voter history: ${error.message}`, + }; + } + }, +}; + + +export default getVoterHistoryAction; \ No newline at end of file diff --git a/src/agent/index.ts b/src/agent/index.ts index 09beb9b7..ff3d4f85 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -7,6 +7,11 @@ import { CreateSingleOptions, StoreInitOptions, } from "@3land/listings-sdk/dist/types/implementation/implementationTypes"; +import type { + TokenOwnerRecord, + VoteRecord, + RealmV2 as Realm, +} from "governance-idl-sdk"; import { DEFAULT_OPTIONS } from "../constants"; import { deploy_collection, @@ -141,6 +146,21 @@ import { getTopGainers, getTrendingPools, getTrendingTokens, + createNewRealm, + createNewProposal, + castVoteOnProposal, + getRealmInfo, + getTokenOwnerRecord, + getVoterHistory, + GovernanceMonitor, + MembershipChangeCallback, + VotingPowerChangeCallback, + configureCouncilSettings, + addCouncilMember, + removeCouncilMember, + updateCouncilMemberWeight, + CouncilConfig, + CouncilMemberConfig, } from "../tools"; import { Config, @@ -163,6 +183,9 @@ import { deBridgeOrderStatusResponse, deBridgeTokensInfoResponse, SplAuthorityInput, + VoteConfig, + ProposalConfig, + RealmConfig, } from "../types"; import { DasApiAsset, @@ -181,6 +204,7 @@ import { getSmartTwitterAccountStats, } from "../tools/elfa_ai"; + /** * Main class for interacting with Solana blockchain * Provides a unified interface for token operations, NFT management, trading and more @@ -196,6 +220,7 @@ export class SolanaAgentKit { public wallet: Keypair; public wallet_address: PublicKey; public config: Config; + governanceMonitors: any; /** * @deprecated Using openai_api_key directly in constructor is deprecated. @@ -230,6 +255,8 @@ export class SolanaAgentKit { } } + + // Tool methods async requestFaucetFunds() { return request_faucet_funds(this); @@ -785,6 +812,119 @@ export class SolanaAgentKit { return `Transaction: ${tx}`; } + + + async createRealm(config: RealmConfig): Promise { + return createNewRealm(this, config); + } + + async createProposal( + realm: PublicKey, + config: ProposalConfig, + ): Promise { + return createNewProposal(this, realm, config); + } + async castVote(config: VoteConfig): Promise { + return castVoteOnProposal(this, config); + } + async getRealm(realm: PublicKey): Promise { + return getRealmInfo(this, realm); + } + async getTokenOwnerRecord( + realm: PublicKey, + governingTokenMint: PublicKey, + governingTokenOwner: PublicKey, + ): Promise { + return getTokenOwnerRecord( + this, + realm, + governingTokenMint, + governingTokenOwner, + ); + } + async getVoterHistory(voter: PublicKey): Promise { + return getVoterHistory(this, voter); + } + + async monitorRealmMembership( + realm: PublicKey, + governingTokenMint: PublicKey, + callback: MembershipChangeCallback, + ): Promise { + const key = `${realm.toBase58()}-${governingTokenMint.toBase58()}`; + let monitor = this.governanceMonitors.get(key); + + if (!monitor) { + monitor = new GovernanceMonitor(this, realm, governingTokenMint); + this.governanceMonitors.set(key, monitor); + } + + await monitor.monitorMembershipChanges(callback); + } + + async monitorVotingPower( + realm: PublicKey, + governingTokenMint: PublicKey, + callback: VotingPowerChangeCallback, + ): Promise { + const key = `${realm.toBase58()}-${governingTokenMint.toBase58()}`; + let monitor = this.governanceMonitors.get(key); + + if (!monitor) { + monitor = new GovernanceMonitor(this, realm, governingTokenMint); + this.governanceMonitors.set(key, monitor); + } + + await monitor.monitorVotingPowerChanges(callback); + } + + async stopMonitoring( + realm: PublicKey, + governingTokenMint: PublicKey, + ): Promise { + const key = `${realm.toBase58()}-${governingTokenMint.toBase58()}`; + const monitor = this.governanceMonitors.get(key); + + if (monitor) { + await monitor.stopMonitoring(); + this.governanceMonitors.delete(key); + } + } + + async configureCouncil( + realm: PublicKey, + config: CouncilConfig, + ): Promise { + return configureCouncilSettings(this, realm, config); + } + + async addCouncilMember( + realm: PublicKey, + councilMint: PublicKey, + memberConfig: CouncilMemberConfig, + ): Promise { + return addCouncilMember(this, realm, councilMint, memberConfig); + } + + async removeCouncilMember( + realm: PublicKey, + councilMint: PublicKey, + member: PublicKey, + ): Promise { + return removeCouncilMember(this, realm, councilMint, member); + } + + async updateCouncilMemberWeight( + realm: PublicKey, + councilMint: PublicKey, + memberConfig: CouncilMemberConfig, + ): Promise { + return updateCouncilMemberWeight(this, realm, councilMint, memberConfig); + } + + + + async create3LandNft( collectionAccount: string, createItemOptions: CreateSingleOptions, diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 8ede830a..92783ff8 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -35,6 +35,7 @@ export * from "./switchboard"; export * from "./elfa_ai"; export * from "./debridge"; export * from "./fluxbeam"; +export * from "./realm-governance"; import type { SolanaAgentKit } from "../agent"; import { @@ -158,8 +159,13 @@ import { ElfaGetTopMentionsTool, ElfaAccountSmartStatsTool, SolanaFluxbeamCreatePoolTool, + SolanaCreateRealmTool, + SolanaCreateProposalTool, + SolanaCastVoteTool, + SolanaGetRealmInfoTool, } from "./index"; + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaGetInfoTool(solanaKit), @@ -282,5 +288,11 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new ElfaGetTopMentionsTool(solanaKit), new ElfaAccountSmartStatsTool(solanaKit), new SolanaFluxbeamCreatePoolTool(solanaKit), + new SolanaApproveProposal2by2Multisig(solanaKit), + new SolanaCreateRealmTool(solanaKit), + new SolanaCreateProposalTool(solanaKit), + new SolanaCastVoteTool(solanaKit), + new SolanaGetRealmInfoTool(solanaKit), + new SolanaApproveProposal2by2Multisig(solanaKit), ]; } diff --git a/src/langchain/realm-governance/index.ts b/src/langchain/realm-governance/index.ts new file mode 100644 index 00000000..4d5d819a --- /dev/null +++ b/src/langchain/realm-governance/index.ts @@ -0,0 +1 @@ + export * from "./realm-governance"; \ No newline at end of file diff --git a/src/langchain/realm-governance/realm-governance.ts b/src/langchain/realm-governance/realm-governance.ts new file mode 100644 index 00000000..9b308063 --- /dev/null +++ b/src/langchain/realm-governance/realm-governance.ts @@ -0,0 +1,201 @@ +import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; +import { Tool } from "@langchain/core/tools"; +import { SolanaAgentKit } from "../../agent"; +import { VoteConfig, ProposalConfig } from "../../types"; + +export class SolanaCreateRealmTool extends Tool { + name = "create_realm"; + description = "Create a new DAO realm with specified configuration"; + + override schema = z + .object({ + input: z.string().optional(), + }) + .transform((args) => args.input); + + constructor(private agent: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const args = JSON.parse(input || "{}") as { + name: string; + communityMint: string; + minCommunityTokens: number; + councilMint?: string; + }; + + const result = await this.agent.createRealm({ + name: args.name, + communityMint: new PublicKey(args.communityMint), + minCommunityTokensToCreateGovernance: args.minCommunityTokens, + councilMint: args.councilMint + ? new PublicKey(args.councilMint) + : undefined, + }); + return `Successfully created realm with address: ${result.toString()}`; + } catch (error) { + throw new Error(`Failed to create realm: ${error}`); + } + } +} + +export class SolanaCreateProposalTool extends Tool { + name = "create_proposal"; + description = "Create a new proposal in a DAO realm"; + + override schema = z + .object({ + input: z.string().optional(), + }) + .transform((args) => args.input); + + constructor(private agent: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const args = JSON.parse(input || "{}") as { + realm: string; + name: string; + description: string; + governingTokenMint: string; + voteType: "single-choice" | "multiple-choice"; + }; + + const result = await this.agent.createProposal( + new PublicKey(args.realm), + { + name: args.name, + description: args.description, + governingTokenMint: new PublicKey(args.governingTokenMint), + voteType: args.voteType, + options: ["Approve", "Deny"], + } as ProposalConfig, + ); + return `Successfully created proposal with address: ${result.toString()}`; + } catch (error) { + throw new Error(`Failed to create proposal: ${error}`); + } + } +} + +export class SolanaCastVoteTool extends Tool { + name = "cast_vote"; + description = "Cast a vote on a proposal"; + + override schema = z + .object({ + input: z.string().optional(), + }) + .transform((args) => args.input); + + constructor(private agent: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const args = JSON.parse(input || "{}") as VoteConfig; + const result = await this.agent.castVote(args); + return `Successfully cast vote on proposal: ${result.toString()}`; + } catch (error) { + throw new Error(`Failed to cast vote: ${error}`); + } + } +} + +export class SolanaGetRealmInfoTool extends Tool { + name = "get_realm_info"; + description = "Get information about a DAO realm"; + + override schema = z + .object({ + input: z.string().optional(), + }) + .transform((args) => args.input); + + constructor(private agent: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const args = JSON.parse(input || "{}") as { + realm: string; + }; + + const result = await this.agent.getRealm(new PublicKey(args.realm)); + return JSON.stringify(result); + } catch (error) { + throw new Error(`Failed to get realm info: ${error}`); + } + } +} + +export class SolanaGetTokenOwnerRecordTool extends Tool { + name = "get_token_owner_record"; + description = "Get token owner record for a member in a DAO realm"; + + override schema = z + .object({ + input: z.string().optional(), + }) + .transform((args) => args.input); + + constructor(private agent: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const args = JSON.parse(input || "{}") as { + realm: string; + governingTokenMint: string; + governingTokenOwner: string; + }; + + const result = await this.agent.getTokenOwnerRecord( + new PublicKey(args.realm), + new PublicKey(args.governingTokenMint), + new PublicKey(args.governingTokenOwner), + ); + return JSON.stringify(result); + } catch (error) { + throw new Error(`Failed to get token owner record: ${error}`); + } + } +} + +export class SolanaGetVoterHistoryTool extends Tool { + name = "get_voter_history"; + description = "Get voting history for a specific voter"; + + override schema = z + .object({ + input: z.string().optional(), + }) + .transform((args) => args.input); + + constructor(private agent: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const args = JSON.parse(input || "{}") as { + voter: string; + }; + + const result = await this.agent.getVoterHistory( + new PublicKey(args.voter), + ); + return JSON.stringify(result); + } catch (error) { + throw new Error(`Failed to get voter history: ${error}`); + } + } +} \ No newline at end of file diff --git a/src/tools/index.ts b/src/tools/index.ts index 913ad862..074e0516 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -35,3 +35,4 @@ export * from "./switchboard"; export * from "./elfa_ai"; export * from "./fluxbeam"; export * from "./coingecko"; +export * from "./realm-governance"; \ No newline at end of file diff --git a/src/tools/realm-governance/council.ts b/src/tools/realm-governance/council.ts new file mode 100644 index 00000000..7b9a5031 --- /dev/null +++ b/src/tools/realm-governance/council.ts @@ -0,0 +1,332 @@ +import { PublicKey, Transaction } from "@solana/web3.js"; +import { SplGovernance } from "governance-idl-sdk"; +import { BN } from "@coral-xyz/anchor"; +import { SolanaAgentKit } from "../../agent"; + +export interface CouncilConfig { + minTokensToCreateGovernance: number; + minVotingThreshold: number; + minTransactionHoldUpTime: number; + maxVotingTime: number; + voteTipping: "strict" | "early" | "disabled"; +} + +export interface CouncilMemberConfig { + member: PublicKey; + tokenAmount: number; +} + +export async function configureCouncilSettings( + agent: SolanaAgentKit, + realm: PublicKey, + config: CouncilConfig, +): Promise { + const splGovernance = new SplGovernance(agent.connection); + + // Get the governance account for the realm + const governanceAccounts = + await splGovernance.getGovernanceAccountsByRealm(realm); + + if (!governanceAccounts.length) { + throw new Error("No governance account found for realm"); + } + + const governance = governanceAccounts[0]; + + // Create config instruction + const configureIx = + await splGovernance.program.instruction.setGovernanceConfig( + { + communityVoteThreshold: { value: new BN(config.minVotingThreshold) }, + minCommunityWeightToCreateProposal: new BN( + config.minTokensToCreateGovernance, + ), + minTransactionHoldUpTime: new BN(config.minTransactionHoldUpTime), + maxVotingTime: new BN(config.maxVotingTime), + votingBaseTime: new BN(0), + votingCoolOffTime: new BN(0), + depositExemptProposalCount: 0, + communityVoteTipping: config.voteTipping, + councilVoteTipping: config.voteTipping, + minCouncilWeightToCreateProposal: new BN(1), + councilVoteThreshold: { value: new BN(1) }, + councilVetoVoteThreshold: { value: new BN(0) }, + communityVetoVoteThreshold: { value: new BN(0) }, + }, + { + accounts: { + governanceAccount: governance.publicKey, + }, + }, + ); + + const transaction = new Transaction().add(configureIx); + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + + return signature; +} + +export async function addCouncilMember( + agent: SolanaAgentKit, + realm: PublicKey, + councilMint: PublicKey, + memberConfig: CouncilMemberConfig, +): Promise { + const splGovernance = new SplGovernance(agent.connection); + + // Get all realm configs + const allRealmConfigs = await splGovernance.getAllRealmConfigs(); + const realmConfig = allRealmConfigs.find((config: { realm: PublicKey }) => + config.realm.equals(realm), + ); + if (!realmConfig) { + throw new Error("Realm config not found"); + } + + // Get token owner record + const tokenOwnerRecords = await splGovernance.getTokenOwnerRecordsForOwner( + memberConfig.member, + ); + const tokenOwnerRecord = tokenOwnerRecords.find( + (record) => + record.realm.equals(realm) && + record.governingTokenMint.equals(councilMint), + ); + + if (!tokenOwnerRecord) { + throw new Error("Token owner record not found"); + } + + // Create mint tokens instruction for council member + const mintTokensIx = + await splGovernance.program.instruction.depositGoverningTokens( + new BN(memberConfig.tokenAmount), + { + accounts: { + realmAccount: realm, + realmConfigAccount: realmConfig.publicKey, + governingTokenHoldingAccount: councilMint, + governingTokenOwnerAccount: memberConfig.member, + governingTokenSourceAccount: agent.wallet.publicKey, + governingTokenSourceAccountAuthority: agent.wallet.publicKey, + tokenOwnerRecord: tokenOwnerRecord.publicKey, + payer: agent.wallet.publicKey, + tokenProgram: new PublicKey( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ), + systemProgram: new PublicKey("11111111111111111111111111111111"), + }, + }, + ); + + const transaction = new Transaction().add(mintTokensIx); + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + + return signature; +} + +export async function removeCouncilMember( + agent: SolanaAgentKit, + realm: PublicKey, + councilMint: PublicKey, + member: PublicKey, +): Promise { + const splGovernance = new SplGovernance(agent.connection); + + // Get all realm configs + const allRealmConfigs = await splGovernance.getAllRealmConfigs(); + const realmConfigForBurn = allRealmConfigs.find( + (config: { realm: PublicKey }) => config.realm.equals(realm), + ); + if (!realmConfigForBurn) { + throw new Error("Realm config not found"); + } + + // Get token owner record for the member + const tokenOwnerRecords = + await splGovernance.getTokenOwnerRecordsForOwner(member); + const councilRecord = tokenOwnerRecords.find( + (record) => + record.realm.equals(realm) && + record.governingTokenMint.equals(councilMint), + ); + + if (!councilRecord) { + throw new Error("Council member record not found"); + } + + // Create withdraw tokens instruction + const withdrawTokensIx = + await splGovernance.program.instruction.withdrawGoverningTokens( + councilRecord.governingTokenDepositAmount, + { + accounts: { + realmAccount: realm, + realmConfigAccount: realmConfigForBurn.publicKey, + governingTokenHoldingAccount: councilMint, + governingTokenOwnerAccount: member, + governingTokenDestinationAccount: agent.wallet.publicKey, + tokenOwnerRecord: councilRecord.publicKey, + tokenProgram: new PublicKey( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ), + }, + }, + ); + + const transaction = new Transaction().add(withdrawTokensIx); + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + + return signature; +} + +export async function updateCouncilMemberWeight( + agent: SolanaAgentKit, + realm: PublicKey, + councilMint: PublicKey, + memberConfig: CouncilMemberConfig, +): Promise { + const splGovernance = new SplGovernance(agent.connection); + + // Get current token owner record + const tokenOwnerRecords = await splGovernance.getTokenOwnerRecordsForOwner( + memberConfig.member, + ); + + const councilRecord = tokenOwnerRecords.find( + (record) => + record.realm.equals(realm) && + record.governingTokenMint.equals(councilMint), + ); + + if (!councilRecord) { + throw new Error("Council member record not found"); + } + + const currentAmount = councilRecord.governingTokenDepositAmount; + const targetAmount = new BN(memberConfig.tokenAmount); + + const transaction = new Transaction(); + + if (targetAmount.gt(currentAmount)) { + // Mint additional tokens + const mintAmount = targetAmount.sub(currentAmount); + + // Get realm config + const allRealmConfigs = await splGovernance.getAllRealmConfigs(); + const realmConfig = allRealmConfigs.find((config: { realm: PublicKey }) => + config.realm.equals(realm), + ); + if (!realmConfig) { + throw new Error("Realm config not found"); + } + + // Get token owner record + const tokenOwnerRecords = await splGovernance.getTokenOwnerRecordsForOwner( + memberConfig.member, + ); + const tokenOwnerRecord = tokenOwnerRecords.find( + (record) => + record.realm.equals(realm) && + record.governingTokenMint.equals(councilMint), + ); + + if (!tokenOwnerRecord) { + throw new Error("Token owner record not found"); + } + + // Get realm config account + const realmConfigPda = splGovernance.pda.realmConfigAccount({ + realmAccount: realm, + }).publicKey; + + // Get token owner record + const tokenOwnerRecordPda = splGovernance.pda.tokenOwnerRecordAccount({ + realmAccount: realm, + governingTokenMintAccount: councilMint, + governingTokenOwner: memberConfig.member, + }).publicKey; + + // Create mint tokens instruction + const mintTokensIx = + await splGovernance.program.instruction.depositGoverningTokens( + mintAmount, + { + accounts: { + realmAccount: realm, + realmConfigAccount: realmConfigPda, + governingTokenHoldingAccount: councilMint, + governingTokenOwnerAccount: memberConfig.member, + governingTokenSourceAccount: agent.wallet.publicKey, + governingTokenSourceAccountAuthority: agent.wallet.publicKey, + tokenOwnerRecord: tokenOwnerRecordPda, + payer: agent.wallet.publicKey, + tokenProgram: new PublicKey( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ), + systemProgram: new PublicKey("11111111111111111111111111111111"), + }, + }, + ); + transaction.add(mintTokensIx); + } else if (targetAmount.lt(currentAmount)) { + // Burn excess tokens + const burnAmount = currentAmount.sub(targetAmount); + + // Get realm config + const allRealmConfigs = await splGovernance.getAllRealmConfigs(); + const realmConfig = allRealmConfigs.find((config: { realm: PublicKey }) => + config.realm.equals(realm), + ); + if (!realmConfig) { + throw new Error("Realm config not found"); + } + + // Get token owner record + const tokenOwnerRecords = await splGovernance.getTokenOwnerRecordsForOwner( + memberConfig.member, + ); + const tokenOwnerRecord = tokenOwnerRecords.find( + (record) => + record.realm.equals(realm) && + record.governingTokenMint.equals(councilMint), + ); + + if (!tokenOwnerRecord) { + throw new Error("Token owner record not found"); + } + + const burnTokensIx = + await splGovernance.program.instruction.withdrawGoverningTokens( + burnAmount, + { + accounts: { + realmAccount: realm, + realmConfigAccount: realmConfig.publicKey, + governingTokenHoldingAccount: councilMint, + governingTokenOwnerAccount: memberConfig.member, + governingTokenDestinationAccount: agent.wallet.publicKey, + tokenOwnerRecord: tokenOwnerRecord.publicKey, + tokenProgram: new PublicKey( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ), + }, + }, + ); + transaction.add(burnTokensIx); + } else { + throw new Error("No weight change needed"); + } + + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + + return signature; +} \ No newline at end of file diff --git a/src/tools/realm-governance/governance.ts b/src/tools/realm-governance/governance.ts new file mode 100644 index 00000000..b0f24ace --- /dev/null +++ b/src/tools/realm-governance/governance.ts @@ -0,0 +1,186 @@ +import { Connection, PublicKey, Transaction } from "@solana/web3.js"; +import { SplGovernance } from "governance-idl-sdk"; +import { BN } from "@coral-xyz/anchor"; +import { + RealmConfig, + ProposalConfig, + VoteConfig, + VoteType, + Vote, + VoteChoice, +} from "../../types"; +import { SolanaAgentKit } from "../../agent"; + +export async function createNewRealm( + agent: SolanaAgentKit, + config: RealmConfig, +): Promise { + const splGovernance = new SplGovernance(agent.connection); + + const createRealmIx = await splGovernance.createRealmInstruction( + config.name, + config.communityMint, + config.minCommunityTokensToCreateGovernance, + agent.wallet.publicKey, + undefined, // communityMintMaxVoterWeightSource + config.councilMint, + config.communityTokenConfig?.tokenType || "liquid", + "membership", // councilTokenType + ); + + const transaction = new Transaction().add(createRealmIx); + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + + return new PublicKey(signature); +} + +export async function createNewProposal( + agent: SolanaAgentKit, + realm: PublicKey, + config: ProposalConfig, +): Promise { + const splGovernance = new SplGovernance(agent.connection); + + // Get the governance account for the realm + const governanceAccounts = + await splGovernance.getGovernanceAccountsByRealm(realm); + if (!governanceAccounts.length) { + throw new Error("No governance account found for realm"); + } + + const governance = governanceAccounts[0]; + + // Get token owner record + const tokenOwnerRecords = await splGovernance.getTokenOwnerRecordsForOwner( + agent.wallet.publicKey, + ); + const tokenOwnerRecord = tokenOwnerRecords.find( + (record) => + record.realm.equals(realm) && + record.governingTokenMint.equals(config.governingTokenMint), + ); + + if (!tokenOwnerRecord) { + throw new Error("Token owner record not found"); + } + + const voteType: VoteType = { + choiceType: config.voteType === "single-choice" ? "single" : "multi", + multiChoiceOptions: + config.voteType === "multiple-choice" + ? { + choiceType: "fullWeight", + minVoterOptions: 1, + maxVoterOptions: config.options.length, + maxWinningOptions: 1, + } + : null, + }; + + const createProposalIx = await splGovernance.createProposalInstruction( + config.name, + config.description, + voteType, + [config.options[0]], // Only support single option for now + true, // useDenyOption + realm, + governance.publicKey, + tokenOwnerRecord.publicKey, + config.governingTokenMint, + agent.wallet.publicKey, + agent.wallet.publicKey, // payer + ); + + const transaction = new Transaction().add(createProposalIx); + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + + return new PublicKey(signature); +} + +export async function castVoteOnProposal( + agent: SolanaAgentKit, + config: VoteConfig, +): Promise { + const splGovernance = new SplGovernance(agent.connection); + + // Get token owner record + const tokenOwnerRecords = await splGovernance.getTokenOwnerRecordsForOwner( + config.tokenOwner || agent.wallet.publicKey, + ); + const tokenOwnerRecord = tokenOwnerRecords.find( + (record) => + record.realm.equals(config.realm) && + record.governingTokenMint.equals(config.governingTokenMint), + ); + + if (!tokenOwnerRecord) { + throw new Error("Token owner record not found"); + } + + const proposal = await splGovernance.getProposalByPubkey(config.proposal); + if (!proposal) { + throw new Error("Proposal not found"); + } + + const voteChoice: VoteChoice = { rank: 0, weightPercentage: 100 }; + const vote = { + kind: "enum", + variant: + config.choice === 0 + ? "Approve" + : config.choice === 1 + ? "Deny" + : "Abstain", + fields: config.choice === 0 ? [[voteChoice]] : [], + } as unknown as Vote; + + const castVoteIx = await splGovernance.castVoteInstruction( + vote, + config.realm, + config.governance, + config.proposal, + tokenOwnerRecord.publicKey, // proposalOwnerTokenOwnerRecord + tokenOwnerRecord.publicKey, // voterTokenOwnerRecord + agent.wallet.publicKey, // governanceAuthority + config.governingTokenMint, + agent.wallet.publicKey, // payer + ); + + const transaction = new Transaction().add(castVoteIx); + const signature = await agent.connection.sendTransaction(transaction, [ + agent.wallet, + ]); + + return signature; +} + +export async function getRealmInfo(agent: SolanaAgentKit, realmPk: PublicKey) { + const splGovernance = new SplGovernance(agent.connection); + const realm = await splGovernance.getRealmByPubkey(realmPk); + return realm; +} + +export async function getTokenOwnerRecord( + agent: SolanaAgentKit, + realm: PublicKey, + governingTokenMint: PublicKey, + governingTokenOwner: PublicKey, +) { + const splGovernance = new SplGovernance(agent.connection); + const records = + await splGovernance.getTokenOwnerRecordsForOwner(governingTokenOwner); + return records.find( + (record) => + record.realm.equals(realm) && + record.governingTokenMint.equals(governingTokenMint), + ); +} + +export async function getVoterHistory(agent: SolanaAgentKit, voter: PublicKey) { + const splGovernance = new SplGovernance(agent.connection); + return await splGovernance.getVoteRecordsForUser(voter); +} \ No newline at end of file diff --git a/src/tools/realm-governance/index.ts b/src/tools/realm-governance/index.ts new file mode 100644 index 00000000..6ea83303 --- /dev/null +++ b/src/tools/realm-governance/index.ts @@ -0,0 +1,3 @@ +export * from "./council"; +export * from "./governance"; +export * from "./monitor"; diff --git a/src/tools/realm-governance/monitor.ts b/src/tools/realm-governance/monitor.ts new file mode 100644 index 00000000..a72415aa --- /dev/null +++ b/src/tools/realm-governance/monitor.ts @@ -0,0 +1,102 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { SplGovernance } from "governance-idl-sdk"; +import type { TokenOwnerRecord } from "governance-idl-sdk"; +import { SolanaAgentKit } from "../../agent"; + +export type MembershipChangeCallback = ( + tokenOwnerRecord: TokenOwnerRecord, + isNew: boolean, +) => void; + +export type VotingPowerChangeCallback = ( + owner: PublicKey, + oldBalance: number, + newBalance: number, +) => void; + +export class GovernanceMonitor { + private membershipSubscriptionId?: number | undefined; + private votingPowerSubscriptionId?: number | undefined; + private lastKnownBalances: Map = new Map(); + private splGovernance: SplGovernance; + + constructor( + private agent: SolanaAgentKit, + private realm: PublicKey, + private governingTokenMint: PublicKey, + ) { + this.splGovernance = new SplGovernance(agent.connection); + } + + async monitorMembershipChanges( + callback: MembershipChangeCallback, + ): Promise { + this.membershipSubscriptionId = + this.agent.connection.onProgramAccountChange( + new PublicKey("GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"), + async (accountInfo) => { + try { + const records = + await this.splGovernance.getTokenOwnerRecordsForOwner( + new PublicKey(accountInfo.accountInfo.owner), + ); + + const tokenOwnerRecord = records.find( + (record) => + record.realm.equals(this.realm) && + record.governingTokenMint.equals(this.governingTokenMint), + ); + + if (tokenOwnerRecord) { + const isNew = + accountInfo.accountInfo.lamports > 0 && + accountInfo.accountInfo.data.length === 0; + callback(tokenOwnerRecord, isNew); + } + } catch (error) { + console.error("Error processing membership change:", error); + } + }, + ); + } + + async monitorVotingPowerChanges( + callback: VotingPowerChangeCallback, + ): Promise { + this.votingPowerSubscriptionId = + this.agent.connection.onProgramAccountChange( + this.governingTokenMint, + async (accountInfo) => { + try { + const owner = new PublicKey(accountInfo.accountInfo.owner); + const tokenBalance = accountInfo.accountInfo.lamports; + const oldBalance = + this.lastKnownBalances.get(owner.toBase58()) || 0; + + if (tokenBalance !== oldBalance) { + callback(owner, oldBalance, tokenBalance); + this.lastKnownBalances.set(owner.toBase58(), tokenBalance); + } + } catch (error) { + console.error("Error processing voting power change:", error); + } + }, + ); + } + + async stopMonitoring(): Promise { + if (this.membershipSubscriptionId !== undefined) { + await this.agent.connection.removeAccountChangeListener( + this.membershipSubscriptionId, + ); + this.membershipSubscriptionId = undefined; + } + + if (this.votingPowerSubscriptionId !== undefined) { + await this.agent.connection.removeAccountChangeListener( + this.votingPowerSubscriptionId, + ); + this.votingPowerSubscriptionId = undefined; + } + } +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 9933ab96..88475a23 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,6 +2,8 @@ import { PublicKey } from "@solana/web3.js"; import { SolanaAgentKit } from "../agent"; import { z } from "zod"; import { AlloraInference, AlloraTopic } from "@alloralabs/allora-sdk"; +import { Vote, VoteChoice } from "governance-idl-sdk"; + export interface Config { OPENAI_API_KEY?: string; @@ -513,3 +515,47 @@ export interface SplAuthorityInput { updateAuthority?: PublicKey | undefined; isMutable?: boolean; } + + +export { Vote, VoteChoice }; + +export interface RealmConfig { + name: string; + councilMint?: PublicKey | undefined; + communityMint: PublicKey; + minCommunityTokensToCreateGovernance: number; + communityTokenConfig?: { + tokenType: "liquid" | "membership" | "dormant"; + maxVotingPower?: number; + }; +} + +export interface ProposalConfig { + name: string; + description: string; + governingTokenMint: PublicKey; + voteType: "single-choice" | "multiple-choice"; + options: string[]; + executionTime?: number; +} + +export interface VoteConfig { + realm: PublicKey; + choice: number; + tokenAmount?: number; + governingTokenMint: PublicKey; + tokenOwner?: PublicKey; + governance: PublicKey; + proposal: PublicKey; + tokenOwnerRecord: PublicKey; +} + +export type VoteType = { + choiceType: "single" | "multi"; + multiChoiceOptions: { + choiceType: "fullWeight" | "weighted"; + minVoterOptions: number; + maxVoterOptions: number; + maxWinningOptions: number; + } | null; +}; \ No newline at end of file