diff --git a/examples/governance-example/governance-example.ts b/examples/governance-example/governance-example.ts new file mode 100644 index 00000000..5cf573eb --- /dev/null +++ b/examples/governance-example/governance-example.ts @@ -0,0 +1,68 @@ +import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { GovernanceAgent } from '../../src/agents/governanceAgent'; + +async function main() { + const connection = new Connection('https://api.mainnet-beta.solana.com'); + const governanceProgramId = new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'); + const governanceAgent = new GovernanceAgent(connection, governanceProgramId); + + // Example: Create a proposal + const realm = new PublicKey('...'); // Your realm address + const governance = new PublicKey('...'); // Your governance address + const tokenOwnerRecord = new PublicKey('...'); // Your token owner record + const governingTokenMint = new PublicKey('...'); // Your governing token mint + const proposalSeed = new PublicKey('...'); // Your proposal seed + + // Create instructions (using TransactionInstruction directly) + const instructions: TransactionInstruction[] = [ + new TransactionInstruction({ + programId: governanceProgramId, // Use the correct programId + data: Buffer.from('...'), // Serialized instruction data + keys: [ + { pubkey: new PublicKey('...'), isSigner: false, isWritable: true }, + { pubkey: new PublicKey('...'), isSigner: true, isWritable: false } + ] + }) + ]; + + // Submit the proposal with all required parameters + await governanceAgent.submitProposal( + realm, + governance, + tokenOwnerRecord, + 'My Proposal', + 'Description of my proposal', + governingTokenMint, + proposalSeed, + instructions // Pass the instructions here + ); + + // Monitor proposal status + const proposalAddress = new PublicKey('...'); // Your proposal address + const status = await governanceAgent.monitorProposal(proposalAddress); + console.log('Proposal status:', status); + + // Execute an approved proposal + const proposalToExecute = new PublicKey('...'); // Address of approved proposal + const governanceAccount = new PublicKey('...'); // Governance account address + + const proposalInstructions: TransactionInstruction[] = [ + new TransactionInstruction({ + programId: governanceProgramId, // Use the correct programId + data: Buffer.from('...'), // Serialized instruction data + keys: [ + { pubkey: new PublicKey('...'), isSigner: false, isWritable: true }, + { pubkey: new PublicKey('...'), isSigner: true, isWritable: false } + ] + }) + ]; + + const executeIx = await governanceAgent.executeProposal( + governanceAccount, + proposalToExecute, + proposalInstructions // Pass the instructions here + ); + console.log('Proposal execution instruction created'); +} + +main().catch(console.error); diff --git a/package.json b/package.json index d62664bb..a2dac384 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,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", "@sqds/multisig": "^2.1.3", @@ -81,4 +82,4 @@ "typescript": "^5.7.2" }, "packageManager": "pnpm@9.15.3" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31aed299..c0a53647 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,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) @@ -1276,6 +1279,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'} @@ -1423,6 +1429,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==} @@ -1830,6 +1839,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==} @@ -6583,6 +6595,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(debug@4.4.0) + 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) @@ -7145,6 +7172,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 @@ -7687,6 +7718,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 diff --git a/src/agents/governanceAgent.ts b/src/agents/governanceAgent.ts new file mode 100644 index 00000000..134feee2 --- /dev/null +++ b/src/agents/governanceAgent.ts @@ -0,0 +1,93 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { SplGovernanceProgram } from "./splGovernanceProgram"; // Updated import + +export class GovernanceAgent { + private governanceProgram: SplGovernanceProgram; + + constructor(connection: Connection, governanceProgramId: PublicKey) { + this.governanceProgram = new SplGovernanceProgram( + connection, + governanceProgramId, + ); + } + + async submitProposal( + realm: PublicKey, + governance: PublicKey, + tokenOwnerRecord: PublicKey, + name: string, + description: string, + governingTokenMint: PublicKey, + proposalIndex: number, + governanceAuthority: PublicKey, // New argument + payer: PublicKey, // New argument + options: string[] = [], // Default to an empty array + useDenyOption: boolean = false, // Default to false + voterWeightRecord?: PublicKey, // Optional + ) { + const instructions: TransactionInstruction[] = []; + return await this.governanceProgram.createProposal( + realm, + governance, + tokenOwnerRecord, + name, + description, + governingTokenMint, + proposalIndex, + instructions, + governanceAuthority, + payer, + options, + useDenyOption, + voterWeightRecord, + ); + } + + async cancelProposal( + realm: PublicKey, + governance: PublicKey, + proposal: PublicKey, + tokenOwnerRecord: PublicKey, + governanceAuthority: PublicKey, // New argument + ) { + const instructions: TransactionInstruction[] = []; + return await this.governanceProgram.cancelProposal( + realm, + governance, + proposal, + tokenOwnerRecord, + governanceAuthority, + instructions, + ); + } + + async monitorProposal(proposalAddress: PublicKey) { + try { + return await this.governanceProgram.getProposalStatus(proposalAddress); + } catch (error) { + console.error("Error monitoring proposal:", error); + throw error; + } + } + + async executeProposal( + governance: PublicKey, + proposal: PublicKey, + transactionAddress: PublicKey, // New argument + transactionInstructions: TransactionInstruction[], // New argument + ) { + const instructions: TransactionInstruction[] = []; + try { + return await this.governanceProgram.executeProposal( + governance, + proposal, + instructions, + transactionAddress, + transactionInstructions, + ); + } catch (error) { + console.error("Error executing proposal:", error); + throw error; + } + } +} diff --git a/src/agents/instructionUtils.ts b/src/agents/instructionUtils.ts new file mode 100644 index 00000000..1401f76d --- /dev/null +++ b/src/agents/instructionUtils.ts @@ -0,0 +1,71 @@ +// src/agents/instructionUtils.ts + +import { + Connection, + PublicKey, + TransactionInstruction, + AccountMeta, +} from "@solana/web3.js"; +import { InstructionData } from "@solana/spl-governance"; + +// Conversion function to transform `InstructionData` into `TransactionInstruction` +export function instructionDataToTransactionInstruction( + instructionData: InstructionData, +): TransactionInstruction { + return new TransactionInstruction({ + programId: instructionData.programId, + keys: instructionData.accounts.map((account) => ({ + pubkey: account.pubkey, + isWritable: account.isWritable, + isSigner: account.isSigner, + })), + data: Buffer.from(instructionData.data), + }); +} + +// Conversion function to transform `TransactionInstruction` into `InstructionData` +export function transactionInstructionToInstructionData( + transactionInstruction: TransactionInstruction, +): InstructionData { + return new InstructionData({ + programId: transactionInstruction.programId, + accounts: transactionInstruction.keys.map((key) => ({ + pubkey: key.pubkey, + isWritable: key.isWritable, + isSigner: key.isSigner, + })), + data: new Uint8Array(transactionInstruction.data), + }); +} + +// Main Example +export async function processInstructions( + connection: Connection, + programId: PublicKey, + transactionInstructions: TransactionInstruction[], +): Promise { + // Convert TransactionInstruction[] to InstructionData[] + const instructionDataArray: InstructionData[] = transactionInstructions.map( + (instruction) => transactionInstructionToInstructionData(instruction), + ); + + // Simulate a function that requires `InstructionData[]` as input + const mockFunctionRequiringInstructionData = ( + instructions: InstructionData[], + ) => { + console.log("Processed InstructionData[]", instructions); + }; + + mockFunctionRequiringInstructionData(instructionDataArray); + + // If you need to convert back to TransactionInstruction[], use this: + const convertedBackToTransactionInstructions: TransactionInstruction[] = + instructionDataArray.map((data) => + instructionDataToTransactionInstruction(data), + ); + + console.log( + "Converted back to TransactionInstruction[]", + convertedBackToTransactionInstructions, + ); +} diff --git a/src/agents/splGovernanceProgram.ts b/src/agents/splGovernanceProgram.ts new file mode 100644 index 00000000..1bb1f2c7 --- /dev/null +++ b/src/agents/splGovernanceProgram.ts @@ -0,0 +1,62 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; + +export class SplGovernanceProgram { + private connection: Connection; + private programId: PublicKey; + + constructor(connection: Connection, programId: PublicKey) { + this.connection = connection; + this.programId = programId; + } + + async createProposal( + realm: PublicKey, + governance: PublicKey, + tokenOwnerRecord: PublicKey, + name: string, + description: string, + governingTokenMint: PublicKey, + proposalIndex: number, + instructions: TransactionInstruction[], + governanceAuthority: PublicKey, + payer: PublicKey, + options: string[], + useDenyOption: boolean, + voterWeightRecord?: PublicKey, + ) { + // Logic for creating a proposal (placeholder) + console.log("Creating proposal..."); + // Add your code for creating a proposal here + } + + async cancelProposal( + realm: PublicKey, + governance: PublicKey, + proposal: PublicKey, + tokenOwnerRecord: PublicKey, + governanceAuthority: PublicKey, + instructions: TransactionInstruction[], + ) { + // Logic for canceling a proposal (placeholder) + console.log("Canceling proposal..."); + // Add your code for canceling a proposal here + } + + async getProposalStatus(proposalAddress: PublicKey) { + // Logic for getting the proposal status (placeholder) + console.log("Getting proposal status..."); + // Add your code to get the proposal status here + } + + async executeProposal( + governance: PublicKey, + proposal: PublicKey, + instructions: TransactionInstruction[], + transactionAddress: PublicKey, + transactionInstructions: TransactionInstruction[], + ) { + // Logic for executing a proposal (placeholder) + console.log("Executing proposal..."); + // Add your code for executing a proposal here + } +} diff --git a/src/programs/splGovernance.ts b/src/programs/splGovernance.ts new file mode 100644 index 00000000..1c8293e4 --- /dev/null +++ b/src/programs/splGovernance.ts @@ -0,0 +1,69 @@ +import { + Connection, + PublicKey, + TransactionInstruction, + AccountMeta, +} from "@solana/web3.js"; +import { InstructionData } from "@solana/spl-governance"; + +// Conversion function to transform `InstructionData` into `TransactionInstruction` +export function instructionDataToTransactionInstruction( + instructionData: InstructionData, +): TransactionInstruction { + return new TransactionInstruction({ + programId: instructionData.programId, + keys: instructionData.accounts.map((account) => ({ + pubkey: account.pubkey, + isWritable: account.isWritable, + isSigner: account.isSigner, + })), + data: Buffer.from(instructionData.data), + }); +} + +// Conversion function to transform `TransactionInstruction` into `InstructionData` +export function transactionInstructionToInstructionData( + transactionInstruction: TransactionInstruction, +): InstructionData { + return new InstructionData({ + programId: transactionInstruction.programId, + accounts: transactionInstruction.keys.map((key) => ({ + pubkey: key.pubkey, + isWritable: key.isWritable, + isSigner: key.isSigner, + })), + data: new Uint8Array(transactionInstruction.data), + }); +} + +// Main Example +export async function processInstructions( + connection: Connection, + programId: PublicKey, + transactionInstructions: TransactionInstruction[], +): Promise { + // Convert TransactionInstruction[] to InstructionData[] + const instructionDataArray: InstructionData[] = transactionInstructions.map( + (instruction) => transactionInstructionToInstructionData(instruction), + ); + + // Simulate a function that requires `InstructionData[]` as input + const mockFunctionRequiringInstructionData = ( + instructions: InstructionData[], + ) => { + console.log("Processed InstructionData[]", instructions); + }; + + mockFunctionRequiringInstructionData(instructionDataArray); + + // If you need to convert back to TransactionInstruction[], use this: + const convertedBackToTransactionInstructions: TransactionInstruction[] = + instructionDataArray.map((data) => + instructionDataToTransactionInstruction(data), + ); + + console.log( + "Converted back to TransactionInstruction[]", + convertedBackToTransactionInstructions, + ); +}