@realms-today/sowell-governance
v0.2.11
Published
TypeScript SDK for sowell-gov governance program
Downloads
1,463
Readme
@sowell/governance-sdk
TypeScript SDK for the sowell-gov on-chain governance program.
Install
yarn add @sowell/governance-sdk @solana/web3.jsQuick start
import {
createProposalWithTransactionsInstruction,
getGovernanceAddress,
getProposalAddress,
setProgramId,
} from "@sowell/governance-sdk";
import { PublicKey } from "@solana/web3.js";
setProgramId(new PublicKey("YourProgramId..."));Instructions
All 18 instruction builders return a TransactionInstruction ready to send.
| # | Function | Description |
|---|----------|-------------|
| 0 | createRealmInstruction | Create a new realm (DAO) |
| 1 | depositGoverningTokensInstruction | Deposit tokens to get voting power |
| 2 | withdrawGoverningTokensInstruction | Withdraw deposited tokens |
| 3 | createGovernanceInstruction | Create a governance over an account |
| 4 | createProposalInstruction | Create a bare proposal (legacy) |
| 5 | insertTransactionInstruction | Attach a transaction to a proposal |
| 6 | castVoteInstruction | Cast a vote on a proposal |
| 7 | relinquishVoteInstruction | Withdraw a vote |
| 8 | executeTransactionInstruction | Execute an approved proposal's transaction |
| 9 | setGovernanceConfigInstruction | Update governance config (via proposal) |
| 10 | setRealmAuthorityInstruction | Transfer/remove realm authority |
| 11 | setRealmConfigInstruction | Update realm settings |
| 12 | createTokenOwnerRecordInstruction | Create a TOR without depositing |
| 13 | createNativeTreasuryInstruction | Create the native SOL treasury PDA |
| 14 | refundProposalDepositInstruction | Reclaim proposal deposit after completion |
| 15 | createProposalWithTransactionsInstruction | Atomic proposal + transactions + vote |
| 16 | setGovernanceLimitsInstruction | Set per-mint spending limits |
| 17 | closeProposalInstruction | Cancel/finalize/dispose a proposal |
Key workflows
One-shot proposal (create + vote in a single tx)
createProposalWithTransactionsInstruction is the primary instruction for fast treasury operations. It batches up to four steps in one instruction:
- Create proposal (always SingleChoice with deny option)
- Attach N transactions (each with
holdUpTime+ instructions) - Enter voting (
finalize: true) - Cast an approve vote (
vote: true, impliesfinalize)
import {
createProposalWithTransactionsInstruction,
ProposalTransactionInput,
InstructionData,
} from "@sowell/governance-sdk";
const tx: InstructionData = {
programId: SystemProgram.programId,
accounts: [
{ pubkey: treasuryPda, isSigner: true, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
],
data: transferInstructionData,
};
const ix = createProposalWithTransactionsInstruction({
programId,
realm,
governance,
proposalOwnerRecord: tokenOwnerRecord,
governingTokenMint: communityMint,
governanceAuthority: wallet.publicKey,
payer: wallet.publicKey,
name: "Transfer 5 SOL",
descriptionLink: "https://...",
proposalSeed: Keypair.generate().publicKey,
transactions: [{ holdUpTime: 0, instructions: [tx] }],
finalize: true, // Draft -> Voting
vote: true, // also cast an Approve vote (implies finalize)
// maxVotingTime: 3600, // optional: override governance default (can only extend)
// voterWeightRecord, // optional: voter weight addin
// maxVoterWeightRecord, // optional: max voter weight addin
});Tips:
vote: trueimpliesfinalize: true-- you don't need to set both- If the voter has enough weight to tip the vote (e.g. >60% under Early tipping), the proposal immediately transitions to Succeeded within the same instruction
holdUpTime: 0means the transaction is executable immediately after the vote passes (after 1 slot)proposalSeedmust be unique per proposal -- useKeypair.generate().publicKey- Max ~4 SOL-transfer-sized transactions fit in a single instruction due to Solana's 1232-byte tx limit
Execute a succeeded proposal
import {
executeTransactionInstruction,
getProposalTransactionAddress,
getGovernanceLimitsAddress,
} from "@sowell/governance-sdk";
const txPda = getProposalTransactionAddress(programId, proposal, 0, 0);
const ix = executeTransactionInstruction({
programId,
governance,
proposal,
proposalTransaction: txPda,
instructionAccounts: [
// accounts required by the inner CPI, in order
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: treasuryPda, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
],
// Pass this to enforce spending limits (required when governance has limits set)
governanceLimits: getGovernanceLimitsAddress(programId, governance),
});Close / cancel a proposal
closeProposalInstruction is a unified instruction that handles cancellation, finalization, and account disposal (reclaims rent).
const ix = closeProposalInstruction({
programId,
realm,
governance,
proposal,
proposalOwnerRecord: tokenOwnerRecord,
governingTokenMint: communityMint,
authority: wallet.publicKey,
beneficiary: wallet.publicKey, // receives reclaimed rent
proposalTransactions: [txPda1, txPda2], // must pass ALL transaction PDAs
});Who can close:
| Proposal state | Who | Outcome | |---|---|---| | Completed, Cancelled, Defeated, Vetoed | Anyone | Dispose (reclaim rent) | | Draft, SigningOff | Owner/delegate | Cancel + dispose | | Voting (active) | Owner/delegate | Cancel + dispose | | Voting (expired) | Anyone | Finalize vote; dispose if Defeated/Vetoed | | Succeeded, Executing, ExecutingWithErrors | No one | Must execute txns or expire |
Spending limits
Set per-mint caps on how much a governance can spend per ExecuteTransaction:
import {
setGovernanceLimitsInstruction,
MintSpendingLimit,
NATIVE_SOL_MINT,
SpendingLimitTypeKind,
} from "@sowell/governance-sdk";
const limits: MintSpendingLimit[] = [
{ mint: usdcMint, limit: { kind: SpendingLimitTypeKind.Absolute, amount: 1_000_000_000n } },
{ mint: NATIVE_SOL_MINT, limit: { kind: SpendingLimitTypeKind.Bps, bps: 500 } }, // 5% of balance
];
const ix = setGovernanceLimitsInstruction(
programId,
governance,
realm,
realmAuthority, // or governance PDA (via proposal)
payer,
limits,
);Tips:
NATIVE_SOL_MINT(PublicKey.default) is the sentinel for native SOL limits- BPS values are basis points:
10000= 100%,500= 5% - BPS limits are per-execution against the current balance (not the original)
- Pass an empty
limitsarray to clear all limits and unset thehas_spending_limitsflag - Once limits are set,
executeTransactionInstructionmust include thegovernanceLimitsPDA
Account decoders
Deserialize on-chain account data into typed objects. Pass the raw Buffer from connection.getAccountInfo().
import {
decodeRealm,
decodeGovernance,
decodeProposal,
decodeTokenOwnerRecord,
decodeVoteRecord,
decodeProposalTransaction,
decodeRealmConfig,
decodeProposalDeposit,
decodeGovernanceLimits,
} from "@sowell/governance-sdk";
const info = await connection.getAccountInfo(proposalAddress);
const proposal = decodeProposal(info.data);
console.log(proposal.state); // ProposalState enum
console.log(proposal.name);
console.log(proposal.options); // ProposalOption[]Account fetchers
Fetch and decode governance accounts in one call:
import {
fetchRealm,
fetchGovernance,
fetchProposal,
fetchTokenOwnerRecord,
fetchVoteRecord,
fetchAccount,
fetchAccounts,
AccountNotFoundError,
} from "@sowell/governance-sdk";
const realm = await fetchRealm(connection, realmAddress);
const proposal = await fetchProposal(connection, proposalAddress, {
throwIfMissing: false, // returns null instead of throwing
});
try {
const governance = await fetchGovernance(connection, governanceAddress);
console.log(governance.sowellian.betEnabled);
} catch (e) {
if (e instanceof AccountNotFoundError) {
console.log("Governance account does not exist yet");
}
}Available typed fetchers:
fetchRealmfetchGovernancefetchProposalfetchProposalTransactionfetchTokenOwnerRecordfetchVoteRecordfetchRealmConfigfetchProposalDepositfetchGovernanceLimitsfetchSowellianConfigfetchBetfetchSecondaryMarketfetchReceipt
Governance query APIs (Oyster-style)
For indexer and UI-style queries, use the get* APIs:
import {
getRealms,
getAllGovernances,
getProposalsByGovernance,
getGovernanceAccounts,
pubkeyFilter,
decodeProposal,
GovernanceAccountType,
} from "@sowell/governance-sdk";
const realms = await getRealms(connection, programId);
const governances = await getAllGovernances(connection, programId, realmPk);
const proposals = await getProposalsByGovernance(connection, programId, governancePk);
const custom = await getGovernanceAccounts(
connection,
programId,
decodeProposal,
GovernanceAccountType.Proposal,
[pubkeyFilter(1, governancePk)!]
);Included helpers:
getGovernanceAccountsgetGovernanceAccounttryGetGovernanceAccountgetRealm,getRealmsgetRealmConfig,tryGetRealmConfiggetVoteRecord,getVoteRecordsByVotergetTokenOwnerRecord,getTokenOwnerRecordForRealm,getTokenOwnerRecordsByOwner,getAllTokenOwnerRecordsgetGovernance,getAllGovernancesgetProposal,getProposalsByGovernance,getAllProposalsgetProposalDepositsByDepositPayerpubkeyFilter
PDA derivation
All 14 PDA helpers follow the pattern get*Address(programId, ...seeds): PublicKey.
import {
getRealmAddress,
getGovernanceAddress,
getTokenOwnerRecordAddress,
getProposalAddress,
getProposalTransactionAddress,
getVoteRecordAddress,
getNativeTreasuryAddress,
getGovernanceLimitsAddress,
getGoverningTokenHoldingAddress,
getRealmConfigAddress,
getProposalDepositAddress,
getProgramGovernanceAddress,
getMintGovernanceAddress,
getTokenGovernanceAddress,
} from "@sowell/governance-sdk";
const realm = getRealmAddress(programId, "My DAO");
const governance = getGovernanceAddress(programId, realm, governedAccount);
const treasury = getNativeTreasuryAddress(programId, governance);
const limits = getGovernanceLimitsAddress(programId, governance);
const proposal = getProposalAddress(programId, governance, mint, seed);
const txPda = getProposalTransactionAddress(programId, proposal, 0, 0);Instruction decoder (for indexers)
Parse raw instruction data into typed discriminated unions:
import { decodeGovernanceInstruction, getInstructionName } from "@sowell/governance-sdk";
const decoded = decodeGovernanceInstruction(instructionData);
if (decoded && decoded.instruction === 15) {
// CreateProposalWithTransactions
console.log(decoded.name, decoded.finalize, decoded.vote);
}
getInstructionName(6); // "CastVote"Error handling
Parse custom program errors from transaction failures:
import {
parseGovernanceError,
isGovernanceError,
getGovernanceErrorName,
getGovernanceErrorMessage,
} from "@sowell/governance-sdk";
// From a failed transaction's error code:
const err = parseGovernanceError(604);
// { code: 604, name: "SpendingLimitExceeded", message: "Treasury spending limit breached" }
isGovernanceError(604); // true
getGovernanceErrorName(604); // "SpendingLimitExceeded"
getGovernanceErrorMessage(604); // "Treasury spending limit breached"Error codes range from 500-606. Key ones:
| Code | Name | When |
|---|---|---|
| 500 | InvalidInstruction | Unrecognized instruction discriminant |
| 503 | InvalidGoverningTokenMint | Wrong mint for the governance |
| 509 | InvalidProposalState | Operation not valid for current proposal state |
| 548 | VoteWeightSourceNotAllowed | Voter weight addin check failed |
| 602 | MathematicalOverflow | Arithmetic overflow |
| 603 | InvalidMaxVotingTime | maxVotingTime below votingBaseTime |
| 604 | SpendingLimitExceeded | Transaction exceeds treasury spending limit |
| 605 | InvalidGovernanceLimitsAccount | Wrong or invalid GovernanceLimits PDA |
| 606 | InvalidCloseProposalState | Invalid state for CloseProposal |
Enums
All state enums are exported for matching against decoded account data:
import {
ProposalState,
VoteTipping,
VoteThresholdType,
VoteKind,
SpendingLimitTypeKind,
GovernanceAccountType,
TransactionExecutionStatus,
GovernanceInstruction,
} from "@sowell/governance-sdk";| Enum | Values |
|---|---|
| ProposalState | Draft, SigningOff, Voting, Succeeded, Executing, Completed, Cancelled, Defeated, ExecutingWithErrors, Vetoed |
| VoteTipping | Strict, Early, Disabled |
| VoteThresholdType | YesVotePercentage, Disabled |
| VoteKind | Approve, Deny, Veto |
| SpendingLimitTypeKind | Absolute, Bps |
| GovernanceInstruction | 0-17, maps to all 18 instructions |
Testing
yarn test # run all tests
yarn test:watch # watch mode
yarn build # compile TypeScriptTests use LiteSVM for fast in-process Solana program testing.
License
Apache-2.0
