Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add SPL Governance Integration for DAO Management #205

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"dotenv": "^16.4.7",
"flash-sdk": "^2.24.3",
"form-data": "^4.0.1",
"governance-idl-sdk": "^0.0.4",
"langchain": "^0.3.8",
"openai": "^4.77.0",
"typedoc": "^0.27.6",
Expand All @@ -77,5 +78,6 @@
"prettier": "^3.4.2",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
},
"packageManager": "[email protected]+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please get rid of this change as it is unrelated to the PR and there is already a pnpm version assigned in the main branch

}
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

332 changes: 332 additions & 0 deletions src/actions/council-governance.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<string> {
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<string> {
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<string> {
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;
}
Loading