diff --git a/.gitignore b/.gitignore index 3debef07..e2cf4556 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,9 @@ ByaDoc1TAiSkp3gGkRDR4bVSqbe55W37mjwYbTy38Tq3.json HkAGc7akgEH7HxkHWNk3htZHMid7fmG2eU6SxyGf9PKa.json .env -pm-testnet.json \ No newline at end of file +pm-testnet.json + +node_modules +.next + +ax.json \ No newline at end of file diff --git a/contract/Anchor.toml b/contract/Anchor.toml index 4d245f33..a488cd6a 100644 --- a/contract/Anchor.toml +++ b/contract/Anchor.toml @@ -12,7 +12,7 @@ govcontract = "AXnkQnEEMBsKcJ1gSXP1aW6tZMGWodzEaoB6b3bRib2r" govcontract = "GoVpHPV3EY89hwKJjfw19jTdgMsGKG4UFSE2SfJqTuhc" [programs.testnet] -govcontract = "GoVpHPV3EY89hwKJjfw19jTdgMsGKG4UFSE2SfJqTuhc" +govcontract = "6MX2RaV2vfTGv6c7zCmRAod2E6MdAgR6be2Vb3NsMxPW" [registry] url = "https://api.apr.dev" diff --git a/contract/Cargo.lock b/contract/Cargo.lock index 33a615f7..92b504ee 100644 --- a/contract/Cargo.lock +++ b/contract/Cargo.lock @@ -601,7 +601,7 @@ dependencies = [ [[package]] name = "gov-v1" version = "0.1.0" -source = "git+https://github.com/exo-tech-xyz/gov-v1?branch=signer-check#7c7db845821bd2bfaba12dc604e1b4977ba1bb4e" +source = "git+https://github.com/dhruvsol/gov-v1-testnet?branch=signer-check#3f92046b48c173313f9eb80d409161fac57494f7" dependencies = [ "anchor-lang", ] diff --git a/contract/programs/govcontract/Cargo.toml b/contract/programs/govcontract/Cargo.toml index 765b5c06..591a82c4 100644 --- a/contract/programs/govcontract/Cargo.toml +++ b/contract/programs/govcontract/Cargo.toml @@ -17,5 +17,5 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { version = "0.31.1", features = ["init-if-needed"]} -gov-v1 = { git = "https://github.com/exo-tech-xyz/gov-v1", branch = "signer-check",features = ["cpi"] } +gov-v1 = { git = "https://github.com/dhruvsol/gov-v1-testnet", branch = "signer-check",features = ["cpi"] } diff --git a/contract/programs/govcontract/src/constants.rs b/contract/programs/govcontract/src/constants.rs index 3273427e..c394bc84 100644 --- a/contract/programs/govcontract/src/constants.rs +++ b/contract/programs/govcontract/src/constants.rs @@ -10,9 +10,9 @@ pub const BASIS_POINTS_MAX: u64 = 10_000; // Anchor discriminator size pub const ANCHOR_DISCRIMINATOR: usize = 8; -pub const MIN_PROPOSAL_STAKE_LAMPORTS: u64 = 100_000 * 1_000_000_000; +pub const MIN_PROPOSAL_STAKE_LAMPORTS: u64 = 1000 * 1_000_000_000; -pub const CLUSTER_SUPPORT_MULTIPLIER: u128 = 100; +pub const CLUSTER_SUPPORT_MULTIPLIER: u128 = 1000; pub const CLUSTER_STAKE_MULTIPLIER: u128 = 5; diff --git a/contract/programs/govcontract/src/events.rs b/contract/programs/govcontract/src/events.rs index e1f890a9..73385d46 100644 --- a/contract/programs/govcontract/src/events.rs +++ b/contract/programs/govcontract/src/events.rs @@ -105,3 +105,16 @@ pub struct MerkleRootFlushed { pub new_snapshot_slot: u64, pub flush_timestamp: i64, } + +#[event] +pub struct ProposalTimingAdjusted { + pub proposal_id: Pubkey, + pub author: Pubkey, + pub new_creation_timestamp: i64, + pub new_creation_epoch: u64, + pub new_start_epoch: u64, + pub new_end_epoch: u64, + pub new_snapshot_slot: u64, + pub new_consensus_result: Option, + pub adjustment_timestamp: i64, +} diff --git a/contract/programs/govcontract/src/instructions/adjust_proposal_timing.rs b/contract/programs/govcontract/src/instructions/adjust_proposal_timing.rs new file mode 100644 index 00000000..613edb3e --- /dev/null +++ b/contract/programs/govcontract/src/instructions/adjust_proposal_timing.rs @@ -0,0 +1,62 @@ +use anchor_lang::prelude::*; + +use crate::{error::GovernanceError, events::ProposalTimingAdjusted, state::Proposal}; + +#[derive(Accounts)] +pub struct AdjustProposalTiming<'info> { + #[account(mut)] + pub signer: Signer<'info>, // Proposal author + #[account( + mut, + constraint = proposal.author == signer.key() @ GovernanceError::Unauthorized, + constraint = !proposal.finalized @ GovernanceError::ProposalFinalized, + )] + pub proposal: Account<'info, Proposal>, +} + +impl<'info> AdjustProposalTiming<'info> { + pub fn adjust_timing( + &mut self, + creation_timestamp: Option, + creation_epoch: Option, + start_epoch: Option, + end_epoch: Option, + snapshot_slot: Option, + consensus_result: Option>, + ) -> Result<()> { + let clock = Clock::get()?; + + // Update fields if provided + if let Some(ts) = creation_timestamp { + self.proposal.creation_timestamp = ts; + } + if let Some(epoch) = creation_epoch { + self.proposal.creation_epoch = epoch; + } + if let Some(epoch) = start_epoch { + self.proposal.start_epoch = epoch; + } + if let Some(epoch) = end_epoch { + self.proposal.end_epoch = epoch; + } + if let Some(slot) = snapshot_slot { + self.proposal.snapshot_slot = slot; + } + if let Some(consensus_result_value) = consensus_result { + self.proposal.consensus_result = consensus_result_value; + } + emit!(ProposalTimingAdjusted { + proposal_id: self.proposal.key(), + author: self.signer.key(), + new_creation_timestamp: self.proposal.creation_timestamp, + new_creation_epoch: self.proposal.creation_epoch, + new_start_epoch: self.proposal.start_epoch, + new_end_epoch: self.proposal.end_epoch, + new_snapshot_slot: self.proposal.snapshot_slot, + new_consensus_result: self.proposal.consensus_result, + adjustment_timestamp: clock.unix_timestamp, + }); + + Ok(()) + } +} diff --git a/contract/programs/govcontract/src/instructions/flush_merkle_root.rs b/contract/programs/govcontract/src/instructions/flush_merkle_root.rs index 666367b9..d78a1190 100644 --- a/contract/programs/govcontract/src/instructions/flush_merkle_root.rs +++ b/contract/programs/govcontract/src/instructions/flush_merkle_root.rs @@ -35,7 +35,14 @@ impl<'info> FlushMerkleRoot<'info> { let clock = Clock::get()?; // Clear the consensus_result - self.proposal.consensus_result = None; + require!( + self.proposal.snapshot_slot > 0, + GovernanceError::InvalidSnapshotSlot + ); + require!( + self.proposal.consensus_result.is_some(), + GovernanceError::ConsensusResultNotSet + ); // Recalculate snapshot_slot based on current epoch // Using the same logic as in support_proposal @@ -66,6 +73,7 @@ impl<'info> FlushMerkleRoot<'info> { b"proposal".as_ref(), &proposal_seed_val, vote_account_key.as_ref(), + &[self.proposal.proposal_bump], ]; let signer = &[&seeds[..]]; // Initialize the ballot box via CPI diff --git a/contract/programs/govcontract/src/instructions/mod.rs b/contract/programs/govcontract/src/instructions/mod.rs index 1c90c1d5..d6608705 100644 --- a/contract/programs/govcontract/src/instructions/mod.rs +++ b/contract/programs/govcontract/src/instructions/mod.rs @@ -1,3 +1,4 @@ +pub mod adjust_proposal_timing; pub mod cast_vote; pub mod cast_vote_override; pub mod create_proposal; @@ -8,6 +9,7 @@ pub mod modify_vote; pub mod modify_vote_override; pub mod support_proposal; +pub use adjust_proposal_timing::*; pub use cast_vote::*; pub use cast_vote_override::*; pub use create_proposal::*; diff --git a/contract/programs/govcontract/src/instructions/support_proposal.rs b/contract/programs/govcontract/src/instructions/support_proposal.rs index fe818597..860c83e2 100644 --- a/contract/programs/govcontract/src/instructions/support_proposal.rs +++ b/contract/programs/govcontract/src/instructions/support_proposal.rs @@ -2,9 +2,13 @@ use anchor_lang::{ prelude::*, solana_program::{ epoch_stake::{get_epoch_stake_for_vote_account, get_epoch_total_stake}, + instruction::Instruction, + program::invoke_signed, vote::{program as vote_program, state::VoteState}, }, + ToAccountMetas, }; +use borsh::BorshSerialize; use crate::{ constants::*, @@ -36,6 +40,7 @@ pub struct SupportProposal<'info> { pub spl_vote_account: UncheckedAccount<'info>, /// CHECK: Ballot box account - may or may not exist, checked with data_is_empty() + #[account(mut)] pub ballot_box: UncheckedAccount<'info>, /// CHECK: Ballot program account @@ -44,9 +49,25 @@ pub struct SupportProposal<'info> { )] pub ballot_program: UncheckedAccount<'info>, + /// CHECK: Program config account + #[account( + seeds = [b"ProgramConfig"], + bump, + seeds::program = ballot_program.key(), + constraint = program_config.owner == &gov_v1::ID @ ProgramError::InvalidAccountOwner, + )] + pub program_config: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, } +pub struct InitBallotBox<'info> { + pub payer: AccountInfo<'info>, + pub proposal: AccountInfo<'info>, + pub ballot_box: AccountInfo<'info>, + pub program_config: AccountInfo<'info>, + pub system_program: AccountInfo<'info>, +} impl<'info> SupportProposal<'info> { pub fn support_proposal(&mut self, bumps: &SupportProposalBumps) -> Result<()> { let clock = Clock::get()?; @@ -103,30 +124,31 @@ impl<'info> SupportProposal<'info> { // Create seed components with sufficient lifetime let proposal_seed_val = self.proposal.proposal_seed.to_le_bytes(); let vote_account_key = self.proposal.vote_account_pubkey.key(); + let seeds: &[&[u8]] = &[ b"proposal".as_ref(), &proposal_seed_val, vote_account_key.as_ref(), + &[self.proposal.proposal_bump], ]; - let signer = &[&seeds[..]]; - // Initialize the consensus result + let signer_seeds = &[&seeds[..]]; + let cpi_ctx = CpiContext::new_with_signer( self.ballot_program.to_account_info(), gov_v1::cpi::accounts::InitBallotBox { payer: self.signer.to_account_info(), proposal: self.proposal.to_account_info(), ballot_box: self.ballot_box.to_account_info(), - program_config: self.ballot_program.to_account_info(), + program_config: self.program_config.to_account_info(), system_program: self.system_program.to_account_info(), }, - signer, + signer_seeds, ); - gov_v1::cpi::init_ballot_box( cpi_ctx, snapshot_slot, - self.proposal.proposal_seed, // we are not storing this - self.spl_vote_account.key(), + self.proposal.proposal_seed, + self.proposal.vote_account_pubkey, )?; } diff --git a/contract/programs/govcontract/src/lib.rs b/contract/programs/govcontract/src/lib.rs index 00ed2501..49260b10 100644 --- a/contract/programs/govcontract/src/lib.rs +++ b/contract/programs/govcontract/src/lib.rs @@ -11,7 +11,7 @@ use instructions::*; use gov_v1::StakeMerkleLeaf; -declare_id!("GoVpHPV3EY89hwKJjfw19jTdgMsGKG4UFSE2SfJqTuhc"); +declare_id!("6MX2RaV2vfTGv6c7zCmRAod2E6MdAgR6be2Vb3NsMxPW"); #[program] pub mod govcontract { @@ -108,4 +108,24 @@ pub mod govcontract { ctx.accounts.flush_merkle_root()?; Ok(()) } + + pub fn adjust_proposal_timing( + ctx: Context, + creation_timestamp: Option, + creation_epoch: Option, + start_epoch: Option, + end_epoch: Option, + snapshot_slot: Option, + consensus_result: Option>, + ) -> Result<()> { + ctx.accounts.adjust_timing( + creation_timestamp, + creation_epoch, + start_epoch, + end_epoch, + snapshot_slot, + consensus_result, + )?; + Ok(()) + } } diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..3e598f2a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,12 @@ +.next +node_modules +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.nextra +.nextra-cache +.nextra-cache-dev +.nextra-cache-prod +.nextra-cache-test \ No newline at end of file diff --git a/docs/bun.lockb b/docs/bun.lockb new file mode 100755 index 00000000..19b0c532 Binary files /dev/null and b/docs/bun.lockb differ diff --git a/docs/next.config.mjs b/docs/next.config.mjs new file mode 100644 index 00000000..45810330 --- /dev/null +++ b/docs/next.config.mjs @@ -0,0 +1,11 @@ +import nextra from 'nextra'; + +// Set up Nextra with its configuration +const withNextra = nextra({ + search: { codeblocks: false }, +}); + +// Export the final Next.js config with Nextra included +export default withNextra({ + // ... Add regular Next.js options here +}); diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..d6e4f6e8 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,21 @@ +{ + "name": "svmgov-docs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next --turbopack", + "build": "next build", + "start": "next start", + "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind" + }, + "dependencies": { + "next": "^16.0.2", + "nextra": "^4.6.0", + "nextra-theme-docs": "^4.6.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "pagefind": "^1.4.0" + } +} \ No newline at end of file diff --git a/docs/src/app/[[...mdxPath]]/page.jsx b/docs/src/app/[[...mdxPath]]/page.jsx new file mode 100644 index 00000000..371ee7a5 --- /dev/null +++ b/docs/src/app/[[...mdxPath]]/page.jsx @@ -0,0 +1,27 @@ +import { generateStaticParamsFor, importPage } from 'nextra/pages'; +import { useMDXComponents as getMDXComponents } from '../../mdx-components'; + +export const generateStaticParams = generateStaticParamsFor('mdxPath'); + +export async function generateMetadata(props) { + const params = await props.params; + const { metadata } = await importPage(params.mdxPath); + return metadata; +} + +const Wrapper = getMDXComponents().wrapper; + +export default async function Page(props) { + const params = await props.params; + const { + default: MDXContent, + toc, + metadata, + sourceCode, + } = await importPage(params.mdxPath); + return ( + + + + ); +} diff --git a/docs/src/app/layout.jsx b/docs/src/app/layout.jsx new file mode 100644 index 00000000..f7d8b74d --- /dev/null +++ b/docs/src/app/layout.jsx @@ -0,0 +1,62 @@ +import { Footer, Layout, Navbar } from 'nextra-theme-docs'; +import { Banner, Head } from 'nextra/components'; +import { getPageMap } from 'nextra/page-map'; +import 'nextra-theme-docs/style.css'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +export const metadata = {}; + +// Get version from env or package.json +function getVersion() { + if (process.env.NEXT_PUBLIC_DOCS_VERSION) { + return process.env.NEXT_PUBLIC_DOCS_VERSION; + } + try { + const packageJsonPath = join(process.cwd(), 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + return `v${packageJson.version || '0.1.0'}`; + } catch { + return 'v0.1.0'; + } +} + +const version = getVersion(); + +const banner = ( + svmgov Governance CLI +); +const navbar = ( + svmgov Governance CLI} + rightContent={ + + {version} + + } + /> +); +const footer = ( +
+ {new Date().getFullYear()} © svmgov. Documentation version {version} +
+); + +export default async function RootLayout({ children }) { + return ( + + + + + {children} + + + + ); +} diff --git a/docs/src/content/_meta.js b/docs/src/content/_meta.js new file mode 100644 index 00000000..ae587ede --- /dev/null +++ b/docs/src/content/_meta.js @@ -0,0 +1,18 @@ +export default { + index: { + display: 'hidden', + }, + validators: { + title: 'For Validators', + type: 'page', + }, + stakers: { + title: 'For Stakers', + type: 'page', + }, + reference: { + title: 'Reference', + type: 'page', + }, +}; + diff --git a/docs/src/content/index.mdx b/docs/src/content/index.mdx new file mode 100644 index 00000000..e8f14b5c --- /dev/null +++ b/docs/src/content/index.mdx @@ -0,0 +1,134 @@ +# svmgov Governance CLI + +Welcome to the svmgov Governance CLI documentation. svmgov is a command-line interface tool designed for Solana validators and delegators to interact with the Solana Validator Governance program. + +## What is svmgov? + +svmgov enables validators and delegators to: + +- **Create governance proposals** - Validators can propose new governance rules +- **Support proposals** - Validators can support proposals to help them reach the cluster support threshold +- **Cast votes** - Validators can vote on active proposals +- **Override votes** - Delegators can override their validator's vote using stake account verification +- **View proposals and votes** - List and inspect governance proposals and votes + +## Installation + +Install svmgov using Cargo: + +```bash +cargo install --path svmgov +``` + +Or build from source: + +```bash +git clone https://github.com/3uild-3thos/govcontract.git +cd govcontract/svmgov +cargo build --release +``` + +## Configuration + +svmgov supports environment variables for global options: + +- `SVMGOV_KEY` - Path to identity keypair (equivalent to `--identity-keypair`) +- `SVMGOV_RPC` - RPC URL (equivalent to `--rpc-url`) + +Flags override environment variables if both are provided. + +## Documentation Structure + +This documentation is organized by user type: + +- **[For Validators](/validators)** - Commands for validators to create proposals, support proposals, and cast votes +- **[For Stakers](/stakers)** - Commands for delegators to override validator votes +- **[Reference](/reference)** - Detailed reference for arguments and configuration + +## Quick Start (recommended) + +svmgov reads a global config stored at `~/.svmgov/config.toml`. Configure it once, then run commands without repeating flags. + +1) Initialize configuration (interactive): + +```bash +svmgov init +``` + +This will ask: +- Are you a Validator or Staker? +- Path to your keypair (validator identity or staker keypair) +- Default network (mainnet/testnet) + +It creates/updates `~/.svmgov/config.toml` with fields like: + +```toml +# ~/.svmgov/config.toml +user_type = "Validator" # or "Staker" +identity_keypair_path = "/path/to/validator.json" +staker_keypair_path = "/path/to/staker.json" +network = "mainnet" +# Optional overrides +rpc_url = "https://api.mainnet-beta.solana.com" +operator_api_url = "https://operator.example.com" +``` + +2) Adjust any value later using the config command: + +```bash +# Show entire config +svmgov config show + +# Set values +svmgov config set network mainnet +svmgov config set rpc-url https://api.mainnet-beta.solana.com +svmgov config set operator-api-url https://operator.example.com + +# Keypair depends on user type selected during init +svmgov config set identity-keypair /path/to/validator.json # for validators +svmgov config set staker-keypair /path/to/staker.json # for stakers +``` + +3) Run commands (no repeated flags needed): + +```bash +# List proposals using defaults from ~/.svmgov/config.toml +svmgov list-proposals + +# Create a proposal (validators) +svmgov create-proposal \ + --title "Update Fee Structure" \ + --description "https://github.com/repo/issue/123" \ + --network mainnet + +# Cast a vote (validators) +svmgov cast-vote \ + --proposal-id \ + --for-votes 7000 --against-votes 2000 --abstain-votes 1000 \ + --network mainnet + +# Override a vote (stakers) +svmgov cast-vote-override \ + --proposal-id \ + --for-votes 6000 --against-votes 3000 --abstain-votes 1000 \ + --stake-account \ + --network mainnet +``` + +Notes: +- The CLI merges sources in this order: explicit flags > config file > built-in defaults. +- `svmgov config set` supports keys: `network`, `rpc-url`, `operator-api-url`, `identity-keypair`, `staker-keypair`. + +## Getting Help + +For command-specific help, use: + +```bash +svmgov --help +``` + +For example: +```bash +svmgov create-proposal --help +``` + diff --git a/docs/src/content/reference/_meta.js b/docs/src/content/reference/_meta.js new file mode 100644 index 00000000..7feb556f --- /dev/null +++ b/docs/src/content/reference/_meta.js @@ -0,0 +1,6 @@ +export default { + arguments: { + title: 'Arguments Reference', + }, +}; + diff --git a/docs/src/content/reference/arguments.mdx b/docs/src/content/reference/arguments.mdx new file mode 100644 index 00000000..c11bcbc5 --- /dev/null +++ b/docs/src/content/reference/arguments.mdx @@ -0,0 +1,115 @@ +# Arguments Reference + +Complete reference for all command-line arguments used in svmgov. + +## Global Arguments + +These arguments are available to all commands: + +| Name | Short | Type | Required | Default | Description | +|------|-------|------|----------|---------|-------------| +| `--identity-keypair` | `-i` | String | No | `SVMGOV_KEY` env var | Path to the identity keypair JSON file | +| `--rpc-url` | `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL for Solana network | + +### Environment Variables + +You can set these environment variables instead of using flags: + +- `SVMGOV_KEY` - Equivalent to `--identity-keypair` +- `SVMGOV_RPC` - Equivalent to `--rpc-url` + +Flags override environment variables if both are provided. + +## Common Argument Types + +### Basis Points + +Basis points (bp) represent percentages where: +- **10,000 bp = 100%** +- **7,000 bp = 70%** +- **1,000 bp = 10%** + +When voting, basis points must sum to exactly 10,000 (100% of stake). + +### Proposal ID + +A proposal ID is a Program Derived Address (PDA) represented as a base58-encoded string. You can find proposal IDs using the `list-proposals` command. + +### Network + +Network identifier for fetching merkle proofs. Common values: +- `mainnet` - Solana mainnet +- `testnet` - Solana testnet +- `devnet` - Solana devnet + +### Keypair Paths + +Paths to JSON keypair files. These files contain the private key needed to sign transactions. + +## Validator Commands Arguments + +### Create Proposal + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--seed` | u64 | No | Random | Unique seed for proposal PDA derivation | +| `--title` | String | Yes | - | Proposal title (max 50 chars) | +| `--description` | String | Yes | - | GitHub link (must start with `https://github.com`, max 250 chars) | +| `--network` | String | Yes | - | Network for merkle proofs | + +### Support Proposal + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | Proposal ID (PDA) | +| `--network` | String | Yes | - | Network for merkle proofs | + +### Cast Vote / Modify Vote + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | Proposal ID (PDA) | +| `--for-votes` | u64 | Yes | - | Basis points for 'For' (0-10000) | +| `--against-votes` | u64 | Yes | - | Basis points for 'Against' (0-10000) | +| `--abstain-votes` | u64 | Yes | - | Basis points for 'Abstain' (0-10000) | +| `--network` | String | Yes | - | Network for merkle proofs | + +### List Proposals + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--status` | String | No | - | Filter by status (e.g., `active`) | +| `--limit` | usize | No | `0` | Limit results (0 = no limit) | +| `--json` | bool | No | `false` | Output in JSON format | + +### List Votes + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | Proposal ID (PDA) | +| `--verbose` | bool | No | `false` | Show detailed vote information | +| `--limit` | usize | No | `0` | Limit results (0 = no limit) | +| `--json` | bool | No | `false` | Output in JSON format | + +## Staker Commands Arguments + +### Cast Vote Override / Modify Vote Override + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | Proposal ID (PDA) | +| `--for-votes` | u64 | Yes | - | Basis points for 'For' (0-10000) | +| `--against-votes` | u64 | Yes | - | Basis points for 'Against' (0-10000) | +| `--abstain-votes` | u64 | Yes | - | Basis points for 'Abstain' (0-10000) | +| `--stake-account` | String | Yes | - | Stake account pubkey (base58) | +| `--network` | String | Yes | - | Network for merkle proofs | +| `--staker-keypair` | String | Yes | - | Staker keypair path for signing | +| `--vote-account` | String | Yes | - | Validator vote account pubkey (base58) | +| `--operator-api` | String | No | Env var | Operator API endpoint for snapshot data | + +## Related Smart Contract References + +- [Proposal State](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/state/proposal.rs) +- [Vote State](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/state/vote.rs) +- [Support State](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/state/support.rs) + diff --git a/docs/src/content/stakers/_meta.js b/docs/src/content/stakers/_meta.js new file mode 100644 index 00000000..9d72eaf4 --- /dev/null +++ b/docs/src/content/stakers/_meta.js @@ -0,0 +1,9 @@ +export default { + 'cast-vote-override': { + title: 'Cast Vote Override', + }, + 'modify-vote-override': { + title: 'Modify Vote Override', + }, +}; + diff --git a/docs/src/content/stakers/cast-vote-override/index.mdx b/docs/src/content/stakers/cast-vote-override/index.mdx new file mode 100644 index 00000000..269ca607 --- /dev/null +++ b/docs/src/content/stakers/cast-vote-override/index.mdx @@ -0,0 +1,80 @@ +# Cast Vote Override + +Override a validator's vote as a delegator using stake account verification. + +## Description + +This command allows a delegator to override their validator's vote on a proposal. The CLI fetches snapshot data from the operator API and submits the override vote. You can either let the CLI auto-select the first stake account from your voter summary, or explicitly specify a stake account. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) | +| `--for-votes` | u64 | Yes | - | Basis points for 'For' vote (0-10000) | +| `--against-votes` | u64 | Yes | - | Basis points for 'Against' vote (0-10000) | +| `--abstain-votes` | u64 | Yes | - | Basis points for 'Abstain' vote (0-10000) | +| `--stake-account` | String | Yes | - | Stake account pubkey (base58) to use for override | +| `--network` | String | Yes | - | Network for fetching merkle proofs (e.g., `mainnet`, `testnet`) | +| `--staker-keypair` | String | Yes | - | Staker keypair for signing the transaction | +| `--vote-account` | String | Yes | - | Vote account pubkey (base58) for the validator | +| `--operator-api` | String | No | Env var | Operator API endpoint for snapshot data | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|----------|---------|-------------| +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: With `~/.svmgov/config.toml`, the RPC URL and `operator-api-url` can be set globally. Only pass `--operator-api` to override the configured default. + +## Requirements + +- `for_votes + against_votes + abstain_votes` must equal **10,000** (100%) +- Stake account must be owned by the signer +- Stake account must be delegated to the specified validator +- Validator must have voted on the proposal + +## Examples + +```bash +# With config (recommended) +svmgov cast-vote-override \ + --proposal-id "ABC123..." \ + --for-votes 6000 \ + --against-votes 3000 \ + --abstain-votes 1000 \ + --stake-account "StakeAccountPubkey..." \ + --network mainnet \ + --staker-keypair /path/to/staker_key.json \ + --vote-account "VoteAccountPubkey..." + +# Override operator API (explicit override) +svmgov cast-vote-override \ + --proposal-id "ABC123..." \ + --for-votes 7000 \ + --against-votes 2000 \ + --abstain-votes 1000 \ + --stake-account "StakeAccountPubkey..." \ + --network mainnet \ + --staker-keypair /path/to/staker_key.json \ + --vote-account "VoteAccountPubkey..." \ + --operator-api https://api.example.com + +# Without config (explicit RPC) +svmgov cast-vote-override \ + --proposal-id "ABC123..." \ + --for-votes 6000 \ + --against-votes 3000 \ + --abstain-votes 1000 \ + --stake-account "StakeAccountPubkey..." \ + --network mainnet \ + --staker-keypair /path/to/staker_key.json \ + --vote-account "VoteAccountPubkey..." \ + --rpc-url https://api.mainnet-beta.solana.com +``` + +## Related Smart Contract + +See [cast_vote_override.rs](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/instructions/cast_vote_override.rs) in the smart contract. + diff --git a/docs/src/content/stakers/index.mdx b/docs/src/content/stakers/index.mdx new file mode 100644 index 00000000..28d1343e --- /dev/null +++ b/docs/src/content/stakers/index.mdx @@ -0,0 +1,37 @@ +# For Stakers + +Commands available to delegators (stakers) for overriding validator votes. + +## Available Commands + +- **[Cast Vote Override](/stakers/cast-vote-override)** - Override a validator's vote as a delegator +- **[Modify Vote Override](/stakers/modify-vote-override)** - Modify an existing vote override + +## Overview + +Delegators can override their validator's vote on governance proposals using stake account verification. This allows delegators to express their own voting preferences independently of their validator. + +## Requirements + +- Stake account owned by the delegator +- Stake account must be delegated to a validator +- Active stake in the account +- Access to operator API for snapshot data (or use default) + +## How Vote Overrides Work + +When a delegator casts a vote override: +1. The CLI fetches snapshot data from the operator API +2. Verifies the delegator owns the stake account +3. Submits the override vote to the governance program +4. The override vote takes precedence over the validator's vote for that specific stake account + +## Global Arguments + +All commands support these global arguments: + +- `--identity-keypair ` (or `-i`) - Path to identity keypair JSON file +- `--rpc-url ` (or `-r`) - Custom RPC URL + +These can also be set via environment variables `SVMGOV_KEY` and `SVMGOV_RPC`. + diff --git a/docs/src/content/stakers/modify-vote-override/index.mdx b/docs/src/content/stakers/modify-vote-override/index.mdx new file mode 100644 index 00000000..4d76dcb4 --- /dev/null +++ b/docs/src/content/stakers/modify-vote-override/index.mdx @@ -0,0 +1,61 @@ +# Modify Vote Override + +Modify an existing vote override on a proposal. + +## Description + +This command allows a delegator to modify their existing vote override on a proposal. The vote allocation can be changed as long as the proposal is still in the voting phase. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) | +| `--for-votes` | u64 | Yes | - | Basis points for 'For' vote (0-10000) | +| `--against-votes` | u64 | Yes | - | Basis points for 'Against' vote (0-10000) | +| `--abstain-votes` | u64 | Yes | - | Basis points for 'Abstain' vote (0-10000) | +| `--stake-account` | String | Yes | - | Stake account pubkey (base58) to use for override | +| `--network` | String | Yes | - | Network for fetching merkle proofs (e.g., `mainnet`, `testnet`) | +| `--staker-keypair` | String | Yes | - | Staker keypair for signing the transaction | +| `--vote-account` | String | Yes | - | Vote account pubkey (base58) for the validator | +| `--operator-api` | String | No | Env var | Operator API endpoint for snapshot data | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|----------|---------|-------------| +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: With `~/.svmgov/config.toml`, the RPC URL and `operator-api-url` can be set globally. + +## Example + +```bash +# With config (recommended) — Change to 80% For, 10% Against, 10% Abstain +svmgov modify-vote-override \ + --proposal-id "ABC123..." \ + --for-votes 8000 \ + --against-votes 1000 \ + --abstain-votes 1000 \ + --stake-account "StakeAccountPubkey..." \ + --network mainnet \ + --staker-keypair /path/to/staker_key.json \ + --vote-account "VoteAccountPubkey..." + +# Without config (explicit RPC) +svmgov modify-vote-override \ + --proposal-id "ABC123..." \ + --for-votes 8000 \ + --against-votes 1000 \ + --abstain-votes 1000 \ + --stake-account "StakeAccountPubkey..." \ + --network mainnet \ + --staker-keypair /path/to/staker_key.json \ + --vote-account "VoteAccountPubkey..." \ + --rpc-url https://api.mainnet-beta.solana.com +``` + +## Related Smart Contract + +See [modify_vote_override.rs](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/instructions/modify_vote_override.rs) in the smart contract. + diff --git a/docs/src/content/validators/_meta.js b/docs/src/content/validators/_meta.js new file mode 100644 index 00000000..1f5ceee3 --- /dev/null +++ b/docs/src/content/validators/_meta.js @@ -0,0 +1,30 @@ +export default { + 'init-index': { + title: 'Initialize Index', + }, + 'create-proposal': { + title: 'Create Proposal', + }, + 'support-proposal': { + title: 'Support Proposal', + }, + 'cast-vote': { + title: 'Cast Vote', + }, + 'modify-vote': { + title: 'Modify Vote', + }, + 'finalize-proposal': { + title: 'Finalize Proposal', + }, + 'get-proposal': { + title: 'Get Proposal', + }, + 'list-proposals': { + title: 'List Proposals', + }, + 'list-votes': { + title: 'List Votes', + }, +}; + diff --git a/docs/src/content/validators/cast-vote/index.mdx b/docs/src/content/validators/cast-vote/index.mdx new file mode 100644 index 00000000..126c14ab --- /dev/null +++ b/docs/src/content/validators/cast-vote/index.mdx @@ -0,0 +1,67 @@ +# Cast Vote + +Cast a vote on an active governance proposal. + +## Description + +This command allows a validator to cast a vote on an active proposal. Votes are allocated using basis points (bp), where 10,000 basis points equals 100% of the validator's stake. The allocation must be split across three options: For, Against, and Abstain. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) | +| `--for-votes` | u64 | Yes | - | Basis points for 'For' vote (0-10000) | +| `--against-votes` | u64 | Yes | - | Basis points for 'Against' vote (0-10000) | +| `--abstain-votes` | u64 | Yes | - | Basis points for 'Abstain' vote (0-10000) | +| `--network` | String | Yes | - | Network for fetching merkle proofs (e.g., `mainnet`, `testnet`) | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--identity-keypair`, `-i` | String | No | `SVMGOV_KEY` env var | Path to identity keypair JSON file | +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: With `~/.svmgov/config.toml`, identity and RPC URL are resolved automatically. Pass `--network` only to override the configured default. + +## Requirements + +- Proposal must be in voting phase (activated after reaching 5% cluster support) +- `for_votes + against_votes + abstain_votes` must equal **10,000** (100%) +- Validator must not have already voted on this proposal + +## Examples + +```bash +# With config (recommended) — 70% For, 20% Against, 10% Abstain +svmgov cast-vote \ + --proposal-id "ABC123..." \ + --for-votes 7000 \ + --against-votes 2000 \ + --abstain-votes 1000 \ + --network mainnet + +# Without config (explicit flags) — 60% For, 30% Against, 10% Abstain +svmgov cast-vote \ + --proposal-id "ABC123..." \ + --for-votes 6000 \ + --against-votes 3000 \ + --abstain-votes 1000 \ + --network mainnet \ + --identity-keypair /path/to/key.json +``` + +## Vote Allocation + +Basis points represent percentages: +- 10,000 bp = 100% of stake +- 7,000 bp = 70% of stake +- 1,000 bp = 10% of stake + +Example: A validator with 100 SOL staked allocating 7,000 bp to "For" means 70 SOL (70%) supports the proposal. + +## Related Smart Contract + +See [cast_vote.rs](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/instructions/cast_vote.rs) in the smart contract. + diff --git a/docs/src/content/validators/create-proposal/index.mdx b/docs/src/content/validators/create-proposal/index.mdx new file mode 100644 index 00000000..b4b76038 --- /dev/null +++ b/docs/src/content/validators/create-proposal/index.mdx @@ -0,0 +1,52 @@ +# Create Proposal + +Create a new governance proposal with a title and GitHub description link. + +## Description + +This command creates a new governance proposal. The validator must have at least 100,000 SOL staked to create a proposal. The proposal will be assigned a unique PDA (Program Derived Address) based on the provided seed (or a random seed if not specified). + +## Arguments + +| Name | Type | Required | Default | Description | +| --------------- | ------ | -------- | ------- | --------------------------------------------------------------------------------------------------- | +| `--seed` | u64 | No | Random | Unique seed for the proposal (used to derive the PDA) | +| `--title` | String | Yes | - | Proposal title (max 50 characters) | +| `--description` | String | Yes | - | GitHub link for the proposal description (must start with `https://github.com`, max 250 characters) | +| `--network` | String | Yes | - | Network for fetching merkle proofs (e.g., `mainnet`, `testnet`) | + +## Global Arguments + +| Name | Type | Required | Default | Description | +| -------------------------- | ------ | -------- | -------------------- | ---------------------------------- | +| `--identity-keypair`, `-i` | String | No | `SVMGOV_KEY` env var | Path to identity keypair JSON file | +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +## Requirements + +- Validator must have at least **100,000 SOL** staked +- Identity keypair must match the validator's identity +- Description must be a valid GitHub URL + +## Examples + +```bash +# Create proposal with auto-generated seed +svmgov create-proposal \ + --title "Update Fee Structure" \ + --description "https://github.com/repo/issue/123" \ + --network mainnet \ + --identity-keypair /path/to/key.json + +# Create proposal with specific seed +svmgov create-proposal \ + --seed 42 \ + --title "New Governance Rule" \ + --description "https://github.com/repo/proposal" \ + --network mainnet \ + --identity-keypair /path/to/key.json +``` + +## Related Smart Contract + +See [create_proposal.rs](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/instructions/create_proposal.rs) in the smart contract. diff --git a/docs/src/content/validators/finalize-proposal/index.mdx b/docs/src/content/validators/finalize-proposal/index.mdx new file mode 100644 index 00000000..1700568a --- /dev/null +++ b/docs/src/content/validators/finalize-proposal/index.mdx @@ -0,0 +1,41 @@ +# Finalize Proposal + +Finalize a proposal after its voting period has ended. + +## Description + +This command finalizes a governance proposal after its voting period has ended. Once finalized, the proposal's results are locked and cannot be changed. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) to finalize | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|----------|---------|-------------| +| `--identity-keypair`, `-i` | String | No | `SVMGOV_KEY` env var | Path to identity keypair JSON file | +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: With `~/.svmgov/config.toml`, identity and RPC URL are resolved automatically. + +## Example + +```bash +# With config (recommended) +svmgov finalize-proposal \ + --proposal-id "ABC123..." + +# Without config (explicit flags) +svmgov finalize-proposal \ + --proposal-id "ABC123..." \ + --identity-keypair /path/to/key.json \ + --rpc-url https://api.mainnet-beta.solana.com +``` + +## Related Smart Contract + +See [finalize_proposal.rs](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/instructions/finalize_proposal.rs) in the smart contract. + diff --git a/docs/src/content/validators/get-proposal/index.mdx b/docs/src/content/validators/get-proposal/index.mdx new file mode 100644 index 00000000..37ecf0be --- /dev/null +++ b/docs/src/content/validators/get-proposal/index.mdx @@ -0,0 +1,38 @@ +# Get Proposal + +Display a specific governance proposal and its details. + +## Description + +This command retrieves and displays detailed information about a specific governance proposal, including its status, votes, and metadata. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) to display | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|----------|---------|-------------| +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +## Example + +```bash +svmgov get-proposal \ + --proposal-id "ABC123..." \ + --rpc-url https://api.mainnet-beta.solana.com +``` + +## Output + +The command displays: +- Proposal title and description +- Author and creation timestamp +- Voting status (active/inactive) +- Current vote counts (For, Against, Abstain) +- Cluster support percentage +- Snapshot slot and consensus result + diff --git a/docs/src/content/validators/index.mdx b/docs/src/content/validators/index.mdx new file mode 100644 index 00000000..b6d27947 --- /dev/null +++ b/docs/src/content/validators/index.mdx @@ -0,0 +1,31 @@ +# For Validators + +Commands available to validators for creating and managing governance proposals. + +## Available Commands + +- **[Initialize Index](/validators/init-index)** - Initialize the proposal index PDA (one-time setup) +- **[Create Proposal](/validators/create-proposal)** - Create a new governance proposal +- **[Support Proposal](/validators/support-proposal)** - Support an existing proposal +- **[Cast Vote](/validators/cast-vote)** - Cast a vote on an active proposal +- **[Modify Vote](/validators/modify-vote)** - Modify an existing vote +- **[Finalize Proposal](/validators/finalize-proposal)** - Finalize a proposal after voting ends +- **[Get Proposal](/validators/get-proposal)** - Display a specific proposal's details +- **[List Proposals](/validators/list-proposals)** - List all governance proposals +- **[List Votes](/validators/list-votes)** - List votes for a specific proposal + +## Requirements + +- Validator identity keypair with sufficient stake +- For creating proposals: minimum 100,000 SOL staked +- Access to Solana RPC endpoint + +## Global Arguments + +All commands support these global arguments: + +- `--identity-keypair ` (or `-i`) - Path to identity keypair JSON file +- `--rpc-url ` (or `-r`) - Custom RPC URL + +These can also be set via environment variables `SVMGOV_KEY` and `SVMGOV_RPC`. + diff --git a/docs/src/content/validators/init-index/index.mdx b/docs/src/content/validators/init-index/index.mdx new file mode 100644 index 00000000..197801cf --- /dev/null +++ b/docs/src/content/validators/init-index/index.mdx @@ -0,0 +1,37 @@ +# Initialize Index + +Initialize the proposal index PDA. This is a one-time setup that must be run before creating any proposals. + +## Description + +The proposal index PDA tracks all proposals created in the governance program. This command initializes it and must be executed once before any proposals can be created. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| None | - | - | - | This command takes no arguments | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--identity-keypair`, `-i` | String | No | `SVMGOV_KEY` env var | Path to identity keypair JSON file | +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: When `~/.svmgov/config.toml` is set (via `svmgov init`), you typically don't need to pass `--identity-keypair` or `--rpc-url`. + +## Example + +```bash +# With config (recommended) +svmgov init-index + +# Without config (explicit flags) +svmgov --identity-keypair /path/to/key.json --rpc-url https://api.mainnet-beta.solana.com init-index +``` + +## Related + +- [Create Proposal](/validators/create-proposal) - Create a new proposal (requires index to be initialized) + diff --git a/docs/src/content/validators/list-proposals/index.mdx b/docs/src/content/validators/list-proposals/index.mdx new file mode 100644 index 00000000..fffd5e8e --- /dev/null +++ b/docs/src/content/validators/list-proposals/index.mdx @@ -0,0 +1,44 @@ +# List Proposals + +List all governance proposals, optionally filtered by status. + +## Description + +This command retrieves and displays a list of all governance proposals from the Solana Validator Governance program. You can filter by status and limit the number of results. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--status` | String | No | - | Filter by proposal status (e.g., `active`) | +| `--limit` | usize | No | `0` | Limit the number of proposals listed (0 = no limit) | +| `--json` | bool | No | `false` | Output in JSON format | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|----------|---------|-------------| +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: With `~/.svmgov/config.toml`, the RPC URL is resolved automatically. + +## Examples + +```bash +# With config (recommended) +svmgov list-proposals + +# Filter to only active proposals +svmgov list-proposals --status active + +# List first 5 proposals in JSON format +svmgov list-proposals --limit 5 --json true + +# Without config (explicit RPC) +svmgov list-proposals --rpc-url https://api.mainnet-beta.solana.com +``` + +## Output Format + +By default, proposals are displayed in a human-readable format. Use `--json true` for machine-readable JSON output. + diff --git a/docs/src/content/validators/list-votes/index.mdx b/docs/src/content/validators/list-votes/index.mdx new file mode 100644 index 00000000..3dfc5bb4 --- /dev/null +++ b/docs/src/content/validators/list-votes/index.mdx @@ -0,0 +1,47 @@ +# List Votes + +List all votes for a specific proposal. + +## Description + +This command retrieves and displays all votes cast on a specified governance proposal. You can view votes in summary or verbose format. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) to get votes for | +| `--verbose` | bool | No | `false` | List votes with detailed information | +| `--limit` | usize | No | `0` | Limit the number of votes listed (0 = no limit) | +| `--json` | bool | No | `false` | Output in JSON format | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|----------|---------|-------------| +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: With `~/.svmgov/config.toml`, the RPC URL is resolved automatically. + +## Examples + +```bash +# List votes for a proposal +svmgov list-votes --proposal-id "ABC123..." + +# List votes with detailed information +svmgov list-votes --proposal-id "ABC123..." --verbose true + +# List first 10 votes in JSON format +svmgov list-votes --proposal-id "ABC123..." --limit 10 --json true + +# Without config (explicit RPC) +svmgov list-votes --proposal-id "ABC123..." --rpc-url https://api.mainnet-beta.solana.com +``` + +## Output + +- **Default**: Shows vote account addresses +- **Verbose**: Shows detailed vote information including basis points allocation and timestamps +- **JSON**: Machine-readable format with all vote data + diff --git a/docs/src/content/validators/modify-vote/index.mdx b/docs/src/content/validators/modify-vote/index.mdx new file mode 100644 index 00000000..e5cc68ab --- /dev/null +++ b/docs/src/content/validators/modify-vote/index.mdx @@ -0,0 +1,52 @@ +# Modify Vote + +Modify an existing vote on an active governance proposal. + +## Description + +This command allows a validator to update their existing vote on a proposal. The vote allocation can be changed as long as the proposal is still in the voting phase. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) | +| `--for-votes` | u64 | Yes | - | Basis points for 'For' vote (0-10000) | +| `--against-votes` | u64 | Yes | - | Basis points for 'Against' vote (0-10000) | +| `--abstain-votes` | u64 | Yes | - | Basis points for 'Abstain' vote (0-10000) | +| `--network` | String | Yes | - | Network for fetching merkle proofs (e.g., `mainnet`, `testnet`) | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|----------|---------|-------------| +| `--identity-keypair`, `-i` | String | No | `SVMGOV_KEY` env var | Path to identity keypair JSON file | +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: With `~/.svmgov/config.toml`, identity and RPC URL are resolved automatically. Pass `--network` only to override the configured default. + +## Example + +```bash +# With config (recommended) +svmgov modify-vote \ + --proposal-id "ABC123..." \ + --for-votes 8000 \ + --against-votes 1000 \ + --abstain-votes 1000 \ + --network mainnet + +# Without config (explicit flags) +svmgov modify-vote \ + --proposal-id "ABC123..." \ + --for-votes 8000 \ + --against-votes 1000 \ + --abstain-votes 1000 \ + --network mainnet \ + --identity-keypair /path/to/key.json +``` + +## Related Smart Contract + +See [modify_vote.rs](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/instructions/modify_vote.rs) in the smart contract. + diff --git a/docs/src/content/validators/support-proposal/index.mdx b/docs/src/content/validators/support-proposal/index.mdx new file mode 100644 index 00000000..77b490ec --- /dev/null +++ b/docs/src/content/validators/support-proposal/index.mdx @@ -0,0 +1,50 @@ +# Support Proposal + +Support an existing proposal to help it reach the cluster support threshold. + +## Description + +This command allows a validator to support a governance proposal. Each validator's support contributes to the proposal's cluster support. Voting activates when the proposal reaches **500 basis points (5%)** of total cluster support. + +## Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--proposal-id` | String | Yes | - | The proposal's ID (PDA) to support | +| `--network` | String | Yes | - | Network for fetching merkle proofs (e.g., `mainnet`, `testnet`) | + +## Global Arguments + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--identity-keypair`, `-i` | String | No | `SVMGOV_KEY` env var | Path to identity keypair JSON file | +| `--rpc-url`, `-r` | String | No | `SVMGOV_RPC` env var | Custom RPC URL | + +> Using config: When `~/.svmgov/config.toml` is set (via `svmgov init`), identity keypair and RPC URL are read from config. Only pass `--network` if you want to override the configured default. + +## Example + +```bash +# With config (recommended) +svmgov support-proposal \ + --proposal-id "ABC123..." \ + --network mainnet + +# Without config (explicit flags) +svmgov support-proposal \ + --proposal-id "ABC123..." \ + --network mainnet \ + --identity-keypair /path/to/key.json \ + --rpc-url https://api.mainnet-beta.solana.com +``` + +## Notes + +- Each validator's support contributes their stake weight to the proposal's cluster support +- Once a proposal reaches 5% cluster support, voting is automatically activated +- The proposal's `snapshot_slot` and `consensus_result` are set during the first support + +## Related Smart Contract + +See [support_proposal.rs](https://github.com/3uild-3thos/govcontract/blob/main/contract/programs/govcontract/src/instructions/support_proposal.rs) in the smart contract. + diff --git a/docs/src/mdx-components.js b/docs/src/mdx-components.js new file mode 100644 index 00000000..0fcbd9cb --- /dev/null +++ b/docs/src/mdx-components.js @@ -0,0 +1,46 @@ +import { useMDXComponents as getDocsMDXComponents } from 'nextra-theme-docs'; + +const docsComponents = getDocsMDXComponents(); + +export const useMDXComponents = (components) => ({ + ...docsComponents, + ...components, +}); + +// RepoLink component removed - use regular markdown links instead +// Example: [link text](https://github.com/3uild-3thos/govcontract/blob/main/path/to/file.rs) + +// ArgTable component for displaying command arguments +export const ArgTable = ({ children }) => { + return ( +
+ + + + + + + + + + + + {children} + +
NameTypeRequiredDefaultDescription
+
+ ); +}; + +// ArgRow component for table rows +export const ArgRow = ({ name, type, required, defaultValue, description }) => { + return ( + + {name} + {type} + {required ? 'Yes' : 'No'} + {defaultValue || '-'} + {description} + + ); +}; diff --git a/svmgov/Cargo.lock b/svmgov/Cargo.lock index 0237d5bb..02c1bf07 100644 --- a/svmgov/Cargo.lock +++ b/svmgov/Cargo.lock @@ -1013,7 +1013,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -1026,7 +1026,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.0", "windows-sys 0.60.2", ] @@ -1134,6 +1134,31 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.3" @@ -1326,6 +1351,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1366,6 +1412,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "eager" version = "0.1.0" @@ -1695,6 +1747,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1764,7 +1834,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gov-v1" version = "0.1.0" -source = "git+https://github.com/dhruvsol/gov-v1-testnet.git#ecda1ac1a864aced51ae967af307110de229a328" +source = "git+https://github.com/dhruvsol/gov-v1-testnet?branch=signer-check#3f92046b48c173313f9eb80d409161fac57494f7" dependencies = [ "anchor-lang", ] @@ -2309,7 +2379,7 @@ dependencies = [ "console 0.15.11", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", "web-time", ] @@ -2321,7 +2391,7 @@ checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console 0.16.0", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", "unit-prefix", "web-time", ] @@ -2335,6 +2405,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.9.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2482,6 +2569,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + [[package]] name = "libsecp256k1" version = "0.6.0" @@ -2625,6 +2722,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.3" @@ -2653,6 +2762,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nix" version = "0.29.0" @@ -2902,6 +3020,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking" version = "2.2.1" @@ -3036,7 +3160,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -3289,6 +3413,17 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.11.1" @@ -3716,6 +3851,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3812,6 +3956,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -6356,14 +6511,17 @@ dependencies = [ "anchor-client", "anchor-lang", "anyhow", + "base64 0.22.1", "bs58", "chrono", "clap", + "dirs", "dotenv", "env_logger 0.11.8", "gov-v1", "hex", "indicatif 0.18.0", + "inquire", "log", "rand 0.9.1", "reqwest 0.12.23", @@ -6372,6 +6530,7 @@ dependencies = [ "tempfile", "textwrap", "tokio", + "toml 0.8.23", ] [[package]] @@ -6515,7 +6674,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -6558,6 +6717,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -6623,7 +6791,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -6721,23 +6889,47 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -6860,6 +7052,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.0" @@ -7512,9 +7710,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] diff --git a/svmgov/Cargo.toml b/svmgov/Cargo.toml index 663e67b7..468c6507 100644 --- a/svmgov/Cargo.toml +++ b/svmgov/Cargo.toml @@ -26,7 +26,11 @@ indicatif = "0.18.0" reqwest = { version = "0.12.23", features = ["json"] } bs58 = "0.5.1" hex = "0.4.3" -gov-v1 = { git = "https://github.com/dhruvsol/gov-v1-testnet.git" } +gov-v1 = {git = "https://github.com/dhruvsol/gov-v1-testnet", branch = "signer-check" } dotenv = "0.15.0" +toml = "0.8" +dirs = "5.0" +inquire = "0.7" +base64 = "0.22.1" [dev-dependencies] tempfile = "3.0" \ No newline at end of file diff --git a/svmgov/idls/govcontract.json b/svmgov/idls/govcontract.json index e806f0e7..c6b96ddd 100644 --- a/svmgov/idls/govcontract.json +++ b/svmgov/idls/govcontract.json @@ -1,5 +1,5 @@ { - "address": "DUWGu3sMy4ymJWwUs53eaCyNzZRFoxhmi3Ggf1kh8Q61", + "address": "6MX2RaV2vfTGv6c7zCmRAod2E6MdAgR6be2Vb3NsMxPW", "metadata": { "name": "govcontract", "version": "0.1.0", @@ -8,16 +8,16 @@ }, "instructions": [ { - "name": "add_merkle_root", + "name": "adjust_proposal_timing", "discriminator": [ - 235, - 31, - 120, - 49, - 53, - 9, - 197, - 147 + 82, + 66, + 219, + 217, + 123, + 150, + 223, + 224 ], "accounts": [ { @@ -32,12 +32,41 @@ ], "args": [ { - "name": "merkle_root_hash", + "name": "creation_timestamp", "type": { - "array": [ - "u8", - 32 - ] + "option": "i64" + } + }, + { + "name": "creation_epoch", + "type": { + "option": "u64" + } + }, + { + "name": "start_epoch", + "type": { + "option": "u64" + } + }, + { + "name": "end_epoch", + "type": { + "option": "u64" + } + }, + { + "name": "snapshot_slot", + "type": { + "option": "u64" + } + }, + { + "name": "consensus_result", + "type": { + "option": { + "option": "pubkey" + } } } ] @@ -408,15 +437,6 @@ { "name": "spl_vote_account" }, - { - "name": "snapshot_program" - }, - { - "name": "consensus_result" - }, - { - "name": "meta_merkle_proof" - }, { "name": "system_program", "address": "11111111111111111111111111111111" @@ -449,6 +469,30 @@ 187, 164 ], + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "proposal", + "writable": true + } + ], + "args": [] + }, + { + "name": "flush_merkle_root", + "discriminator": [ + 10, + 71, + 17, + 246, + 162, + 57, + 144, + 87 + ], "accounts": [ { "name": "signer", @@ -458,6 +502,19 @@ { "name": "proposal", "writable": true + }, + { + "name": "spl_vote_account" + }, + { + "name": "ballot_box" + }, + { + "name": "ballot_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" } ], "args": [] @@ -520,7 +577,6 @@ "accounts": [ { "name": "signer", - "writable": true, "signer": true }, { @@ -599,7 +655,6 @@ "accounts": [ { "name": "signer", - "writable": true, "signer": true }, { @@ -817,13 +872,40 @@ "name": "spl_vote_account" }, { - "name": "snapshot_program" + "name": "ballot_box", + "writable": true }, { - "name": "consensus_result" + "name": "ballot_program" }, { - "name": "meta_merkle_proof" + "name": "program_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 80, + 114, + 111, + 103, + 114, + 97, + 109, + 67, + 111, + 110, + 102, + 105, + 103 + ] + } + ], + "program": { + "kind": "account", + "path": "ballot_program" + } + } }, { "name": "system_program", @@ -898,20 +980,33 @@ 176, 188 ] + }, + { + "name": "VoteOverrideCache", + "discriminator": [ + 195, + 82, + 50, + 219, + 140, + 34, + 108, + 57 + ] } ], "events": [ { - "name": "MerkleRootAdded", + "name": "MerkleRootFlushed", "discriminator": [ - 171, - 59, - 45, - 200, - 89, - 55, - 150, - 244 + 120, + 37, + 53, + 216, + 119, + 172, + 17, + 144 ] }, { @@ -953,6 +1048,19 @@ 231 ] }, + { + "name": "ProposalTimingAdjusted", + "discriminator": [ + 151, + 58, + 224, + 49, + 142, + 133, + 182, + 107 + ] + }, { "name": "VoteCast", "discriminator": [ @@ -1204,13 +1312,18 @@ }, { "code": 6039, - "name": "InvalidEpochRange", - "msg": "Invalid epoch range: end_epoch must be greater than start_epoch" + "name": "ConsensusResultNotSet", + "msg": "Consensus result has not been set for this proposal" + }, + { + "code": 6040, + "name": "Unauthorized", + "msg": "Unauthorized: caller is not authorized to perform this action" } ], "types": [ { - "name": "MerkleRootAdded", + "name": "MerkleRootFlushed", "type": { "kind": "struct", "fields": [ @@ -1223,13 +1336,12 @@ "type": "pubkey" }, { - "name": "merkle_root_hash", - "type": { - "array": [ - "u8", - 32 - ] - } + "name": "new_snapshot_slot", + "type": "u64" + }, + { + "name": "flush_timestamp", + "type": "i64" } ] } @@ -1320,17 +1432,9 @@ "type": "u32" }, { - "name": "merkle_root_hash", - "docs": [ - "Merkle root hash representing the snapshot of validator stakes at proposal creation" - ], + "name": "consensus_result", "type": { - "option": { - "array": [ - "u8", - 32 - ] - } + "option": "pubkey" } }, { @@ -1339,6 +1443,14 @@ "Slot number when the validator stake snapshot was taken" ], "type": "u64" + }, + { + "name": "proposal_seed", + "type": "u64" + }, + { + "name": "vote_account_pubkey", + "type": "pubkey" } ] } @@ -1455,6 +1567,56 @@ { "name": "voting_activated", "type": "bool" + }, + { + "name": "snapshot_slot", + "type": "u64" + } + ] + } + }, + { + "name": "ProposalTimingAdjusted", + "type": { + "kind": "struct", + "fields": [ + { + "name": "proposal_id", + "type": "pubkey" + }, + { + "name": "author", + "type": "pubkey" + }, + { + "name": "new_creation_timestamp", + "type": "i64" + }, + { + "name": "new_creation_epoch", + "type": "u64" + }, + { + "name": "new_start_epoch", + "type": "u64" + }, + { + "name": "new_end_epoch", + "type": "u64" + }, + { + "name": "new_snapshot_slot", + "type": "u64" + }, + { + "name": "new_consensus_result", + "type": { + "option": "pubkey" + } + }, + { + "name": "adjustment_timestamp", + "type": "i64" } ] } @@ -1677,6 +1839,10 @@ "type": { "kind": "struct", "fields": [ + { + "name": "delegator", + "type": "pubkey" + }, { "name": "stake_account", "type": "pubkey" @@ -1732,6 +1898,58 @@ ] } }, + { + "name": "VoteOverrideCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator", + "type": "pubkey" + }, + { + "name": "proposal", + "type": "pubkey" + }, + { + "name": "vote_account_validator", + "type": "pubkey" + }, + { + "name": "for_votes_bp", + "type": "u64" + }, + { + "name": "against_votes_bp", + "type": "u64" + }, + { + "name": "abstain_votes_bp", + "type": "u64" + }, + { + "name": "for_votes_lamports", + "type": "u64" + }, + { + "name": "against_votes_lamports", + "type": "u64" + }, + { + "name": "abstain_votes_lamports", + "type": "u64" + }, + { + "name": "total_stake", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, { "name": "VoteOverrideCast", "type": { diff --git a/svmgov/install.sh b/svmgov/install.sh new file mode 100755 index 00000000..72318d73 --- /dev/null +++ b/svmgov/install.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +echo "Installing svmgov CLI..." + +# Check if cargo is installed +if ! command -v cargo &> /dev/null; then + echo -e "${RED}Error: cargo is not installed. Please install Rust first.${NC}" + echo "Visit https://rustup.rs/ to install Rust." + exit 1 +fi + +# Build release binary +echo -e "${YELLOW}Building release binary...${NC}" +if ! cargo build --release; then + echo -e "${RED}Error: Failed to build binary${NC}" + exit 1 +fi + +BINARY_PATH="$SCRIPT_DIR/target/release/svmgov" + +if [ ! -f "$BINARY_PATH" ]; then + echo -e "${RED}Error: Binary not found at $BINARY_PATH${NC}" + exit 1 +fi + +# Determine installation directory +if [ -w "/usr/local/bin" ]; then + INSTALL_DIR="/usr/local/bin" + USE_SUDO=false +else + INSTALL_DIR="$HOME/.local/bin" + USE_SUDO=false +fi + +# Create install directory if it doesn't exist +if [ ! -d "$INSTALL_DIR" ]; then + mkdir -p "$INSTALL_DIR" + echo "Created directory: $INSTALL_DIR" +fi + +# Copy binary +echo -e "${YELLOW}Installing binary to $INSTALL_DIR/svmgov...${NC}" +cp "$BINARY_PATH" "$INSTALL_DIR/svmgov" +chmod +x "$INSTALL_DIR/svmgov" + +# Detect shell +SHELL_NAME=$(basename "$SHELL" 2>/dev/null || echo "bash") +CONFIG_FILE="" + +case "$SHELL_NAME" in + bash) + if [ -f "$HOME/.bashrc" ]; then + CONFIG_FILE="$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + CONFIG_FILE="$HOME/.bash_profile" + else + CONFIG_FILE="$HOME/.bashrc" + fi + EXPORT_LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" + ;; + zsh) + CONFIG_FILE="$HOME/.zshrc" + EXPORT_LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" + ;; + fish) + CONFIG_FILE="$HOME/.config/fish/config.fish" + EXPORT_LINE="set -gx PATH \"$INSTALL_DIR\" \$PATH" + ;; + *) + echo -e "${YELLOW}Warning: Unknown shell '$SHELL_NAME'. Skipping PATH configuration.${NC}" + echo "Please manually add $INSTALL_DIR to your PATH." + CONFIG_FILE="" + ;; +esac + +# Add to PATH if config file exists and entry doesn't already exist +if [ -n "$CONFIG_FILE" ]; then + if [ "$INSTALL_DIR" = "$HOME/.local/bin" ]; then + # Check if PATH entry already exists + if grep -q "$INSTALL_DIR" "$CONFIG_FILE" 2>/dev/null; then + echo -e "${GREEN}PATH entry already exists in $CONFIG_FILE${NC}" + else + echo "" >> "$CONFIG_FILE" + echo "# svmgov CLI" >> "$CONFIG_FILE" + echo "$EXPORT_LINE" >> "$CONFIG_FILE" + echo -e "${GREEN}Added $INSTALL_DIR to PATH in $CONFIG_FILE${NC}" + fi + else + # For /usr/local/bin, it's usually already in PATH, but we'll check + echo -e "${GREEN}Binary installed to $INSTALL_DIR (usually already in PATH)${NC}" + fi +fi + +echo "" +echo -e "${GREEN}✓ Installation complete!${NC}" +echo "" +echo "Next steps:" +echo " 1. Reload your shell configuration:" +if [ "$SHELL_NAME" = "bash" ]; then + echo " source $CONFIG_FILE" +elif [ "$SHELL_NAME" = "zsh" ]; then + echo " source $CONFIG_FILE" +elif [ "$SHELL_NAME" = "fish" ]; then + echo " (or restart your terminal)" +fi +echo "" +echo " 2. Verify installation:" +echo " svmgov --version" +echo "" +echo " 3. Initialize configuration (optional):" +echo " svmgov init" +echo "" + +# Check if --skip-init flag is passed +SKIP_INIT=false +for arg in "$@"; do + if [ "$arg" = "--skip-init" ]; then + SKIP_INIT=true + break + fi +done + +# Run init if not skipped and binary is accessible +if [ "$SKIP_INIT" = false ]; then + if command -v svmgov &> /dev/null || [ -f "$INSTALL_DIR/svmgov" ]; then + echo -e "${YELLOW}Would you like to run 'svmgov init' now? (y/n)${NC}" + read -r response + if [[ "$response" =~ ^[Yy]$ ]]; then + "$INSTALL_DIR/svmgov" init || echo -e "${YELLOW}Note: You can run 'svmgov init' later${NC}" + fi + fi +fi + diff --git a/svmgov/readme.md b/svmgov/readme.md deleted file mode 100644 index 81612acb..00000000 --- a/svmgov/readme.md +++ /dev/null @@ -1,400 +0,0 @@ -# Solana Validator Governance CLI (`svmgov`) - -`svmgov` is a command-line interface (CLI) tool designed for Solana validators and delegators to interact with the Solana Validator Governance program. It enables validators to create governance proposals, support proposals, cast votes, and manage vote overrides with merkle proof verification. Delegators can override their validator's votes using stake account verification. The CLI integrates with external APIs for real-time validator stake data and supports comprehensive governance operations. - ---- - -## Requirements - -[![Rust](https://img.shields.io/badge/Rust-1.85%2B-black?logo=rust)](https://www.rust-lang.org/) - -- **Rust**: 1.85.0 or higher (stable version only—no nightly required; edition 2024 is fully supported on stable Rust) -- Install via [rustup](https://rustup.rs/) - -## Installation - -To use `svmgov`, follow these steps: - -1. **Clone the Repository**: - - ```sh - git clone https://github.com/3uild-3thos/govcontract.git - cd govcontract/svmgov - ``` - -2. **Build the Project**: - - ```sh - cargo build --release - ``` - -3. **Run the CLI**: - ```sh - ./target/release/svmgov - ``` - ---- - -## Usage - -`svmgov` offers a set of commands to manage governance proposals and voting. Most commands require the `--identity-keypair` flag to specify the validator’s identity keypair for signing transactions. - -### Global Arguments - -- `--identity-keypair ` (short: `-i`; env: `SVMGOV_KEY`): Path to the validator’s identity keypair JSON file (required for most commands). -- `--rpc-url ` (short: `-r`; env: `SVMGOV_RPC`): Custom RPC URL for the Solana network (optional; defaults to `https://api.mainnet-beta.solana.com`). - -Example env usage: Set `export SVMGOV_KEY="/path/to/key.json"` and `export SVMGOV_RPC="https://api.testnet.solana.com"`, then run commands without these flags (e.g., `svmgov list-proposals` uses the env values). - -### Available Commands - -- `init-index`: Initialize the proposal index PDA (one-time setup). -- `create-proposal`: Create a new governance proposal with merkle proof verification. -- `support-proposal`: Support an existing proposal with stake verification. -- `cast-vote`: Cast a validator vote on an active proposal. -- `cast-vote-override`: Override validator vote as a delegator (requires stake account). -- `modify-vote`: Modify an existing vote on a proposal. -- `modify-vote-override`: Modify an existing vote override on a proposal (for delegators). -- `add-merkle-root`: Add merkle root hash to a proposal for verification. -- `finalize-proposal`: Finalize a proposal after voting period ends. -- `get-proposal`: Display a specific governance proposal. -- `list-proposals`: List all governance proposals, with optional status filtering. -- `list-votes`: List votes for a specific proposal, with optional verbose details. - -Run any command with `--help` for detailed usage, e.g., `svmgov create-proposal --help`. - -### Typical Workflow - -Here's a common sequence to get started: - -1. **Initialize index** (one-time): `svmgov init-index -i /path/to/identity_key.json` -2. **List proposals** to find IDs: `svmgov list-proposals -r https://api.mainnet-beta.solana.com` -3. **View proposal details**: `svmgov get-proposal --proposal-id -r https://api.mainnet-beta.solana.com` -4. **Create a proposal** (validators): `svmgov create-proposal --title "Proposal Title" --description "https://github.com/repo/issue" --ballot-id --snapshot-slot --network -i /path/to/identity_key.json` -5. **Support a proposal** (validators): `svmgov support-proposal --proposal-id --ballot-id --snapshot-slot --network -i /path/to/identity_key.json` -6. **Cast validator vote**: `svmgov cast-vote --proposal-id --ballot-id --for-votes 7000 --against-votes 2000 --abstain-votes 1000 --snapshot-slot --network -i /path/to/identity_key.json` -7. **Cast vote override** (delegators): `svmgov cast-vote-override --proposal-id --ballot-id --for-votes 7000 --against-votes 2000 --abstain-votes 1000 --snapshot-slot --network -i /path/to/identity_key.json` -8. **Modify vote** (if needed): `svmgov modify-vote --proposal-id --ballot-id --for-votes 8000 --against-votes 1000 --abstain-votes 1000 --snapshot-slot --network -i /path/to/identity_key.json` -9. **Modify vote override** (delegators, if needed): `svmgov modify-vote-override --proposal-id --ballot-id --for-votes 8000 --against-votes 1000 --abstain-votes 1000 --snapshot-slot --network -i /path/to/identity_key.json` -10. **Add merkle root** (proposal author): `svmgov add-merkle-root --proposal-id --merkle-root -i /path/to/identity_key.json` -11. **Finalize proposal** after voting ends: `svmgov finalize-proposal --proposal-id -i /path/to/identity_key.json` -12. **List votes** for verification: `svmgov list-votes --proposal-id --verbose -r https://api.mainnet-beta.solana.com` - ---- - -## Governance Mechanics - -The Solana Validator Governance program enforces the following rules, which impact CLI usage: - -- **Minimum Stake for Proposal Creation**: A validator must have at least **100,000 SOL** staked to create a proposal. If this requirement isn't met, the `create-proposal` command will fail with a `NotEnoughStake` error. -- **Cluster Support Threshold**: A proposal requires **500 basis points (5%) of total cluster support** to activate voting. Validators contribute to this using the `support-proposal` command. The smart contract calculates and enforces this threshold. -- **Merkle Proof Verification**: All stake-related operations use merkle proof verification to ensure stake ownership and prevent double-voting. The CLI integrates with external APIs to fetch and verify proofs. -- **Vote Override**: Delegators can override their validator's vote using stake account verification. This allows for more democratic participation in governance. -- **Proposal Lifecycle**: Proposals follow a lifecycle: Creation → Support Phase → Voting Phase → Finalization. Currently, proposals are created without specific epoch-based scheduling and become available for voting once they reach the cluster support threshold. - -The CLI does not perform local validation of these conditions; the smart contract handles enforcement through merkle proof verification, and the CLI relays any resulting errors. - -## Event Monitoring - -All CLI commands that interact with the governance contract emit comprehensive events that frontend applications and external services can monitor in real-time. These events provide complete transparency into governance activities. - -### Available Events - -- **`ProposalCreated`** - Emitted when `create-proposal` is executed -- **`ProposalSupported`** - Emitted when `support-proposal` is executed -- **`VoteCast`** - Emitted when `cast-vote` is executed -- **`VoteOverrideCast`** - Emitted when `cast-vote-override` is executed -- **`VoteModified`** - Emitted when `modify-vote` is executed -- **`VoteOverrideModified`** - Emitted when `modify-vote-override` is executed -- **`MerkleRootAdded`** - Emitted when `add-merkle-root` is executed -- **`ProposalFinalized`** - Emitted when `finalize-proposal` is executed - -### Monitoring Events - -Frontend applications can listen to these events for real-time updates: - -For detailed event data structures and usage examples, refer to the [Contract Events Documentation](../contract/readme.md#events). - ---- - -## Commands in Detail - -### `init-index` - -Initialize the proposal index PDA (one-time setup). - -**Arguments**: None required. - -**Requirements**: - -- Must be run once before creating any proposals to set up the index PDA. - -**Example**: - -```sh -svmgov init-index --identity-keypair /path/to/key.json -``` - -### `create-proposal` - -Create a new governance proposal with merkle proof verification. - -**Arguments**: - -- `--seed `: Optional unique seed for the proposal (used to derive the PDA). -- `--title `: Proposal title (required; max 50 characters). -- `--description <DESCRIPTION>`: Proposal description (required; must start with `https://github.com`; max 250 characters). -- `--ballot-id <ID>`: Ballot ID for consensus result PDA derivation (required). -- `--snapshot-slot <SLOT>`: Snapshot slot for fetching merkle proofs (required). -- `--network <NETWORK>`: Network for fetching merkle proofs (required). - -**Requirements**: - -- The validator's identity keypair must have at least **100,000 SOL** staked. -- Merkle proof verification is performed to validate stake ownership. - -**Example**: - -```sh -svmgov create-proposal --title "Update Fee Structure" --description "https://github.com/repo/test-proposal" --ballot-id 1 --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json -``` - -### `support-proposal` - -Support an existing proposal with stake verification to help it reach the 5% cluster support threshold. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal's ID (PDA) to support (required). -- `--ballot-id <ID>`: Ballot ID for consensus result PDA derivation (required). -- `--snapshot-slot <SLOT>`: Snapshot slot for fetching merkle proofs (required). -- `--network <NETWORK>`: Network for fetching merkle proofs (required). - -**Notes**: - -- Each validator's support contributes to the proposal's `cluster_support_bp`. Voting activates only when this reaches **500 basis points (5%)**. -- Merkle proof verification ensures stake ownership. - -**Example**: - -```sh -svmgov support-proposal --proposal-id "123" --ballot-id 1 --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json -``` - -### `cast-vote` - -Cast a validator vote on an active governance proposal with merkle proof verification. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal's ID (PDA) (required). -- `--ballot-id <ID>`: Ballot ID for consensus result PDA derivation (required). -- `--for-votes <BASIS_POINTS>`: Basis points for 'For' (required). -- `--against-votes <BASIS_POINTS>`: Basis points for 'Against' (required). -- `--abstain-votes <BASIS_POINTS>`: Basis points for 'Abstain' (required). -- `--snapshot-slot <SLOT>`: Snapshot slot for fetching merkle proofs (required). -- `--network <NETWORK>`: Network for fetching merkle proofs (required). - -**Requirements**: - -- Basis points must sum to 10,000 (100%). -- Merkle proof verification validates stake ownership. - -**Example**: - -```sh -svmgov cast-vote --proposal-id "123" --ballot-id 1 --for-votes 7000 --against-votes 2000 --abstain-votes 1000 --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json -``` - -### `cast-vote-override` - -Override a validator's vote as a delegator using stake account verification. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal's ID (PDA) (required). -- `--ballot-id <ID>`: Ballot ID for consensus result PDA derivation (required). -- `--for-votes <BASIS_POINTS>`: Basis points for 'For' (required). -- `--against-votes <BASIS_POINTS>`: Basis points for 'Against' (required). -- `--abstain-votes <BASIS_POINTS>`: Basis points for 'Abstain' (required). -- `--stake-account <PUBKEY>`: Optional stake account pubkey (base58). If omitted, the CLI selects the first stake account from the voter summary for the signer. -- `--snapshot-slot <SLOT>`: Snapshot slot for fetching merkle proofs (required). -- `--network <NETWORK>`: Network for fetching merkle proofs (required). -- `--operator-api <URL>`: Optional operator API endpoint for snapshot data. Uses env var by default. - -**Requirements**: - -- Basis points must sum to 10,000 (100%). -- Requires stake account ownership and merkle proof verification. -- Can only override votes for stake accounts delegated to the caller. - -**Examples**: - -```sh -# Auto-select first stake account from summary -svmgov cast-vote-override --proposal-id "123" --ballot-id 1 --for-votes 7000 --against-votes 2000 --abstain-votes 1000 --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json - -# Use an explicit stake account -svmgov cast-vote-override --proposal-id "123" --ballot-id 1 --for-votes 7000 --against-votes 2000 --abstain-votes 1000 --stake-account <STAKE_PUBKEY> --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json -``` - -### `modify-vote` - -Modify an existing vote on a proposal with merkle proof verification. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal's ID (PDA) (required). -- `--ballot-id <ID>`: Ballot ID for consensus result PDA derivation (required). -- `--for-votes <BASIS_POINTS>`: Basis points for 'For' (required). -- `--against-votes <BASIS_POINTS>`: Basis points for 'Against' (required). -- `--abstain-votes <BASIS_POINTS>`: Basis points for 'Abstain' (required). -- `--snapshot-slot <SLOT>`: Snapshot slot for fetching merkle proofs (required). -- `--network <NETWORK>`: Network for fetching merkle proofs (required). - -**Requirements**: - -- Basis points must sum to 10,000 (100%). -- Merkle proof verification validates stake ownership. - -**Example**: - -```sh -svmgov modify-vote --proposal-id "123" --ballot-id 1 --for-votes 8000 --against-votes 1000 --abstain-votes 1000 --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json -``` - -### `modify-vote-override` - -Modify an existing vote override on a proposal (for delegators). - -**Arguments**: - -- `--proposal-id <ID>`: The proposal's ID (PDA) (required). -- `--ballot-id <ID>`: Ballot ID for consensus result PDA derivation (required). -- `--for-votes <BASIS_POINTS>`: Basis points for 'For' (required). -- `--against-votes <BASIS_POINTS>`: Basis points for 'Against' (required). -- `--abstain-votes <BASIS_POINTS>`: Basis points for 'Abstain' (required). -- `--stake-account <PUBKEY>`: Optional stake account pubkey (base58). If omitted, the CLI selects the first stake account from the voter summary for the signer. -- `--snapshot-slot <SLOT>`: Snapshot slot for fetching merkle proofs (required). -- `--network <NETWORK>`: Network for fetching merkle proofs (required). -- `--operator-api <URL>`: Optional operator API endpoint for snapshot data. Uses env var by default. - -**Requirements**: - -- Basis points must sum to 10,000 (100%). -- Requires stake account ownership and merkle proof verification. -- Can only modify vote overrides for stake accounts delegated to the caller. - -**Examples**: - -```sh -# Auto-select first stake account from summary -svmgov modify-vote-override --proposal-id "123" --ballot-id 1 --for-votes 8000 --against-votes 1000 --abstain-votes 1000 --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json - -# Use an explicit stake account -svmgov modify-vote-override --proposal-id "123" --ballot-id 1 --for-votes 8000 --against-votes 1000 --abstain-votes 1000 --stake-account <STAKE_PUBKEY> --snapshot-slot 123456 --network mainnet --identity-keypair /path/to/key.json -``` - -### `add-merkle-root` - -Add a merkle root hash to a proposal for stake verification. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal's ID (PDA) (required). -- `--merkle-root <HASH>`: The merkle root hash as a hex string (required). Accepts with or without `0x` prefix; must decode to exactly 32 bytes. - -**Requirements**: - -- Can only be called by the original proposal author. -- Merkle root hash cannot be all zeros. -- Must decode to exactly 32 bytes. - -**Example**: - -```sh -svmgov add-merkle-root --proposal-id "123" --merkle-root "0x1234567890abcdef..." --identity-keypair /path/to/key.json -``` - -### `finalize-proposal` - -Finalize a proposal after its voting period has ended. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal's ID (PDA) (required). - -**Requirements**: - -- The voting period must have ended. -- All votes must have been counted. - -**Example**: - -```sh -svmgov finalize-proposal --proposal-id "123" --identity-keypair /path/to/key.json -``` - -### `get-proposal` - -Display a specific governance proposal. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal’s ID (PDA) (required). - -**Example**: - -```sh -svmgov get-proposal --proposal-id "123" --rpc-url https://api.mainnet-beta.solana.com -``` - -### `list-proposals` - -List all governance proposals, optionally filtered by status. - -**Arguments**: - -- `--status <STATUS>`: Optional filter (e.g., "active"). -- `--limit <NUMBER>`: Limit the number of proposals listed (default: 0, meaning no limit). -- `--json`: Output in JSON format (default: false). - -**Example**: - -```sh -svmgov list-proposals --rpc-url https://api.mainnet-beta.solana.com --limit 5 --json -``` - -### `list-votes` - -List votes for a specific proposal, with optional verbose details. - -**Arguments**: - -- `--proposal-id <ID>`: The proposal’s ID (PDA) (required). -- `--verbose`: List votes with details (default: false). -- `--limit <NUMBER>`: Limit the number of votes listed (default: 0, meaning no limit). -- `--json`: Output in JSON format (default: false). - -**Example**: - -```sh -svmgov list-votes --proposal-id "123" --rpc-url https://api.mainnet-beta.solana.com --verbose --limit 10 --json -``` - ---- - -## Additional Notes - -- **Identity Keypair**: Must have sufficient stake and permissions for actions like creating proposals or voting. Both validators and delegators can use the CLI. -- **Vote Allocation**: In `cast-vote`, `cast-vote-override`, `modify-vote`, and `modify-vote-override`, basis points (`--for-votes`, `--against-votes`, `--abstain-votes`) must sum to 10,000 (100%). E.g., 70% 'For' (7000), 20% 'Against' (2000), 10% 'Abstain' (1000). -- **Merkle Proof Verification**: All voting operations use merkle proof verification to ensure stake ownership and prevent double-voting. Commands that require merkle proofs need `--ballot-id`, `--snapshot-slot`, and `--network` flags. -- **API Integration**: The CLI integrates with external APIs for real-time validator stake data and merkle proof generation. -- **Vote Override**: Delegators can override their validator's vote using stake account verification, providing additional governance flexibility. Both `cast-vote-override` and `modify-vote-override` support this functionality. -- **Proposal States**: Proposals go through states: Created → Supported → Voting → Finalized, with appropriate validation at each step. - -## Troubleshooting - -- **Compilation fails on older Rust?** Ensure you're using Rust 1.85.0 or higher (stable). No nightly features are used in this project—do not install nightly Rust, as it may introduce unrelated issues. Update your toolchain with `rustup update stable` and set `rustup default stable`. -- **Merkle proof errors?** Ensure your validator/delegator has sufficient stake and the merkle proof service is accessible. -- **Vote override fails?** Verify you own the stake account and it has active stake delegated to a validator. diff --git a/svmgov/src/config.rs b/svmgov/src/config.rs new file mode 100644 index 00000000..846cf6c7 --- /dev/null +++ b/svmgov/src/config.rs @@ -0,0 +1,127 @@ +use std::fs; +use std::path::PathBuf; + +use anyhow::{Result, anyhow}; +use dirs::home_dir; +use serde::{Deserialize, Serialize}; + +use crate::constants::{DEFAULT_MAINNET_RPC_URL, DEFAULT_TESTNET_RPC_URL}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum UserType { + Validator, + Staker, +} + +impl std::fmt::Display for UserType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserType::Validator => write!(f, "Validator"), + UserType::Staker => write!(f, "Staker"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub user_type: Option<UserType>, + pub identity_keypair_path: Option<String>, + pub staker_keypair_path: Option<String>, + pub network: String, + pub rpc_url: Option<String>, + pub operator_api_url: Option<String>, +} + +impl Default for Config { + fn default() -> Self { + Config { + user_type: None, + identity_keypair_path: None, + staker_keypair_path: Some(default_staker_keypair_path()), + network: "mainnet".to_string(), + rpc_url: None, + operator_api_url: None, + } + } +} + +fn default_staker_keypair_path() -> String { + if let Some(home) = home_dir() { + home.join(".config") + .join("solana") + .join("id.json") + .to_string_lossy() + .to_string() + } else { + "~/.config/solana/id.json".to_string() + } +} + +impl Config { + pub fn config_dir() -> Result<PathBuf> { + let home = home_dir().ok_or_else(|| anyhow!("Could not find home directory"))?; + Ok(home.join(".svmgov")) + } + + pub fn config_file_path() -> Result<PathBuf> { + Ok(Self::config_dir()?.join("config.toml")) + } + + pub fn load() -> Result<Config> { + let config_path = Self::config_file_path()?; + + if !config_path.exists() { + return Ok(Config::default()); + } + + let content = fs::read_to_string(&config_path) + .map_err(|e| anyhow!("Failed to read config file {}: {}", config_path.display(), e))?; + + let config: Config = toml::from_str(&content) + .map_err(|e| anyhow!("Failed to parse config file {}: {}", config_path.display(), e))?; + + Ok(config) + } + + pub fn save(&self) -> Result<()> { + let config_dir = Self::config_dir()?; + let config_path = Self::config_file_path()?; + + // Create config directory if it doesn't exist + if !config_dir.exists() { + fs::create_dir_all(&config_dir) + .map_err(|e| anyhow!("Failed to create config directory {}: {}", config_dir.display(), e))?; + } + + let toml_string = toml::to_string_pretty(self) + .map_err(|e| anyhow!("Failed to serialize config: {}", e))?; + + fs::write(&config_path, toml_string) + .map_err(|e| anyhow!("Failed to write config file {}: {}", config_path.display(), e))?; + + Ok(()) + } + + pub fn get_rpc_url(&self) -> String { + self.rpc_url.clone().unwrap_or_else(|| { + get_default_rpc_url(&self.network) + }) + } + + pub fn get_identity_keypair_path(&self) -> Option<String> { + match &self.user_type { + Some(UserType::Validator) => self.identity_keypair_path.clone(), + Some(UserType::Staker) => self.staker_keypair_path.clone(), + None => None, + } + } +} + +pub fn get_default_rpc_url(network: &str) -> String { + match network.to_lowercase().as_str() { + "mainnet" => DEFAULT_MAINNET_RPC_URL.to_string(), + "testnet" => DEFAULT_TESTNET_RPC_URL.to_string(), + _ => DEFAULT_MAINNET_RPC_URL.to_string(), + } +} + diff --git a/svmgov/src/constants.rs b/svmgov/src/constants.rs index 66ed444a..25b93995 100644 --- a/svmgov/src/constants.rs +++ b/svmgov/src/constants.rs @@ -2,6 +2,10 @@ pub const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com"; pub const DEFAULT_WSS_URL: &str = "wss://api.mainnet-beta.solana.com"; +// Network-specific default RPC URLs +pub const DEFAULT_MAINNET_RPC_URL: &str = "https://api.mainnet-beta.solana.com"; +pub const DEFAULT_TESTNET_RPC_URL: &str = "https://api.testnet.solana.com"; + // Voting constants pub const BASIS_POINTS_TOTAL: u64 = 10_000; @@ -11,14 +15,11 @@ pub const DEFAULT_OPERATOR_API_URL: &str = "http://84.32.100.123:8000"; // UI constants pub const SPINNER_TICK_DURATION_MS: u64 = 100; -// Stake account constants -pub const STAKE_ACCOUNT_DATA_SIZE: u64 = 200; -pub const STAKE_ACCOUNT_WITHDRAW_AUTHORITY_OFFSET: usize = 44; - -// Mock data constants -pub const MOCK_MERKLE_PROOF_LEVELS: usize = 3; - // Environment variable names pub const SVMGOV_KEY_ENV: &str = "SVMGOV_KEY"; pub const SVMGOV_RPC_ENV: &str = "SVMGOV_RPC"; pub const SVMGOV_OPERATOR_URL_ENV: &str = "SVMGOV_OPERATOR_URL"; + +pub const DISCUSSION_EPOCHS: u64 = 4; +pub const VOTING_EPOCHS: u64 = 3; +pub const SNAPSHOT_EPOCH_EXTENSION: u64 = 1; diff --git a/svmgov/src/instructions/add_merkle_root.rs b/svmgov/src/instructions/add_merkle_root.rs deleted file mode 100644 index 8e9756fa..00000000 --- a/svmgov/src/instructions/add_merkle_root.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::str::FromStr; - -use anchor_client::solana_sdk::{pubkey::Pubkey, signer::Signer}; -use anchor_lang::AnchorDeserialize; -use anyhow::{Result, anyhow}; -use log::info; - -use crate::{ - govcontract::client::{accounts, args}, - utils::utils::{create_spinner, setup_all}, -}; - -pub async fn add_merkle_root( - proposal_id: String, - merkle_root_hash: String, - identity_keypair: Option<String>, - rpc_url: Option<String>, -) -> Result<()> { - let spinner = create_spinner("Adding merkle root hash to proposal..."); - - let (payer, _payer_pubkey, program, _merkle_proof_program) = - setup_all(identity_keypair, rpc_url).await?; - - let proposal_pubkey = - Pubkey::from_str(&proposal_id).map_err(|_| anyhow!("Invalid proposal ID format"))?; - - let merkle_root_bytes = Pubkey::from_str(&merkle_root_hash) - .expect("Invalid merkle root hash format") - .to_bytes(); - - if merkle_root_bytes.len() != 32 { - return Err(anyhow!("Merkle root hash must be exactly 32 bytes")); - } - - let mut merkle_root_array = [0u8; 32]; - merkle_root_array.copy_from_slice(&merkle_root_bytes); - - let signature = program - .request() - .args(args::AddMerkleRoot { - merkle_root_hash: merkle_root_array, - }) - .accounts(accounts::AddMerkleRoot { - signer: payer.pubkey(), - proposal: proposal_pubkey, - }) - .send() - .await?; - - spinner.finish_with_message(format!( - "Merkle root hash added successfully! Signature: {}", - signature - )); - - Ok(()) -} diff --git a/svmgov/src/instructions/adjust_proposal_timing.rs b/svmgov/src/instructions/adjust_proposal_timing.rs new file mode 100644 index 00000000..30f698e2 --- /dev/null +++ b/svmgov/src/instructions/adjust_proposal_timing.rs @@ -0,0 +1,71 @@ +use std::str::FromStr; + +use anchor_client::solana_sdk::{pubkey::Pubkey, signer::Signer}; +use anyhow::{anyhow, Result}; + +use crate::{ + govcontract::client::{accounts, args}, + utils::utils::{create_spinner, setup_all}, +}; + +pub async fn adjust_proposal_timing( + proposal_id: String, + creation_timestamp: Option<i64>, + creation_epoch: Option<u64>, + start_epoch: Option<u64>, + end_epoch: Option<u64>, + snapshot_slot: Option<u64>, + consensus_result: Option<Option<String>>, + identity_keypair: Option<String>, + rpc_url: Option<String>, +) -> Result<()> { + let proposal_pubkey = Pubkey::from_str(&proposal_id) + .map_err(|_| anyhow!("Invalid proposal ID: {}", proposal_id))?; + + let (payer, _vote_account, program, _merkle_proof_program) = setup_all(identity_keypair, rpc_url).await?; + + // Parse consensus_result if provided + // None = don't change, Some(None) = clear it, Some(Some(pubkey)) = set to pubkey + let consensus_result_pubkey: Option<Option<Pubkey>> = if let Some(cr_opt) = consensus_result { + if let Some(cr_str) = cr_opt { + if cr_str.to_lowercase() == "none" || cr_str.is_empty() { + Some(None) // Clear consensus_result + } else { + let pubkey = Pubkey::from_str(&cr_str) + .map_err(|_| anyhow!("Invalid consensus result pubkey: {}", cr_str))?; + Some(Some(pubkey)) // Set to pubkey + } + } else { + Some(None) // Clear consensus_result + } + } else { + None // Don't change consensus_result + }; + + let spinner = create_spinner("Adjusting proposal timing..."); + + let sig = program + .request() + .args(args::AdjustProposalTiming { + creation_timestamp, + creation_epoch, + start_epoch, + end_epoch, + snapshot_slot, + consensus_result: consensus_result_pubkey, + }) + .accounts(accounts::AdjustProposalTiming { + signer: payer.pubkey(), + proposal: proposal_pubkey, + }) + .send() + .await?; + + spinner.finish_with_message(format!( + "Proposal timing adjusted successfully. https://explorer.solana.com/tx/{}", + sig + )); + + Ok(()) +} + diff --git a/svmgov/src/instructions/cast_vote.rs b/svmgov/src/instructions/cast_vote.rs index 44252ed7..bb71c539 100644 --- a/svmgov/src/instructions/cast_vote.rs +++ b/svmgov/src/instructions/cast_vote.rs @@ -8,22 +8,20 @@ use log::info; use crate::{ constants::*, - govcontract::client::{accounts, args}, + govcontract::{accounts::Proposal, client::{accounts, args}}, utils::{ - api_helpers::{generate_pdas_from_vote_proof_response, get_vote_account_proof}, + api_helpers::{self, get_vote_account_proof}, utils::{create_spinner, derive_vote_override_cache_pda, derive_vote_pda, setup_all}, }, }; pub async fn cast_vote( proposal_id: String, - ballot_id: u64, votes_for: u64, votes_against: u64, abstain: u64, identity_keypair: Option<String>, rpc_url: Option<String>, - snapshot_slot: u64, network: String, ) -> Result<()> { if votes_for + votes_against + abstain != BASIS_POINTS_TOTAL { @@ -39,11 +37,24 @@ pub async fn cast_vote( let (payer, vote_account, program, merkle_proof_program) = setup_all(identity_keypair, rpc_url).await?; + // Fetch proposal to get snapshot_slot and consensus_result + let proposal = program + .account::<Proposal>(proposal_pubkey) + .await + .map_err(|e| anyhow!("Failed to fetch proposal: {}", e))?; + + let snapshot_slot = proposal.snapshot_slot; + let consensus_result_pda = proposal + .consensus_result + .ok_or_else(|| anyhow!("Proposal consensus_result is not set"))?; + let proof_response = get_vote_account_proof(&vote_account.to_string(), snapshot_slot, &network).await?; - let (consensus_result_pda, meta_merkle_proof_pda) = - generate_pdas_from_vote_proof_response(ballot_id, &proof_response)?; + // Generate meta_merkle_proof_pda using the consensus_result from proposal + let vote_account_pubkey = Pubkey::from_str(&proof_response.meta_merkle_leaf.vote_account) + .map_err(|e| anyhow!("Invalid vote_account pubkey in response: {}", e))?; + let meta_merkle_proof_pda = api_helpers::generate_meta_merkle_proof_pda(&consensus_result_pda, &vote_account_pubkey)?; let vote_pda = derive_vote_pda(&proposal_pubkey, &vote_account, &program.id()); let vote_override_cache_pda = diff --git a/svmgov/src/instructions/cast_vote_override.rs b/svmgov/src/instructions/cast_vote_override.rs index 81aa7a90..344c5e98 100644 --- a/svmgov/src/instructions/cast_vote_override.rs +++ b/svmgov/src/instructions/cast_vote_override.rs @@ -8,12 +8,11 @@ use log::info; use crate::{ constants::*, - govcontract::client::{accounts, args}, + govcontract::{accounts::Proposal, client::{accounts, args}}, utils::{ api_helpers::{ - convert_merkle_proof_strings, convert_stake_merkle_leaf_data_to_idl_type, - generate_pdas_from_vote_proof_response, get_stake_account_proof, - get_vote_account_proof, + self, convert_merkle_proof_strings, convert_stake_merkle_leaf_data_to_idl_type, + get_stake_account_proof, get_vote_account_proof, }, utils::{ create_spinner, derive_vote_override_cache_pda, derive_vote_override_pda, @@ -24,7 +23,6 @@ use crate::{ pub async fn cast_vote_override( proposal_id: String, - ballot_id: u64, for_votes: u64, against_votes: u64, abstain_votes: u64, @@ -33,7 +31,6 @@ pub async fn cast_vote_override( _operator_api: Option<String>, stake_account_override: String, vote_account: String, - snapshot_slot: u64, network: String, ) -> Result<()> { if for_votes + against_votes + abstain_votes != BASIS_POINTS_TOTAL { @@ -48,6 +45,17 @@ pub async fn cast_vote_override( let (payer, program, merkle_proof_program) = setup_all_with_staker(staker_keypair, rpc_url)?; + // Fetch proposal to get snapshot_slot and consensus_result + let proposal = program + .account::<Proposal>(proposal_pubkey) + .await + .map_err(|e| anyhow!("Failed to fetch proposal: {}", e))?; + + let snapshot_slot = proposal.snapshot_slot; + let consensus_result_pda = proposal + .consensus_result + .ok_or_else(|| anyhow!("Proposal consensus_result is not set"))?; + let stake_account_str = stake_account_override.clone(); let vote_account_pubkey = Pubkey::from_str(&vote_account) .map_err(|_| anyhow!("Invalid vote account: {}", vote_account))?; @@ -57,8 +65,8 @@ pub async fn cast_vote_override( let stake_merkle_proof = get_stake_account_proof(&stake_account_str, snapshot_slot, &network).await?; - let (consensus_result_pda, meta_merkle_proof_pda) = - generate_pdas_from_vote_proof_response(ballot_id, &meta_merkle_proof)?; + // Generate meta_merkle_proof_pda using the consensus_result from proposal + let meta_merkle_proof_pda = api_helpers::generate_meta_merkle_proof_pda(&consensus_result_pda, &vote_account_pubkey)?; let validator_vote_pda = derive_vote_pda(&proposal_pubkey, &vote_account_pubkey, &program.id()); let vote_override_pda = derive_vote_override_pda( diff --git a/svmgov/src/instructions/create_proposal.rs b/svmgov/src/instructions/create_proposal.rs index f760a99a..48fa9f47 100644 --- a/svmgov/src/instructions/create_proposal.rs +++ b/svmgov/src/instructions/create_proposal.rs @@ -8,10 +8,7 @@ use log::info; use crate::{ govcontract::client::{accounts, args}, - utils::{ - api_helpers::{generate_pdas_from_vote_proof_response, get_vote_account_proof}, - utils::{create_spinner, derive_proposal_index_pda, derive_proposal_pda, setup_all}, - }, + utils::utils::{create_spinner, derive_proposal_index_pda, derive_proposal_pda, setup_all}, }; pub async fn create_proposal( @@ -20,9 +17,7 @@ pub async fn create_proposal( seed: Option<u64>, identity_keypair: Option<String>, rpc_url: Option<String>, - snapshot_slot: u64, network: String, - ballot_id: u64, ) -> Result<()> { log::debug!( "create_proposal: title={}, description={}, seed={:?}, identity_keypair={:?}, rpc_url={:?}", @@ -42,96 +37,7 @@ pub async fn create_proposal( let proposal_index_pda = derive_proposal_index_pda(&program.id()); - let proof_response = - get_vote_account_proof(&vote_account.to_string(), snapshot_slot, &network).await?; - - let (consensus_result_pda, meta_merkle_proof_pda) = - generate_pdas_from_vote_proof_response(ballot_id, &proof_response)?; - let voting_wallet = Pubkey::from_str(&proof_response.meta_merkle_leaf.voting_wallet) - .map_err(|e| anyhow::anyhow!("Invalid voting wallet in proof: {}", e))?; - if voting_wallet != payer.pubkey() { - return Err(anyhow::anyhow!( - "Voting wallet in proof ({}) doesn't match signer ({})", - voting_wallet, - payer.pubkey() - )); - } - - info!( - "snapshot_program: {:?} consensus_result: {:?}, meta_merkle_proof: {:?}", - SNAPSHOT_PROGRAM_ID.to_string(), - consensus_result_pda.to_string(), - meta_merkle_proof_pda.to_string(), - ); - - let meta_merkle_proof_account = match program - .account::<MetaMerkleProof>(meta_merkle_proof_pda) - .await - { - Ok(account) => Some(account), - Err(_e) => { - info!("Unable to get meta merkle proof account"); - None - } - }; - - // First transaction: Initialize meta merkle proof if needed - if meta_merkle_proof_account.is_none() { - info!("Creating meta merkle proof account"); - - let init_spinner = create_spinner("Initializing meta merkle proof..."); - - let init_meta_merkle_proof_ix = merkle_proof_program - .request() - .args(gov_v1::instruction::InitMetaMerkleProof { - close_timestamp: 1, - meta_merkle_leaf: MetaMerkleLeaf { - voting_wallet, - vote_account, - stake_merkle_root: Pubkey::from_str_const( - proof_response.meta_merkle_leaf.stake_merkle_root.as_str(), - ) - .to_bytes(), - active_stake: proof_response.meta_merkle_leaf.active_stake, - }, - meta_merkle_proof: proof_response - .meta_merkle_proof - .iter() - .map(|s| Pubkey::from_str_const(s).to_bytes()) - .collect(), - }) - .accounts(gov_v1::accounts::InitMetaMerkleProof { - consensus_result: consensus_result_pda, - merkle_proof: meta_merkle_proof_pda, - payer: payer.pubkey(), - system_program: system_program::ID, - }) - .instructions()?; - - let blockhash = merkle_proof_program.rpc().get_latest_blockhash().await?; - let transaction = Transaction::new_signed_with_payer( - &init_meta_merkle_proof_ix, - Some(&payer.pubkey()), - &[&payer], - blockhash, - ); - - let sig = merkle_proof_program - .rpc() - .send_and_confirm_transaction(&transaction) - .await?; - log::debug!( - "Meta merkle proof initialization transaction sent successfully: signature={}", - sig - ); - - init_spinner.finish_with_message(format!( - "Meta merkle proof initialized. https://explorer.solana.com/tx/{}", - sig - )); - } - - // Second transaction: Create proposal + // Create proposal - snapshot_slot and consensus_result will be set later in support_proposal let spinner = create_spinner("Creating proposal..."); let create_proposal_ixs = program @@ -146,9 +52,6 @@ pub async fn create_proposal( spl_vote_account: vote_account, proposal: proposal_pda, proposal_index: proposal_index_pda, - snapshot_program: SNAPSHOT_PROGRAM_ID, - consensus_result: consensus_result_pda, - meta_merkle_proof: meta_merkle_proof_pda, system_program: system_program::ID, }) .instructions()?; diff --git a/svmgov/src/instructions/mod.rs b/svmgov/src/instructions/mod.rs index 7b14989f..c6f55ac2 100644 --- a/svmgov/src/instructions/mod.rs +++ b/svmgov/src/instructions/mod.rs @@ -1,4 +1,4 @@ -pub mod add_merkle_root; +pub mod adjust_proposal_timing; pub mod cast_vote; pub mod cast_vote_override; pub mod create_proposal; @@ -8,7 +8,7 @@ pub mod modify_vote; pub mod modify_vote_override; pub mod support_proposal; -pub use add_merkle_root::add_merkle_root; +pub use adjust_proposal_timing::adjust_proposal_timing; pub use cast_vote::cast_vote; pub use cast_vote_override::cast_vote_override; pub use create_proposal::create_proposal; diff --git a/svmgov/src/instructions/modify_vote.rs b/svmgov/src/instructions/modify_vote.rs index d0fadf19..34e2704a 100644 --- a/svmgov/src/instructions/modify_vote.rs +++ b/svmgov/src/instructions/modify_vote.rs @@ -7,22 +7,20 @@ use gov_v1::ID as SNAPSHOT_PROGRAM_ID; use crate::{ constants::*, - govcontract::client::{accounts, args}, + govcontract::{accounts::Proposal, client::{accounts, args}}, utils::{ - api_helpers::{generate_pdas_from_vote_proof_response, get_vote_account_proof}, + api_helpers::{self, get_vote_account_proof}, utils::{create_spinner, derive_vote_pda, setup_all}, }, }; pub async fn modify_vote( proposal_id: String, - ballot_id: u64, for_votes: u64, against_votes: u64, abstain_votes: u64, identity_keypair: Option<String>, rpc_url: Option<String>, - snapshot_slot: u64, network: String, ) -> Result<()> { if for_votes + against_votes + abstain_votes != BASIS_POINTS_TOTAL { @@ -38,11 +36,24 @@ pub async fn modify_vote( let (payer, vote_account, program, _merkle_proof_program) = setup_all(identity_keypair, rpc_url).await?; + // Fetch proposal to get snapshot_slot and consensus_result + let proposal = program + .account::<Proposal>(proposal_pubkey) + .await + .map_err(|e| anyhow!("Failed to fetch proposal: {}", e))?; + + let snapshot_slot = proposal.snapshot_slot; + let consensus_result_pda = proposal + .consensus_result + .ok_or_else(|| anyhow!("Proposal consensus_result is not set"))?; + let proof_response = get_vote_account_proof(&vote_account.to_string(), snapshot_slot, &network).await?; - let (consensus_result_pda, meta_merkle_proof_pda) = - generate_pdas_from_vote_proof_response(ballot_id, &proof_response)?; + // Generate meta_merkle_proof_pda using the consensus_result from proposal + let vote_account_pubkey = Pubkey::from_str(&proof_response.meta_merkle_leaf.vote_account) + .map_err(|e| anyhow!("Invalid vote_account pubkey in response: {}", e))?; + let meta_merkle_proof_pda = api_helpers::generate_meta_merkle_proof_pda(&consensus_result_pda, &vote_account_pubkey)?; let vote_pda = derive_vote_pda(&proposal_pubkey, &vote_account, &program.id()); diff --git a/svmgov/src/instructions/modify_vote_override.rs b/svmgov/src/instructions/modify_vote_override.rs index aaf38765..3e16c5c5 100644 --- a/svmgov/src/instructions/modify_vote_override.rs +++ b/svmgov/src/instructions/modify_vote_override.rs @@ -4,16 +4,17 @@ use anchor_client::solana_sdk::{pubkey::Pubkey, signer::Signer}; use anchor_lang::system_program; use anyhow::{Result, anyhow}; use gov_v1::ID as SNAPSHOT_PROGRAM_ID; -use log::info; use crate::{ constants::*, - govcontract::client::{accounts, args}, + govcontract::{ + accounts::Proposal, + client::{accounts, args}, + }, utils::{ api_helpers::{ - convert_merkle_proof_strings, convert_stake_merkle_leaf_data_to_idl_type, - generate_pdas_from_vote_proof_response, get_stake_account_proof, - get_vote_account_proof, + self, convert_merkle_proof_strings, convert_stake_merkle_leaf_data_to_idl_type, + get_stake_account_proof, }, utils::{ create_spinner, derive_vote_override_cache_pda, derive_vote_override_pda, @@ -24,7 +25,6 @@ use crate::{ pub async fn modify_vote_override( proposal_id: String, - ballot_id: u64, for_votes: u64, against_votes: u64, abstain_votes: u64, @@ -33,7 +33,6 @@ pub async fn modify_vote_override( _operator_api: Option<String>, stake_account_override: String, vote_account: String, - snapshot_slot: u64, network: String, ) -> Result<()> { if for_votes + against_votes + abstain_votes != BASIS_POINTS_TOTAL { @@ -48,17 +47,27 @@ pub async fn modify_vote_override( let (payer, program, _merkle_proof_program) = setup_all_with_staker(staker_keypair, rpc_url)?; + // Fetch proposal to get snapshot_slot and consensus_result + let proposal = program + .account::<Proposal>(proposal_pubkey) + .await + .map_err(|e| anyhow!("Failed to fetch proposal: {}", e))?; + + let snapshot_slot = proposal.snapshot_slot; + let consensus_result_pda = proposal + .consensus_result + .ok_or_else(|| anyhow!("Proposal consensus_result is not set"))?; + let stake_account_str = stake_account_override.clone(); let vote_account_pubkey = Pubkey::from_str(&vote_account) .map_err(|_| anyhow!("Invalid vote account: {}", vote_account))?; - let meta_merkle_proof = get_vote_account_proof(&vote_account, snapshot_slot, &network).await?; - let stake_merkle_proof = get_stake_account_proof(&stake_account_str, snapshot_slot, &network).await?; - let (consensus_result_pda, meta_merkle_proof_pda) = - generate_pdas_from_vote_proof_response(ballot_id, &meta_merkle_proof)?; + // Generate meta_merkle_proof_pda using the consensus_result from proposal + let meta_merkle_proof_pda = + api_helpers::generate_meta_merkle_proof_pda(&consensus_result_pda, &vote_account_pubkey)?; let validator_vote_pda = derive_vote_pda(&proposal_pubkey, &vote_account_pubkey, &program.id()); let vote_override_pda = derive_vote_override_pda( diff --git a/svmgov/src/instructions/support_proposal.rs b/svmgov/src/instructions/support_proposal.rs index 23f69441..eb39195b 100644 --- a/svmgov/src/instructions/support_proposal.rs +++ b/svmgov/src/instructions/support_proposal.rs @@ -1,16 +1,21 @@ use std::str::FromStr; -use anchor_client::solana_sdk::{pubkey::Pubkey, signer::Signer, transaction::Transaction}; +use anchor_client::{ + solana_client::rpc_config::RpcSendTransactionConfig, + solana_sdk::{pubkey::Pubkey, signer::Signer, transaction::Transaction}, +}; use anchor_lang::system_program; use anyhow::{Result, anyhow}; -use gov_v1::{ID as SNAPSHOT_PROGRAM_ID, MetaMerkleLeaf, MetaMerkleProof}; -use log::info; +use base64::Engine; +use gov_v1::ID as SNAPSHOT_PROGRAM_ID; +use serde::Serialize; use crate::{ + constants::{DISCUSSION_EPOCHS, SNAPSHOT_EPOCH_EXTENSION}, govcontract::client::{accounts, args}, - utils::{ - api_helpers::{generate_pdas_from_vote_proof_response, get_vote_account_proof}, - utils::{create_spinner, derive_support_pda, setup_all}, + utils::utils::{ + create_spinner, derive_program_config_pda, derive_support_pda, get_epoch_slot_range, + setup_all, }, }; @@ -18,95 +23,31 @@ pub async fn support_proposal( proposal_id: String, identity_keypair: Option<String>, rpc_url: Option<String>, - ballot_id: u64, - snapshot_slot: u64, - network: String, + _network: String, ) -> Result<()> { let proposal_pubkey = Pubkey::from_str(&proposal_id) .map_err(|_| anyhow!("Invalid proposal ID: {}", proposal_id))?; - let (payer, vote_account, program, merkle_proof_program) = + let (payer, vote_account, program, _merkle_proof_program) = setup_all(identity_keypair, rpc_url).await?; - let proof_response = - get_vote_account_proof(&vote_account.to_string(), snapshot_slot, &network).await?; + let support_pda = derive_support_pda(&proposal_pubkey, &vote_account, &program.id()); - let (consensus_result_pda, meta_merkle_proof_pda) = - generate_pdas_from_vote_proof_response(ballot_id, &proof_response)?; + let spinner = create_spinner("Supporting proposal..."); - let support_pda = derive_support_pda(&proposal_pubkey, &vote_account, &program.id()); + let clock = program.rpc().get_epoch_info().await?; + let target_epoch = clock.epoch + DISCUSSION_EPOCHS + SNAPSHOT_EPOCH_EXTENSION; + + let (start_slot, _) = get_epoch_slot_range(target_epoch); + let snapshot_slot = start_slot + 1000; - let meta_merkle_proof_account = match program - .account::<MetaMerkleProof>(meta_merkle_proof_pda) - .await - { - Ok(account) => Some(account), - Err(_e) => { - info!("Unable to get meta merkle proof account"); - None - } + let ballot_box_pda = { + let seeds = &[b"BallotBox".as_ref(), &snapshot_slot.to_le_bytes()]; + let (pda, _) = Pubkey::find_program_address(seeds, &SNAPSHOT_PROGRAM_ID); + pda }; - // First transaction: Initialize meta merkle proof if needed - if meta_merkle_proof_account.is_none() { - info!("Creating meta merkle proof account"); - - let init_spinner = create_spinner("Initializing meta merkle proof..."); - - let voting_wallet = Pubkey::from_str(&proof_response.meta_merkle_leaf.voting_wallet)?; - - let init_meta_merkle_proof_ix = merkle_proof_program - .request() - .args(gov_v1::instruction::InitMetaMerkleProof { - close_timestamp: 1, - meta_merkle_leaf: MetaMerkleLeaf { - voting_wallet, - vote_account, - stake_merkle_root: Pubkey::from_str_const( - proof_response.meta_merkle_leaf.stake_merkle_root.as_str(), - ) - .to_bytes(), - active_stake: proof_response.meta_merkle_leaf.active_stake, - }, - meta_merkle_proof: proof_response - .meta_merkle_proof - .iter() - .map(|s| Pubkey::from_str_const(s).to_bytes()) - .collect(), - }) - .accounts(gov_v1::accounts::InitMetaMerkleProof { - consensus_result: consensus_result_pda, - merkle_proof: meta_merkle_proof_pda, - payer: payer.pubkey(), - system_program: system_program::ID, - }) - .instructions()?; - - let blockhash = merkle_proof_program.rpc().get_latest_blockhash().await?; - let transaction = Transaction::new_signed_with_payer( - &init_meta_merkle_proof_ix, - Some(&payer.pubkey()), - &[&payer], - blockhash, - ); - - let sig = merkle_proof_program - .rpc() - .send_and_confirm_transaction(&transaction) - .await?; - log::debug!( - "Meta merkle proof initialization transaction sent successfully: signature={}", - sig - ); - - init_spinner.finish_with_message(format!( - "Meta merkle proof initialized. https://explorer.solana.com/tx/{}", - sig - )); - } - - // Second transaction: Support proposal - let spinner = create_spinner("Supporting proposal..."); + let program_config_pda = derive_program_config_pda(&SNAPSHOT_PROGRAM_ID); let support_proposal_ixs = program .request() @@ -116,9 +57,9 @@ pub async fn support_proposal( proposal: proposal_pubkey, support: support_pda, spl_vote_account: vote_account, - consensus_result: consensus_result_pda, - meta_merkle_proof: meta_merkle_proof_pda, - snapshot_program: SNAPSHOT_PROGRAM_ID, + ballot_box: ballot_box_pda, + program_config: program_config_pda, + ballot_program: SNAPSHOT_PROGRAM_ID, system_program: system_program::ID, }) .instructions()?; @@ -131,14 +72,18 @@ pub async fn support_proposal( blockhash, ); + // let transaction_data = + // base64::engine::general_purpose::STANDARD.encode(transaction.message_data()); + + // spinner.finish_with_message(format!( + // "Proposal supported base64 encoded transaction data: {:?}", + // transaction_data + // )); + let sig = program .rpc() .send_and_confirm_transaction(&transaction) .await?; - log::debug!( - "Support proposal transaction sent successfully: signature={}", - sig - ); spinner.finish_with_message(format!( "Proposal supported. https://explorer.solana.com/tx/{}", diff --git a/svmgov/src/main.rs b/svmgov/src/main.rs index 970cc8cc..2e3cc5d2 100644 --- a/svmgov/src/main.rs +++ b/svmgov/src/main.rs @@ -1,3 +1,4 @@ +mod config; mod constants; mod instructions; mod utils; @@ -6,8 +7,14 @@ use anchor_client::anchor_lang::declare_program; use anyhow::Result; use clap::{Parser, Subcommand}; +use config::Config; use constants::*; -use utils::{commands, utils::*}; +use utils::{ + commands, + config_command::{ConfigSubcommand, handle_config_command}, + init, + utils::*, +}; declare_program!(govcontract); @@ -77,17 +84,9 @@ enum Commands { #[arg(long, help = "GitHub link for the proposal description")] description: String, - /// Snapshot slot for fetching merkle proofs - #[arg(long, help = "Snapshot slot for fetching merkle proofs")] - snapshot_slot: u64, - /// Network for fetching merkle proofs #[arg(long, help = "Network for fetching merkle proofs")] network: String, - - /// Ballot ID for consensus result PDA derivation - #[arg(long, help = "Ballot ID")] - ballot_id: u64, }, #[command( @@ -102,14 +101,6 @@ enum Commands { #[arg(long, help = "Proposal ID")] proposal_id: String, - /// Ballot ID for consensus result PDA derivation - #[arg(long, help = "Ballot ID")] - ballot_id: u64, - - /// Snapshot slot for fetching merkle proofs - #[arg(long, help = "Snapshot slot for fetching merkle proofs")] - snapshot_slot: u64, - /// Network for fetching merkle proofs #[arg(long, help = "Network for fetching merkle proofs")] network: String, @@ -133,10 +124,6 @@ enum Commands { #[arg(long, help = "Proposal ID")] proposal_id: String, - /// Ballot ID for consensus result PDA derivation - #[arg(long, help = "Ballot ID")] - ballot_id: u64, - /// Basis points for 'For' vote. #[arg(long, help = "Basis points for 'For'")] for_votes: u64, @@ -149,10 +136,6 @@ enum Commands { #[arg(long, help = "Basis points for 'Abstain'")] abstain_votes: u64, - /// Snapshot slot for fetching merkle proofs - #[arg(long, help = "Snapshot slot for fetching merkle proofs")] - snapshot_slot: u64, - /// Network for fetching merkle proofs #[arg(long, help = "Network for fetching merkle proofs")] network: String, @@ -171,10 +154,6 @@ enum Commands { #[arg(long, help = "Proposal ID")] proposal_id: String, - /// Ballot ID for consensus result PDA derivation - #[arg(long, help = "Ballot ID")] - ballot_id: u64, - /// Basis points for 'For' vote. #[arg(long, help = "Basis points for 'For'")] for_votes: u64, @@ -187,10 +166,6 @@ enum Commands { #[arg(long, help = "Basis points for 'Abstain'")] abstain_votes: u64, - /// Snapshot slot for fetching merkle proofs - #[arg(long, help = "Snapshot slot for fetching merkle proofs")] - snapshot_slot: u64, - /// Network for fetching merkle proofs #[arg(long, help = "Network for fetching merkle proofs")] network: String, @@ -211,6 +186,45 @@ enum Commands { proposal_id: String, }, + #[command( + about = "Adjust proposal timing fields (author only)", + long_about = "This command allows the proposal author to adjust timing-related fields of a proposal. \ + Only the proposal author can call this command. All timing fields are optional - only provided fields will be updated. \ + The proposal must not be finalized.\n\n\ + Examples:\n\ + $ svmgov --identity-keypair /path/to/key.json adjust-proposal-timing --proposal-id \"123\" --start-epoch 100 --end-epoch 110\n\ + $ svmgov --identity-keypair /path/to/key.json adjust-proposal-timing --proposal-id \"123\" --snapshot-slot 5000000 --creation-timestamp 1234567890" + )] + AdjustProposalTiming { + /// Proposal ID to adjust timing for. + #[arg(long, help = "Proposal ID")] + proposal_id: String, + + /// Creation timestamp (Unix timestamp in seconds). + #[arg(long, help = "Creation timestamp (Unix timestamp in seconds)")] + creation_timestamp: Option<i64>, + + /// Creation epoch. + #[arg(long, help = "Creation epoch")] + creation_epoch: Option<u64>, + + /// Start epoch for voting. + #[arg(long, help = "Start epoch for voting")] + start_epoch: Option<u64>, + + /// End epoch for voting. + #[arg(long, help = "End epoch for voting")] + end_epoch: Option<u64>, + + /// Snapshot slot number. + #[arg(long, help = "Snapshot slot number")] + snapshot_slot: Option<u64>, + + /// Consensus result PDA pubkey (use "none" to clear). + #[arg(long, help = "Consensus result PDA pubkey (use \"none\" to clear)")] + consensus_result: Option<Option<String>>, + }, + #[command( about = "Display a proposal and it's details", long_about = "This command retrieves and displays a governance proposal and it's details from the Solana Validator Governance program. \ @@ -308,10 +322,6 @@ enum Commands { #[arg(long, help = "Proposal ID")] proposal_id: String, - /// Ballot ID for consensus result PDA derivation - #[arg(long, help = "Ballot ID")] - ballot_id: u64, - /// Basis points for 'For' vote #[arg( long, @@ -344,10 +354,6 @@ enum Commands { )] stake_account: String, - /// Snapshot slot for fetching merkle proofs - #[arg(long, help = "Snapshot slot for fetching merkle proofs")] - snapshot_slot: u64, - /// Network for fetching merkle proofs #[arg(long, help = "Network for fetching merkle proofs")] network: String, @@ -379,10 +385,6 @@ enum Commands { #[arg(long, help = "Proposal ID")] proposal_id: String, - /// Ballot ID for consensus result PDA derivation - #[arg(long, help = "Ballot ID")] - ballot_id: u64, - /// Basis points for 'For' vote #[arg( long, @@ -415,10 +417,6 @@ enum Commands { )] stake_account: String, - /// Snapshot slot for fetching merkle proofs - #[arg(long, help = "Snapshot slot for fetching merkle proofs")] - snapshot_slot: u64, - /// Network for fetching merkle proofs #[arg(long, help = "Network for fetching merkle proofs")] network: String, @@ -433,25 +431,58 @@ enum Commands { }, #[command( - about = "Add merkle root hash to a proposal for verification", - long_about = "This command adds a merkle root hash to a proposal for stake verification. \ - It requires the proposal ID and the merkle root hash as a hex string. \ - Only the original proposal author can call this command.\n\n\ + about = "Initialize the CLI configuration", + long_about = "This command sets up the initial configuration for svmgov CLI. \ + It will ask you whether you are a validator or staker, and prompt for \ + the appropriate keypair paths and network preferences.\n\n\ Example:\n\ - $ svmgov --identity-keypair /path/to/key.json add-merkle-root --proposal-id \"123\" --merkle-root \"0x1234567890abcdef...\"" + $ svmgov init" )] - AddMerkleRoot { - /// Proposal ID to add the merkle root to - #[arg(long, help = "Proposal ID")] - proposal_id: String, + Init, - /// Merkle root hash as a hex string - #[arg(long, help = "Merkle root hash (hex string)")] - merkle_root: String, + #[command( + about = "Manage CLI configuration", + long_about = "This command allows you to view and modify configuration settings. \ + Use 'config show' to view all settings, 'config get <key>' to get a specific \ + value, or 'config set <key> <value>' to set a value.\n\n\ + Examples:\n\ + $ svmgov config show\n\ + $ svmgov config set network testnet\n\ + $ svmgov config get rpc-url" + )] + Config { + #[command(subcommand)] + subcommand: ConfigSubcommand, }, } +fn merge_cli_with_config(cli: Cli, config: Config) -> Cli { + // Merge identity_keypair: CLI arg > config (based on user_type) > None + let identity_keypair = cli + .identity_keypair + .or_else(|| config.get_identity_keypair_path()); + + // Merge rpc_url: CLI arg > config rpc_url > config network default > constants default + let rpc_url = cli.rpc_url.or_else(|| { + if config.rpc_url.is_some() { + config.rpc_url.clone() + } else { + Some(config.get_rpc_url()) + } + }); + + Cli { + identity_keypair, + rpc_url, + command: cli.command, + } +} + async fn handle_command(cli: Cli) -> Result<()> { + // Load config and merge with CLI args + let config = Config::load().unwrap_or_default(); + let cli = merge_cli_with_config(cli, config); + log::debug!( "Handling command: identity_keypair={:?}, rpc_url={:?}, command={:?}", cli.identity_keypair, @@ -464,9 +495,7 @@ async fn handle_command(cli: Cli) -> Result<()> { seed, title, description, - snapshot_slot, network, - ballot_id, } => { instructions::create_proposal( title.to_string(), @@ -474,68 +503,54 @@ async fn handle_command(cli: Cli) -> Result<()> { *seed, cli.identity_keypair, cli.rpc_url, - snapshot_slot.clone(), network.clone(), - ballot_id.clone(), ) .await?; } Commands::SupportProposal { proposal_id, - ballot_id, - snapshot_slot, network, } => { instructions::support_proposal( proposal_id.to_string(), cli.identity_keypair, cli.rpc_url, - ballot_id.clone(), - snapshot_slot.clone(), network.clone(), ) .await?; } Commands::CastVote { proposal_id, - ballot_id, for_votes, against_votes, abstain_votes, - snapshot_slot, network, } => { instructions::cast_vote( proposal_id.to_string(), - ballot_id.clone(), *for_votes, *against_votes, *abstain_votes, cli.identity_keypair, cli.rpc_url, - snapshot_slot.clone(), network.clone(), ) .await?; } Commands::ModifyVote { proposal_id, - ballot_id, for_votes, against_votes, abstain_votes, - snapshot_slot, network, } => { instructions::modify_vote( proposal_id.to_string(), - ballot_id.clone(), *for_votes, *against_votes, *abstain_votes, cli.identity_keypair, cli.rpc_url, - snapshot_slot.clone(), network.clone(), ) .await?; @@ -548,6 +563,28 @@ async fn handle_command(cli: Cli) -> Result<()> { ) .await?; } + Commands::AdjustProposalTiming { + proposal_id, + creation_timestamp, + creation_epoch, + start_epoch, + end_epoch, + snapshot_slot, + consensus_result, + } => { + instructions::adjust_proposal_timing( + proposal_id.to_string(), + *creation_timestamp, + *creation_epoch, + *start_epoch, + *end_epoch, + *snapshot_slot, + consensus_result.clone(), + cli.identity_keypair, + cli.rpc_url, + ) + .await?; + } Commands::ListProposals { status, limit, @@ -579,20 +616,17 @@ async fn handle_command(cli: Cli) -> Result<()> { } Commands::CastVoteOverride { proposal_id, - ballot_id, for_votes, against_votes, abstain_votes, operator_api, stake_account, - snapshot_slot, network, staker_keypair, vote_account, } => { instructions::cast_vote_override( proposal_id.to_string(), - ballot_id.clone(), *for_votes, *against_votes, *abstain_votes, @@ -601,27 +635,23 @@ async fn handle_command(cli: Cli) -> Result<()> { operator_api.clone(), stake_account.clone(), vote_account.clone(), - snapshot_slot.clone(), network.clone(), ) .await?; } Commands::ModifyVoteOverride { proposal_id, - ballot_id, for_votes, against_votes, abstain_votes, operator_api, stake_account, - snapshot_slot, network, staker_keypair, vote_account, } => { instructions::modify_vote_override( proposal_id.to_string(), - ballot_id.clone(), *for_votes, *against_votes, *abstain_votes, @@ -630,22 +660,15 @@ async fn handle_command(cli: Cli) -> Result<()> { operator_api.clone(), stake_account.clone(), vote_account.clone(), - snapshot_slot.clone(), network.clone(), ) .await?; } - Commands::AddMerkleRoot { - proposal_id, - merkle_root, - } => { - instructions::add_merkle_root( - proposal_id.to_string(), - merkle_root.to_string(), - cli.identity_keypair, - cli.rpc_url, - ) - .await?; + Commands::Init => { + init::run_init().await?; + } + Commands::Config { subcommand } => { + handle_config_command(subcommand.clone()).await?; } } diff --git a/svmgov/src/utils/api_helpers.rs b/svmgov/src/utils/api_helpers.rs index ec5504b9..9d0417c9 100644 --- a/svmgov/src/utils/api_helpers.rs +++ b/svmgov/src/utils/api_helpers.rs @@ -105,7 +105,7 @@ pub async fn get_vote_account_proof( network: &str, ) -> Result<VoteAccountProofResponse> { let base_url = get_api_base_url(); - let mut url = format!( + let url = format!( "{}/proof/vote_account/{}?slot={}&network={}", base_url, vote_account, snapshot_slot, network ); @@ -152,10 +152,19 @@ pub async fn get_stake_account_proof( Ok(proof) } -/// Get the base API URL from environment or default +/// Get the base API URL from config, environment, or default fn get_api_base_url() -> String { dotenv::dotenv().ok(); + // Check config first + if let Ok(config) = crate::config::Config::load() { + if let Some(url) = config.operator_api_url { + info!("API base URL (from config): {}", url); + return url; + } + } + + // Fall back to environment variable or default let url = std::env::var(SVMGOV_OPERATOR_URL_ENV) .unwrap_or_else(|_| DEFAULT_OPERATOR_API_URL.to_string()); info!("API base URL: {}", url); @@ -282,8 +291,8 @@ pub fn convert_stake_merkle_leaf_data_to_idl_type( } /// Generate ConsensusResult PDA for a given snapshot slot -pub fn generate_consensus_result_pda(ballot_id: u64) -> Result<Pubkey> { - let (pda, _bump) = ConsensusResult::pda(ballot_id); +pub fn generate_consensus_result_pda(snapshot_slot: u64) -> Result<Pubkey> { + let (pda, _bump) = ConsensusResult::pda(snapshot_slot); Ok(pda) } @@ -298,10 +307,10 @@ pub fn generate_meta_merkle_proof_pda( /// Generate both ConsensusResult and MetaMerkleProof PDAs from VoteAccountProofResponse pub fn generate_pdas_from_vote_proof_response( - ballot_id: u64, + snapshot_slot: u64, response: &VoteAccountProofResponse, ) -> Result<(Pubkey, Pubkey)> { - let consensus_pda = generate_consensus_result_pda(ballot_id)?; + let consensus_pda = generate_consensus_result_pda(snapshot_slot)?; let vote_account = Pubkey::from_str(&response.meta_merkle_leaf.vote_account) .map_err(|e| anyhow!("Invalid vote_account pubkey in response: {}", e))?; let meta_proof = generate_meta_merkle_proof_pda(&consensus_pda, &vote_account)?; diff --git a/svmgov/src/utils/commands.rs b/svmgov/src/utils/commands.rs index 1babc5d2..d7a6526a 100644 --- a/svmgov/src/utils/commands.rs +++ b/svmgov/src/utils/commands.rs @@ -11,7 +11,7 @@ use log::info; use serde_json::{Value, json}; use crate::{ - anchor_client_setup, create_spinner, find_delegator_stake_accounts, + anchor_client_setup, create_spinner, govcontract::accounts::{Proposal, Vote}, }; @@ -73,9 +73,7 @@ pub async fn list_proposals( "creation_timestamp": proposal.creation_timestamp, "vote_count": proposal.vote_count, "index": proposal.index, - "merkle_root_hash": proposal.merkle_root_hash.map(|hash| - format!("0x{}", hex::encode(hash)) - ), + "consensus_result": proposal.consensus_result.map(|cr| cr.to_string()), "snapshot_slot": proposal.snapshot_slot, }) }) @@ -177,23 +175,3 @@ pub async fn get_proposal(rpc_url: Option<String>, proposal_id: &String) -> Resu Ok(()) } - -pub async fn list_stake_accounts(rpc_url: Option<String>, delegator_wallet: Pubkey) -> Result<()> { - // Create a mock Payer - let mock_payer = Arc::new(Keypair::new()); - - // Set up RPC client via anchor setup (consistent with other commands) - let program = anchor_client_setup(rpc_url, mock_payer)?; - let rpc_client = program.rpc(); - - // Fetch and log - let stakes = find_delegator_stake_accounts(&delegator_wallet, &rpc_client).await?; - for (stake_pk, vote_pk, active_stake) in stakes { - println!( - "Stake Account: {}, Vote Account: {}, Active Stake: {}", - stake_pk, vote_pk, active_stake - ); - } - - Ok(()) -} diff --git a/svmgov/src/utils/config_command.rs b/svmgov/src/utils/config_command.rs new file mode 100644 index 00000000..54ccad76 --- /dev/null +++ b/svmgov/src/utils/config_command.rs @@ -0,0 +1,125 @@ +use anyhow::{Result, anyhow}; + +use crate::config::{Config, UserType, get_default_rpc_url}; + +#[derive(clap::Subcommand, Debug, Clone)] +pub enum ConfigSubcommand { + /// Set a configuration value + Set { + /// Configuration key to set (network, rpc-url, operator-api-url, identity-keypair, staker-keypair) + key: String, + /// Value to set + value: String, + }, + /// Get a configuration value + Get { + /// Configuration key to get + key: String, + }, + /// Show all configuration values + Show, +} + +pub async fn handle_config_command(cmd: ConfigSubcommand) -> Result<()> { + match cmd { + ConfigSubcommand::Set { key, value } => handle_set(&key, &value).await, + ConfigSubcommand::Get { key } => handle_get(&key).await, + ConfigSubcommand::Show => handle_show().await, + } +} + +async fn handle_set(key: &str, value: &str) -> Result<()> { + let mut config = Config::load()?; + + match key.to_lowercase().as_str() { + "network" => { + let network_lower = value.to_lowercase(); + if network_lower != "mainnet" && network_lower != "testnet" { + return Err(anyhow!("Network must be either 'mainnet' or 'testnet'")); + } + config.network = network_lower.clone(); + // Update RPC URL to network default if not explicitly set + if config.rpc_url.is_none() { + config.rpc_url = Some(get_default_rpc_url(&network_lower)); + } + } + "rpc-url" => { + config.rpc_url = Some(value.to_string()); + } + "operator-api-url" => { + config.operator_api_url = Some(value.to_string()); + } + "identity-keypair" => { + if config.user_type != Some(UserType::Validator) { + return Err(anyhow!("identity-keypair is only valid for validators. Use 'staker-keypair' for stakers.")); + } + config.identity_keypair_path = Some(value.to_string()); + } + "staker-keypair" => { + if config.user_type != Some(UserType::Staker) { + return Err(anyhow!("staker-keypair is only valid for stakers. Use 'identity-keypair' for validators.")); + } + config.staker_keypair_path = Some(value.to_string()); + } + _ => { + return Err(anyhow!( + "Unknown config key: {}. Valid keys are: network, rpc-url, operator-api-url, identity-keypair, staker-keypair", + key + )); + } + } + + config.save()?; + println!("✓ Configuration updated: {} = {}", key, value); + Ok(()) +} + +async fn handle_get(key: &str) -> Result<()> { + let config = Config::load()?; + + let value = match key.to_lowercase().as_str() { + "network" => config.network, + "rpc-url" => config.get_rpc_url(), + "operator-api-url" => config.operator_api_url.unwrap_or_else(|| "not set".to_string()), + "identity-keypair" => config.identity_keypair_path.unwrap_or_else(|| "not set".to_string()), + "staker-keypair" => config.staker_keypair_path.unwrap_or_else(|| "not set".to_string()), + "user-type" => config.user_type.map(|u| u.to_string()).unwrap_or_else(|| "not set".to_string()), + _ => { + return Err(anyhow!( + "Unknown config key: {}. Valid keys are: network, rpc-url, operator-api-url, identity-keypair, staker-keypair, user-type", + key + )); + } + }; + + println!("{} = {}", key, value); + Ok(()) +} + +async fn handle_show() -> Result<()> { + let config = Config::load()?; + + println!("Current configuration:"); + println!(" user-type: {}", + config.user_type.as_ref().map(|u| u.to_string()).unwrap_or_else(|| "not set".to_string())); + + if let Some(path) = &config.identity_keypair_path { + println!(" identity-keypair: {}", path); + } + + if let Some(path) = &config.staker_keypair_path { + println!(" staker-keypair: {}", path); + } + + println!(" network: {}", config.network); + println!(" rpc-url: {}", config.get_rpc_url()); + + if let Some(url) = &config.operator_api_url { + println!(" operator-api-url: {}", url); + } else { + println!(" operator-api-url: not set (using default)"); + } + + Ok(()) +} + diff --git a/svmgov/src/utils/init.rs b/svmgov/src/utils/init.rs new file mode 100644 index 00000000..88f5e419 --- /dev/null +++ b/svmgov/src/utils/init.rs @@ -0,0 +1,100 @@ +use std::path::Path; + +use anyhow::{Result, anyhow}; +use inquire::{Select, Text}; + +use crate::config::{Config, UserType}; + +pub async fn run_init() -> Result<()> { + println!("Welcome to svmgov CLI setup!"); + println!(); + + let mut config = Config::load().unwrap_or_default(); + + // Ask user type + let user_type_options = vec!["Validator", "Staker"]; + let user_type_choice = Select::new( + "Are you a validator or staker?", + user_type_options.clone(), + ) + .prompt() + .map_err(|e| anyhow!("Failed to get user input: {}", e))?; + + let user_type = if user_type_choice == "Validator" { + UserType::Validator + } else { + UserType::Staker + }; + + config.user_type = Some(user_type.clone()); + + // Handle validator setup + if user_type == UserType::Validator { + let identity_path = Text::new("Enter the path to your validator identity keypair:") + .with_help_message("Path to the JSON keypair file") + .prompt() + .map_err(|e| anyhow!("Failed to get input: {}", e))?; + + let path = Path::new(&identity_path); + if !path.exists() { + return Err(anyhow!("The specified keypair file does not exist: {}", identity_path)); + } + + config.identity_keypair_path = Some(identity_path); + } else { + // Handle staker setup + let default_path = config.staker_keypair_path.clone().unwrap_or_else(|| { + dirs::home_dir() + .map(|h| h.join(".config").join("solana").join("id.json").to_string_lossy().to_string()) + .unwrap_or_else(|| "~/.config/solana/id.json".to_string()) + }); + + let prompt_msg = format!("Enter the path to your staker keypair (default: {}):", default_path); + let staker_path_input = Text::new(&prompt_msg) + .with_help_message("Path to the JSON keypair file") + .prompt() + .map_err(|e| anyhow!("Failed to get input: {}", e))?; + + let staker_path = if staker_path_input.trim().is_empty() { + default_path + } else { + staker_path_input + }; + + let path = Path::new(&staker_path); + if !path.exists() { + println!("Warning: The specified keypair file does not exist: {}", staker_path); + println!("You may need to create it or update the path later using 'svmgov config set staker-keypair <path>'"); + } + + config.staker_keypair_path = Some(staker_path); + } + + // Ask for network preference + let network_options = vec!["mainnet", "testnet"]; + let network_choice = Select::new( + "Which network do you want to use by default?", + network_options.clone(), + ) + .with_starting_cursor(0) + .prompt() + .map_err(|e| anyhow!("Failed to get input: {}", e))?; + + config.network = network_choice.to_string(); + + // Save config + config.save()?; + + println!(); + println!("✓ Configuration saved successfully!"); + println!(" User type: {}", user_type); + println!(" Network: {}", config.network); + if let Some(path) = config.get_identity_keypair_path() { + println!(" Keypair path: {}", path); + } + println!(); + println!("You can update your configuration anytime using 'svmgov config'"); + + Ok(()) +} + diff --git a/svmgov/src/utils/mod.rs b/svmgov/src/utils/mod.rs index e4ecf7af..488ef867 100644 --- a/svmgov/src/utils/mod.rs +++ b/svmgov/src/utils/mod.rs @@ -1,3 +1,5 @@ pub mod api_helpers; pub mod commands; +pub mod config_command; +pub mod init; pub mod utils; diff --git a/svmgov/src/utils/utils.rs b/svmgov/src/utils/utils.rs index e890d29d..e5d7cbf1 100644 --- a/svmgov/src/utils/utils.rs +++ b/svmgov/src/utils/utils.rs @@ -17,7 +17,7 @@ use anchor_client::{ }, }; use anchor_lang::{AnchorDeserialize, Id, prelude::Pubkey}; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use chrono::prelude::*; use indicatif::{ProgressBar, ProgressStyle}; use textwrap::wrap; @@ -266,59 +266,6 @@ pub async fn find_spl_vote_accounts( Ok(spl_vote_pubkeys) } -/// Returns stake pubkey + vote pubkey + validator pubkey + stake amount -pub(crate) async fn find_delegator_stake_accounts( - withdraw_authority: &Pubkey, - rpc_client: &RpcClient, -) -> Result<Vec<(Pubkey, Pubkey, u64)>> { - let filters = vec![ - RpcFilterType::DataSize(STAKE_ACCOUNT_DATA_SIZE), - RpcFilterType::Memcmp(Memcmp::new( - STAKE_ACCOUNT_WITHDRAW_AUTHORITY_OFFSET, - MemcmpEncodedBytes::Bytes(withdraw_authority.to_bytes().to_vec()), - )), - ]; - - let config = RpcProgramAccountsConfig { - filters: Some(filters), - account_config: RpcAccountInfoConfig { - encoding: Some(UiAccountEncoding::JsonParsed), - commitment: Some(CommitmentConfig { - commitment: CommitmentLevel::Finalized, - }), - ..RpcAccountInfoConfig::default() - }, - with_context: None, - sort_results: Some(true), - }; - - let accounts = rpc_client - .get_program_accounts_with_config(&stake::program::id(), config) - .await?; - - let mut stakes = vec![]; - - for (stake_pubkey, account) in accounts { - if let Ok(StakeStateV2::Stake(_meta, stake, _flags)) = - StakeStateV2::deserialize(&mut &account.data[..]) - { - if stake.delegation.stake > 0 && stake.delegation.deactivation_epoch == u64::MAX { - stakes.push(( - stake_pubkey, - stake.delegation.voter_pubkey, - stake.delegation.stake, - )); - } - } - } - - if stakes.is_empty() { - return Err(anyhow!("No active stake accounts found for this delegator")); - } - - Ok(stakes) -} - fn set_cluster(rpc_url: Option<String>) -> Cluster { if let Some(rpc_url) = rpc_url { let wss_url = rpc_url.replace("https://", "wss://"); @@ -403,17 +350,7 @@ impl fmt::Display for Proposal { "Finalized:", if self.finalized { "Yes" } else { "No" } )?; - if let Some(merkle_root) = self.merkle_root_hash { - writeln!( - f, - "{:<25} 0x{}", - "Merkle Root Hash:", - hex::encode(merkle_root) - )?; - } else { - writeln!(f, "{:<25} Not set", "Merkle Root Hash:")?; - } - writeln!(f, "{:<25} {}", "Snapshot Slot:", self.snapshot_slot)?; + writeln!(f, "{:<25}", "Description:")?; for line in wrapped_desc { writeln!(f, " {}", line)?; @@ -533,3 +470,19 @@ pub fn derive_vote_override_cache_pda( let (pda, _) = Pubkey::find_program_address(seeds, program_id); pda } +/// Derives the ProgramConfig PDA using the seeds [b"ProgramConfig"] +/// This matches the on-chain derivation in the support_proposal instruction. +pub fn derive_program_config_pda(ballot_program_id: &Pubkey) -> Pubkey { + let seeds = &[b"ProgramConfig".as_ref()]; + let (pda, _) = Pubkey::find_program_address(seeds, ballot_program_id); + pda +} + +pub fn get_epoch_slot_range(epoch: u64) -> (u64, u64) { + const SLOTS_PER_EPOCH: u64 = 432_000; + + let start_slot = epoch * SLOTS_PER_EPOCH; + let end_slot = (epoch + 1) * SLOTS_PER_EPOCH - 1; + + (start_slot, end_slot) +}