diff --git a/README.md b/README.md index d669dce5..b35a3f97 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ -# governance-program-library \ No newline at end of file +# governance-program-library + +### Test + +Unit tests contained within all projects can be run with: +```bash +$ cargo test # <-- runs host-based tests +$ cargo test-bpf # <-- runs BPF program tests +``` + +To run a specific program's tests, such as for NFT Voter Plugin: +```bash +$ cd programs/nft-voter +$ cargo test # <-- runs host-based tests +$ cargo test-bpf # <-- runs BPF program tests +``` + +To run a specific test, give the test name (doesnt include the file name) +```bash +$ cargo test test_create_governance_token_holding_account -- --exact # <-- runs host-based tests +$ cargo test-bpf test_create_governance_token_holding_account -- --exact # <-- runs BPF program tests +``` \ No newline at end of file diff --git a/programs/gateway/tests/program_test/program_test_bench.rs b/programs/gateway/tests/program_test/program_test_bench.rs index 3bb269fd..63213c3c 100644 --- a/programs/gateway/tests/program_test/program_test_bench.rs +++ b/programs/gateway/tests/program_test/program_test_bench.rs @@ -258,12 +258,17 @@ impl ProgramTestBench { #[allow(dead_code)] pub async fn with_wallet(&self) -> WalletCookie { let account_rent = self.rent.minimum_balance(0); + self.with_wallet_funded(account_rent).await + } + + #[allow(dead_code)] + pub async fn with_wallet_funded(&self, lamports: u64) -> WalletCookie { let account_keypair = Keypair::new(); let create_account_ix = system_instruction::create_account( &self.context.borrow().payer.pubkey(), &account_keypair.pubkey(), - account_rent, + lamports, 0, &system_program::id(), ); @@ -273,7 +278,7 @@ impl ProgramTestBench { .unwrap(); let account = Account { - lamports: account_rent, + lamports, data: vec![], owner: system_program::id(), executable: false, diff --git a/programs/nft-voter/src/error.rs b/programs/nft-voter/src/error.rs index f0abbaa3..2695a388 100644 --- a/programs/nft-voter/src/error.rs +++ b/programs/nft-voter/src/error.rs @@ -76,4 +76,16 @@ pub enum NftVoterError { #[msg("Cannot configure collection with voting proposals")] CannotConfigureCollectionWithVotingProposals, + + #[msg("NFT owner must withdraw all votes or voting period must end before you may withdraw tokens")] + CannotWithdrawTokensWithActiveVotes, + + #[msg("NFT must belong to a collection configured for the realm")] + InvalidNftCollection, + + #[msg("Invalid NFT voting power holding account token mint")] + InvalidHoldingAccountMint, + + #[msg("Provided NFT power holding account address doesnt match expected")] + InvalidHoldingAccountAddress, } diff --git a/programs/nft-voter/src/instructions/cast_nft_vote.rs b/programs/nft-voter/src/instructions/cast_nft_vote.rs index be2c6a7e..0446f4f1 100644 --- a/programs/nft-voter/src/instructions/cast_nft_vote.rs +++ b/programs/nft-voter/src/instructions/cast_nft_vote.rs @@ -71,7 +71,7 @@ pub fn cast_nft_vote<'a, 'b, 'c, 'info>( let rent = Rent::get()?; - for (nft_info, nft_metadata_info, nft_vote_record_info) in + for (nft_info, nft_metadata_info, nft_vote_record_info, nft_power_holding_account_info) in ctx.remaining_accounts.iter().tuples() { let (nft_vote_weight, nft_mint) = resolve_nft_vote_weight_and_mint( @@ -79,6 +79,7 @@ pub fn cast_nft_vote<'a, 'b, 'c, 'info>( &governing_token_owner, nft_info, nft_metadata_info, + nft_power_holding_account_info, &mut unique_nft_mints, )?; diff --git a/programs/nft-voter/src/instructions/create_governance_token_holding_account.rs b/programs/nft-voter/src/instructions/create_governance_token_holding_account.rs new file mode 100644 index 00000000..53b39ae7 --- /dev/null +++ b/programs/nft-voter/src/instructions/create_governance_token_holding_account.rs @@ -0,0 +1,101 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; + +use crate::{ + error::NftVoterError, + state::Registrar, + tools::{ + governance::NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, + token_metadata::get_token_metadata_for_mint, + }, +}; + +/// Creates a governance token holding account for a given NFT to boost its voting power +/// This instruction should only be executed once per realm/governing_token_mint/nft +/// to create the account +#[derive(Accounts)] +pub struct CreateGovernanceTokenHoldingAccount<'info> { + /// Associated fungible token account for the NFT being backed + #[account( + init, + seeds = [ &NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, + registrar.realm.as_ref(), + realm_governing_token_mint.key().as_ref(), + nft_mint.key().as_ref()], + bump, + payer = payer, + token::mint = realm_governing_token_mint, + token::authority = governance_program_id + )] + pub holding_account_info: Account<'info, TokenAccount>, + + /// The program id of the spl-governance program the realm belongs to + /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time + #[account(executable)] + pub governance_program_id: UncheckedAccount<'info>, + + pub registrar: Account<'info, Registrar>, + + /// Either the realm community mint or the council mint. + pub realm_governing_token_mint: Account<'info, Mint>, + + // pub realm_governing_token_mint: UncheckedAccount<'info>, + #[account(mut)] + pub payer: Signer<'info>, + + /// Mint of the NFT for which the holding account is being created + pub nft_mint: Account<'info, Mint>, + + /// Metadata of the NFT for which the holding account is being created. The + /// NFT must have a verified collection configured for the realm. + /// CHECK: metadata account cant be automatically deserialized by anchor + pub nft_metadata: UncheckedAccount<'info>, + + /// Associated token program that will own the holding account + pub associated_token_program: Program<'info, AssociatedToken>, + + /// Token program of the governance token mint + pub token_program: Program<'info, Token>, + + /// System program required for creating the holding account + pub system_program: Program<'info, System>, + + /// Rent required for creating the holding account + pub rent: Sysvar<'info, Rent>, +} + +/// Deposits tokens into the holding account for a given NFT to boost its voting power +pub fn create_governance_token_holding_account( + ctx: Context, +) -> Result<()> { + let registrar = &ctx.accounts.registrar; + let nft_mint = &ctx.accounts.nft_mint; + let nft_metadata = get_token_metadata_for_mint( + &ctx.accounts.nft_metadata.to_account_info(), + &nft_mint.key(), + )?; + + // The NFT must have a collection and the collection must be verified + let nft_collection = nft_metadata + .collection + .ok_or(NftVoterError::MissingMetadataCollection)?; + + require!( + nft_collection.verified, + NftVoterError::CollectionMustBeVerified + ); + + require!( + registrar + .collection_configs + .iter() + .map(|c| c.collection.key()) + .any(|c| c.key() == nft_collection.key), + NftVoterError::InvalidNftCollection + ); + + Ok(()) +} diff --git a/programs/nft-voter/src/instructions/deposit_governance_tokens.rs b/programs/nft-voter/src/instructions/deposit_governance_tokens.rs new file mode 100644 index 00000000..bb9acce6 --- /dev/null +++ b/programs/nft-voter/src/instructions/deposit_governance_tokens.rs @@ -0,0 +1,111 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token, TokenAccount}; +use spl_governance::state::realm; + +use crate::{ + state::delegator_token_owner_record::DelegatorTokenOwnerRecord, + tools::governance::NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, +}; + +/// Deposits tokens into the holding account for a given NFT to boost its voting power +#[derive(Accounts)] +pub struct DepositGovernanceTokens<'info> { + /// Record tracking what amount of the tokens in the holding + /// account belong to this delegator + #[account( + init_if_needed, + seeds = [&DelegatorTokenOwnerRecord::SEED_PREFIX, + realm.key().as_ref(), + realm_governing_token_mint.key().as_ref(), + nft_mint.key().as_ref(), + governing_token_owner.key().as_ref() + ], + bump, + payer = governing_token_owner, + space = DelegatorTokenOwnerRecord::SPACE + )] + pub token_owner_record: Account<'info, DelegatorTokenOwnerRecord>, + + /// Associated fungible token account for the NFT being backed + #[account( + mut, + seeds = [ &NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, + realm.key().as_ref(), + realm_governing_token_mint.key().as_ref(), + nft_mint.key().as_ref()], + bump, + token::mint = realm_governing_token_mint, + token::authority = governance_program_id + )] + pub holding_account_info: Account<'info, TokenAccount>, + + /// The program id of the spl-governance program the realm belongs to + /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time + #[account(executable)] + pub governance_program_id: UncheckedAccount<'info>, + + /// CHECK: Owned by spl-governance instance specified in governance_program_id + #[account(owner = governance_program_id.key())] + pub realm: UncheckedAccount<'info>, + + /// Either the realm community mint or the council mint. + pub realm_governing_token_mint: Account<'info, Mint>, + + /// Delegator, payer, and wallet that should receive the deposited tokens + /// upon withdrawal + #[account(mut)] + pub governing_token_owner: Signer<'info>, + + /// Mint of the NFT being backed. + // We dont need to check that the NFT has a collection or that the collection + // is one configured for the realm, because a) this already happened when + // creating the holding account, and b) we have a constraint here that the + // holding account's seeds include this mint + pub nft_mint: Account<'info, Mint>, + + /// Associated token account owned by governing_token_owner from which + /// tokens are being withdrawn for the deposit + #[account(mut, constraint = governing_token_source_account.owner == governing_token_owner.key())] + pub governing_token_source_account: Account<'info, TokenAccount>, + + /// System program required for creating the DelegatorTokenOwnerRecord + pub system_program: Program<'info, System>, + + /// Token program required for withdrawing (mutating) the source and holding accounts + pub token_program: Program<'info, Token>, +} + +/// Deposits tokens into the holding account for a given NFT to boost its voting power +pub fn deposit_governance_tokens(ctx: Context, amount: u64) -> Result<()> { + // Deserialize the Realm to validate it + let _realm = realm::get_realm_data_for_governing_token_mint( + &ctx.accounts.governance_program_id.key(), + &ctx.accounts.realm, + &ctx.accounts.realm_governing_token_mint.key(), + )?; + + spl_governance::tools::spl_token::transfer_spl_tokens( + &ctx.accounts + .governing_token_source_account + .to_account_info(), + &ctx.accounts.holding_account_info.to_account_info(), + &ctx.accounts.governing_token_owner.to_account_info(), + amount, + &ctx.accounts.realm_governing_token_mint.to_account_info(), + ) + .unwrap(); + + let token_owner_record = &mut ctx.accounts.token_owner_record; + token_owner_record.set_inner(DelegatorTokenOwnerRecord { + realm: ctx.accounts.realm.key(), + governing_token_mint: ctx.accounts.realm_governing_token_mint.key(), + nft_mint: ctx.accounts.nft_mint.key(), + governing_token_owner: ctx.accounts.governing_token_owner.key(), + governing_token_deposit_amount: token_owner_record + .governing_token_deposit_amount + .checked_add(amount) + .unwrap(), + }); + + Ok(()) +} diff --git a/programs/nft-voter/src/instructions/mod.rs b/programs/nft-voter/src/instructions/mod.rs index 518677c5..e59f6785 100644 --- a/programs/nft-voter/src/instructions/mod.rs +++ b/programs/nft-voter/src/instructions/mod.rs @@ -18,3 +18,12 @@ mod relinquish_nft_vote; pub use cast_nft_vote::*; mod cast_nft_vote; + +pub use create_governance_token_holding_account::*; +mod create_governance_token_holding_account; + +pub use deposit_governance_tokens::*; +mod deposit_governance_tokens; + +pub use withdraw_governance_tokens::*; +mod withdraw_governance_tokens; diff --git a/programs/nft-voter/src/instructions/update_voter_weight_record.rs b/programs/nft-voter/src/instructions/update_voter_weight_record.rs index 43145e2a..9637f543 100644 --- a/programs/nft-voter/src/instructions/update_voter_weight_record.rs +++ b/programs/nft-voter/src/instructions/update_voter_weight_record.rs @@ -49,12 +49,15 @@ pub fn update_voter_weight_record( // Ensure all nfts are unique let mut unique_nft_mints = vec![]; - for (nft_info, nft_metadata_info) in ctx.remaining_accounts.iter().tuples() { + for (nft_info, nft_metadata_info, nft_power_holding_account_info) in + ctx.remaining_accounts.iter().tuples() + { let (nft_vote_weight, _) = resolve_nft_vote_weight_and_mint( registrar, governing_token_owner, nft_info, nft_metadata_info, + nft_power_holding_account_info, &mut unique_nft_mints, )?; diff --git a/programs/nft-voter/src/instructions/withdraw_governance_tokens.rs b/programs/nft-voter/src/instructions/withdraw_governance_tokens.rs new file mode 100644 index 00000000..0b3162e0 --- /dev/null +++ b/programs/nft-voter/src/instructions/withdraw_governance_tokens.rs @@ -0,0 +1,100 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token, TokenAccount}; +use spl_governance::state::realm; + +use crate::{ + error::NftVoterError, state::delegator_token_owner_record::DelegatorTokenOwnerRecord, + tools::governance::NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, +}; + +/// Withdraws tokens from the holding account for a given NFT +#[derive(Accounts)] +#[instruction(amount: u64)] +pub struct WithdrawGovernanceTokens<'info> { + #[account( + mut, + seeds = [b"delegator-token-owner-record".as_ref(), + realm.key().as_ref(), + realm_governing_token_mint.key().as_ref(), + nft_mint.key().as_ref(), + governing_token_owner.key().as_ref() + ], + bump, + constraint = token_owner_record.governing_token_deposit_amount >= amount + )] + pub token_owner_record: Account<'info, DelegatorTokenOwnerRecord>, + + #[account( + mut, + seeds = [ &NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, + realm.key().as_ref(), + realm_governing_token_mint.key().as_ref(), + nft_mint.key().as_ref()], + bump, + token::mint = realm_governing_token_mint, + token::authority = governance_program_id, + constraint = holding_account_info.amount >= amount + )] + pub holding_account_info: Account<'info, TokenAccount>, + + /// The program id of the spl-governance program the realm belongs to + /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time + #[account(executable)] + pub governance_program_id: UncheckedAccount<'info>, + + /// CHECK: Owned by spl-governance instance specified in governance_program_id + #[account(owner = governance_program_id.key())] + pub realm: UncheckedAccount<'info>, + + /// Either the realm community mint or the council mint. + pub realm_governing_token_mint: Account<'info, Mint>, + + #[account(mut)] + pub governing_token_owner: Signer<'info>, + + //TODO add constraint that the nft is the one configured for a realm collection + pub nft_mint: Account<'info, Mint>, + + #[account(mut, constraint = governing_token_source_account.owner == governing_token_owner.key())] + pub governing_token_source_account: Account<'info, TokenAccount>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, +} + +/// Withdraws tokens from the holding account for a given NFT to boost its voting power +pub fn withdraw_governance_tokens( + ctx: Context, + amount: u64, +) -> Result<()> { + let realm = realm::get_realm_data_for_governing_token_mint( + &ctx.accounts.governance_program_id.key(), + &ctx.accounts.realm, + &ctx.accounts.realm_governing_token_mint.key(), + )?; + + //TODO check for proposal status (voted on, expired, etc) from NftUsageRecord + // TODO do all proposals have to have an expiration? + require!(false, NftVoterError::CannotWithdrawTokensWithActiveVotes); + + spl_governance::tools::spl_token::transfer_spl_tokens_signed( + &ctx.accounts.holding_account_info.to_account_info(), + &ctx.accounts + .governing_token_source_account + .to_account_info(), + &ctx.accounts.realm.to_account_info(), + &spl_governance::state::realm::get_realm_address_seeds(&realm.name), + &ctx.accounts.governance_program_id.key(), + amount, + &ctx.accounts.realm_governing_token_mint.to_account_info(), + ) + .unwrap(); + + let token_owner_record = &mut ctx.accounts.token_owner_record; + token_owner_record.governing_token_deposit_amount = token_owner_record + .governing_token_deposit_amount + .checked_sub(amount) + .unwrap(); + + Ok(()) +} diff --git a/programs/nft-voter/src/lib.rs b/programs/nft-voter/src/lib.rs index fcd118fe..782cf397 100644 --- a/programs/nft-voter/src/lib.rs +++ b/programs/nft-voter/src/lib.rs @@ -61,6 +61,29 @@ pub mod nft_voter { log_version(); instructions::cast_nft_vote(ctx, proposal) } + + pub fn create_governance_token_holding_account( + ctx: Context, + ) -> Result<()> { + log_version(); + instructions::create_governance_token_holding_account(ctx) + } + + pub fn deposit_governance_tokens( + ctx: Context, + amount: u64, + ) -> Result<()> { + log_version(); + instructions::deposit_governance_tokens(ctx, amount) + } + + pub fn withdraw_governance_tokens( + ctx: Context, + amount: u64, + ) -> Result<()> { + log_version(); + instructions::withdraw_governance_tokens(ctx, amount) + } } fn log_version() { diff --git a/programs/nft-voter/src/state/delegator_token_owner_record.rs b/programs/nft-voter/src/state/delegator_token_owner_record.rs new file mode 100644 index 00000000..7d4d8164 --- /dev/null +++ b/programs/nft-voter/src/state/delegator_token_owner_record.rs @@ -0,0 +1,103 @@ +use anchor_lang::prelude::*; + +use crate::{id, tools::anchor::DISCRIMINATOR_SIZE}; + +/// Receipt for depositing tokens against a voting NFT to enable +/// token owners to withdraw tokens from the NFT's holding account +#[account] +#[derive(Debug, Copy, PartialEq, Default)] +pub struct DelegatorTokenOwnerRecord { + /// The Realm the token owner is participating in + pub realm: Pubkey, + + /// Governing Token Mint the token owner holds deposit for + pub governing_token_mint: Pubkey, + + /// The mint of the NFT being backed + pub nft_mint: Pubkey, + + /// The owner (either single or multisig) of the deposited governing SPL Tokens + /// This is who can authorize a withdrawal of the tokens + pub governing_token_owner: Pubkey, + + /// The amount of governing tokens deposited into the NFT's holding account + /// This amount affects the voter weight used when voting on proposals + pub governing_token_deposit_amount: u64, +} + +impl DelegatorTokenOwnerRecord { + pub const SEED_PREFIX: [u8; 28] = *b"delegator-token-owner-record"; + + pub const SPACE: usize = DISCRIMINATOR_SIZE + + 32 + // realm + 32 + // governing token mint + 32 + // nft mint + 32 + // governing token owner + 8; // deposit amount + + pub fn make_seeds<'a>( + realm: &'a Pubkey, + governing_token_mint: &'a Pubkey, + nft_mint: &'a Pubkey, + governing_token_owner: &'a Pubkey, + ) -> [&'a [u8]; 5] { + [ + &DelegatorTokenOwnerRecord::SEED_PREFIX, + realm.as_ref(), + governing_token_mint.as_ref(), + nft_mint.as_ref(), + governing_token_owner.as_ref(), + ] + } + + pub fn get_seeds(&self) -> [&[u8]; 5] { + DelegatorTokenOwnerRecord::make_seeds( + &self.realm, + &self.governing_token_mint, + &self.nft_mint, + &self.governing_token_owner, + ) + } + + pub fn find_address( + realm: &Pubkey, + governing_token_mint: &Pubkey, + nft_mint: &Pubkey, + governing_token_owner: &Pubkey, + ) -> Pubkey { + Pubkey::find_program_address( + &DelegatorTokenOwnerRecord::make_seeds( + realm, + governing_token_mint, + nft_mint, + governing_token_owner, + ), + &id(), + ) + .0 + } +} + +#[cfg(test)] +mod test { + + use crate::tools::anchor::DISCRIMINATOR_SIZE; + + use super::*; + + #[test] + fn test_get_space() { + // Arrange + let expected_space = DelegatorTokenOwnerRecord::SPACE; + + // Act + let actual_space = DISCRIMINATOR_SIZE + + DelegatorTokenOwnerRecord::default() + .try_to_vec() + .unwrap() + .len(); + + // Assert + assert_eq!(expected_space, actual_space); + } +} diff --git a/programs/nft-voter/src/state/mod.rs b/programs/nft-voter/src/state/mod.rs index 4084a580..ee00661a 100644 --- a/programs/nft-voter/src/state/mod.rs +++ b/programs/nft-voter/src/state/mod.rs @@ -13,3 +13,6 @@ pub use voter_weight_record::*; pub mod voter_weight_record; pub mod idl_types; + +pub use delegator_token_owner_record::*; +pub mod delegator_token_owner_record; diff --git a/programs/nft-voter/src/state/registrar.rs b/programs/nft-voter/src/state/registrar.rs index a6479c57..e9be0c81 100644 --- a/programs/nft-voter/src/state/registrar.rs +++ b/programs/nft-voter/src/state/registrar.rs @@ -1,10 +1,12 @@ +use std::cmp::max; + use crate::{ error::NftVoterError, id, state::{CollectionConfig, VoterWeightRecord}, tools::{ - anchor::DISCRIMINATOR_SIZE, spl_token::get_spl_token_amount, - token_metadata::get_token_metadata_for_mint, + anchor::DISCRIMINATOR_SIZE, governance::find_nft_power_holding_account_address, + spl_token::get_spl_token_amount, token_metadata::get_token_metadata_for_mint, }, }; use anchor_lang::prelude::*; @@ -88,7 +90,7 @@ pub fn resolve_governing_token_owner( voter_token_owner_record.assert_token_owner_or_delegate_is_signer(voter_authority_info)?; // Assert voter TokenOwnerRecord and VoterWeightRecord are for the same governing_token_owner - require_eq!( + require_keys_eq!( voter_token_owner_record.governing_token_owner, voter_weight_record.governing_token_owner, NftVoterError::InvalidTokenOwnerForVoterWeightRecord @@ -103,9 +105,11 @@ pub fn resolve_nft_vote_weight_and_mint( governing_token_owner: &Pubkey, nft_info: &AccountInfo, nft_metadata_info: &AccountInfo, + nft_power_holding_account_info: &AccountInfo, unique_nft_mints: &mut Vec, ) -> Result<(u64, Pubkey)> { let nft_owner = get_spl_token_owner(nft_info)?; + let nft_mint = get_spl_token_mint(nft_info)?; // voter_weight_record.governing_token_owner must be the owner of the NFT require!( @@ -113,7 +117,30 @@ pub fn resolve_nft_vote_weight_and_mint( NftVoterError::VoterDoesNotOwnNft ); - let nft_mint = get_spl_token_mint(nft_info)?; + let expected_nft_power_holding_account_address = find_nft_power_holding_account_address( + ®istrar.realm, + ®istrar.governing_token_mint, + &nft_mint, + ); + + require_keys_eq!( + expected_nft_power_holding_account_address, + *nft_power_holding_account_info.key, + NftVoterError::InvalidHoldingAccountAddress + ); + + // For compatibility with NFT-based DAOs that dont use fungibles to boost the NFT's weight, + // we cant require the holding account to be initialized + let mut nft_power_holding_account_amount = 0; + if !nft_power_holding_account_info.data_is_empty() { + let nft_power_holding_account_mint = get_spl_token_mint(nft_power_holding_account_info)?; + require_keys_eq!( + nft_power_holding_account_mint, + registrar.governing_token_mint, + NftVoterError::InvalidHoldingAccountMint + ); + nft_power_holding_account_amount = get_spl_token_amount(nft_power_holding_account_info)?; + } // Ensure the same NFT was not provided more than once if unique_nft_mints.contains(&nft_mint) { @@ -137,7 +164,9 @@ pub fn resolve_nft_vote_weight_and_mint( let collection_config = registrar.get_collection_config(collection.key)?; - Ok((collection_config.weight, nft_mint)) + let nft_power = collection_config.weight * max(1, nft_power_holding_account_amount); + + Ok((nft_power, nft_mint)) } #[cfg(test)] diff --git a/programs/nft-voter/src/tools/governance.rs b/programs/nft-voter/src/tools/governance.rs index ccb25732..84449621 100644 --- a/programs/nft-voter/src/tools/governance.rs +++ b/programs/nft-voter/src/tools/governance.rs @@ -1,6 +1,40 @@ use anchor_lang::prelude::Pubkey; use spl_governance::state::{token_owner_record, vote_record}; +use crate::id; + +pub const NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX: [u8; 25] = *b"nft-power-holding-account"; + +pub fn find_nft_power_holding_account_address( + realm: &Pubkey, + governance_token_mint: &Pubkey, + nft_mint: &Pubkey, +) -> Pubkey { + Pubkey::find_program_address( + &[ + &NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, + realm.as_ref(), + governance_token_mint.as_ref(), + nft_mint.as_ref(), + ], + &id(), + ) + .0 +} + +pub fn make_nft_power_holding_account_seeds<'a>( + realm: &'a Pubkey, + governance_token_mint: &'a Pubkey, + nft_mint: &'a Pubkey, +) -> [&'a [u8]; 4] { + [ + &NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX, + realm.as_ref(), + governance_token_mint.as_ref(), + nft_mint.as_ref(), + ] +} + pub fn get_vote_record_address( program_id: &Pubkey, realm: &Pubkey, diff --git a/programs/nft-voter/tests/cast_nft_vote.rs b/programs/nft-voter/tests/cast_nft_vote.rs index 97e3934e..ad33eee8 100644 --- a/programs/nft-voter/tests/cast_nft_vote.rs +++ b/programs/nft-voter/tests/cast_nft_vote.rs @@ -20,23 +20,13 @@ async fn test_cast_nft_vote() -> Result<(), TransportError> { let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -58,7 +48,16 @@ async fn test_cast_nft_vote() -> Result<(), TransportError> { let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; nft_voter_test.bench.advance_clock().await; @@ -69,7 +68,10 @@ async fn test_cast_nft_vote() -> Result<(), TransportError> { .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -110,23 +112,13 @@ async fn test_cast_nft_vote_with_multiple_nfts() -> Result<(), TransportError> { let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -148,12 +140,30 @@ async fn test_cast_nft_vote_with_multiple_nfts() -> Result<(), TransportError> { let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; let nft_cookie2 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; nft_voter_test.bench.advance_clock().await; @@ -164,7 +174,10 @@ async fn test_cast_nft_vote_with_multiple_nfts() -> Result<(), TransportError> { .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -211,20 +224,13 @@ async fn test_cast_nft_vote_with_nft_already_voted_error() -> Result<(), Transpo let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - None, + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, ) .await?; @@ -246,14 +252,26 @@ async fn test_cast_nft_vote_with_nft_already_voted_error() -> Result<(), Transpo let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; nft_voter_test .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -270,7 +288,10 @@ async fn test_cast_nft_vote_with_nft_already_voted_error() -> Result<(), Transpo .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -294,20 +315,13 @@ async fn test_cast_nft_vote_with_invalid_voter_error() -> Result<(), TransportEr let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - None, + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, ) .await?; @@ -329,7 +343,16 @@ async fn test_cast_nft_vote_with_invalid_voter_error() -> Result<(), TransportEr let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; let voter_cookie2 = nft_voter_test.bench.with_wallet().await; @@ -340,7 +363,10 @@ async fn test_cast_nft_vote_with_invalid_voter_error() -> Result<(), TransportEr .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie2, &voter_token_owner_record_cookie, @@ -364,23 +390,13 @@ async fn test_cast_nft_vote_with_unverified_collection_error() -> Result<(), Tra let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -404,7 +420,12 @@ async fn test_cast_nft_vote_with_unverified_collection_error() -> Result<(), Tra let nft_cookie1 = nft_voter_test .token_metadata .with_nft_v2( - &nft_collection_cookie, + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), &voter_cookie, Some(CreateNftArgs { verify_collection: false, @@ -418,7 +439,10 @@ async fn test_cast_nft_vote_with_unverified_collection_error() -> Result<(), Tra .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -442,23 +466,13 @@ async fn test_cast_nft_vote_with_invalid_owner_error() -> Result<(), TransportEr let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -482,7 +496,16 @@ async fn test_cast_nft_vote_with_invalid_owner_error() -> Result<(), TransportEr let nft_cookie = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie2, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie2, + None, + ) .await?; // Act @@ -490,7 +513,10 @@ async fn test_cast_nft_vote_with_invalid_owner_error() -> Result<(), TransportEr .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -514,23 +540,13 @@ async fn test_cast_nft_vote_with_invalid_collection_error() -> Result<(), Transp let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -562,7 +578,10 @@ async fn test_cast_nft_vote_with_invalid_collection_error() -> Result<(), Transp .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -586,23 +605,13 @@ async fn test_cast_nft_vote_with_invalid_metadata_error() -> Result<(), Transpor let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -625,7 +634,12 @@ async fn test_cast_nft_vote_with_invalid_metadata_error() -> Result<(), Transpor let mut nft1_cookie = nft_voter_test .token_metadata .with_nft_v2( - &nft_collection_cookie, + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), &voter_cookie, Some(CreateNftArgs { verify_collection: false, @@ -636,7 +650,16 @@ async fn test_cast_nft_vote_with_invalid_metadata_error() -> Result<(), Transpor let nft2_cookie = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; // Try to use verified NFT Metadata @@ -647,7 +670,10 @@ async fn test_cast_nft_vote_with_invalid_metadata_error() -> Result<(), Transpor .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -671,20 +697,13 @@ async fn test_cast_nft_vote_with_same_nft_error() -> Result<(), TransportError> let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - None, + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, ) .await?; @@ -706,7 +725,16 @@ async fn test_cast_nft_vote_with_same_nft_error() -> Result<(), TransportError> let nft_cookie = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; // Act @@ -714,7 +742,10 @@ async fn test_cast_nft_vote_with_same_nft_error() -> Result<(), TransportError> .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -739,23 +770,13 @@ async fn test_cast_nft_vote_with_no_nft_error() -> Result<(), TransportError> { let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -778,7 +799,12 @@ async fn test_cast_nft_vote_with_no_nft_error() -> Result<(), TransportError> { let nft_cookie1 = nft_voter_test .token_metadata .with_nft_v2( - &nft_collection_cookie, + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), &voter_cookie, Some(CreateNftArgs { amount: 0, @@ -792,7 +818,10 @@ async fn test_cast_nft_vote_with_no_nft_error() -> Result<(), TransportError> { .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -816,23 +845,13 @@ async fn test_cast_nft_vote_with_max_5_nfts() -> Result<(), TransportError> { let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -858,7 +877,16 @@ async fn test_cast_nft_vote_with_max_5_nfts() -> Result<(), TransportError> { nft_voter_test.bench.advance_clock().await; let nft_cookie = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; nft_cookies.push(nft_cookie) @@ -872,7 +900,10 @@ async fn test_cast_nft_vote_with_max_5_nfts() -> Result<(), TransportError> { .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -919,23 +950,13 @@ async fn test_cast_nft_vote_using_multiple_instructions() -> Result<(), Transpor let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -957,7 +978,16 @@ async fn test_cast_nft_vote_using_multiple_instructions() -> Result<(), Transpor let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; nft_voter_test.bench.advance_clock().await; @@ -971,7 +1001,10 @@ async fn test_cast_nft_vote_using_multiple_instructions() -> Result<(), Transpor .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -982,7 +1015,16 @@ async fn test_cast_nft_vote_using_multiple_instructions() -> Result<(), Transpor let nft_cookie2 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; // Act @@ -991,7 +1033,10 @@ async fn test_cast_nft_vote_using_multiple_instructions() -> Result<(), Transpor .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -1028,23 +1073,13 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_nft_already_voted_e let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -1066,7 +1101,16 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_nft_already_voted_e let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; let args = CastNftVoteArgs { @@ -1077,7 +1121,10 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_nft_already_voted_e .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -1092,7 +1139,10 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_nft_already_voted_e .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -1117,23 +1167,13 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_attempted_sandwiche let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -1155,7 +1195,16 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_attempted_sandwiche let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; let args = CastNftVoteArgs { @@ -1167,7 +1216,10 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_attempted_sandwiche .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -1196,7 +1248,10 @@ async fn test_cast_nft_vote_using_multiple_instructions_with_attempted_sandwiche .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -1223,20 +1278,13 @@ async fn test_cast_nft_vote_using_delegate() -> Result<(), TransportError> { let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - None, + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, ) .await?; @@ -1258,7 +1306,16 @@ async fn test_cast_nft_vote_using_delegate() -> Result<(), TransportError> { let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; nft_voter_test.bench.advance_clock().await; @@ -1279,7 +1336,10 @@ async fn test_cast_nft_vote_using_delegate() -> Result<(), TransportError> { .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &delegate_cookie, &voter_token_owner_record_cookie, @@ -1306,20 +1366,13 @@ async fn test_cast_nft_vote_with_invalid_voter_weight_token_owner_error( let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - None, + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, ) .await?; @@ -1343,7 +1396,16 @@ async fn test_cast_nft_vote_with_invalid_voter_weight_token_owner_error( let nft_cookie1 = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) .await?; // Act @@ -1352,7 +1414,10 @@ async fn test_cast_nft_vote_with_invalid_voter_weight_token_owner_error( .cast_nft_vote( ®istrar_cookie, &voter_weight_record_cookie2, - &max_voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), &proposal_cookie, &voter_cookie, &voter_token_owner_record_cookie, @@ -1368,3 +1433,252 @@ async fn test_cast_nft_vote_with_invalid_voter_weight_token_owner_error( Ok(()) } + +#[tokio::test] +async fn test_cast_nft_vote_no_power_holding_account_ok() -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let voter_cookie = nft_voter_test.bench.with_wallet().await; + + let voter_token_owner_record_cookie = nft_voter_test + .governance + .with_token_owner_record(&realm_cookie, &voter_cookie) + .await?; + + let voter_weight_record_cookie = nft_voter_test + .with_voter_weight_record(®istrar_cookie, &voter_cookie) + .await?; + + let proposal_cookie = nft_voter_test + .governance + .with_proposal(&realm_cookie) + .await?; + + let nft_cookie1 = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) + .await?; + + nft_voter_test.bench.advance_clock().await; + + // Act + nft_voter_test + .cast_nft_vote( + ®istrar_cookie, + &voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), + &proposal_cookie, + &voter_cookie, + &voter_token_owner_record_cookie, + &[&nft_cookie1], + None, + ) + .await?; + + // Assert + let voter_weight_record = nft_voter_test + .get_voter_weight_record(&voter_weight_record_cookie.address) + .await; + + assert_eq!(voter_weight_record.voter_weight, 10); + + Ok(()) +} + +#[tokio::test] +async fn test_cast_nft_vote_power_holding_account_zero_balance_uses_to_collection_weight( +) -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let voter_cookie = nft_voter_test.bench.with_wallet().await; + + let voter_token_owner_record_cookie = nft_voter_test + .governance + .with_token_owner_record(&realm_cookie, &voter_cookie) + .await?; + + let voter_weight_record_cookie = nft_voter_test + .with_voter_weight_record(®istrar_cookie, &voter_cookie) + .await?; + + let proposal_cookie = nft_voter_test + .governance + .with_proposal(&realm_cookie) + .await?; + + let nft_cookie1 = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) + .await?; + + nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie1, None) + .await?; + + nft_voter_test.bench.advance_clock().await; + + // Act + nft_voter_test + .cast_nft_vote( + ®istrar_cookie, + &voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), + &proposal_cookie, + &voter_cookie, + &voter_token_owner_record_cookie, + &[&nft_cookie1], + None, + ) + .await?; + + // Assert + let voter_weight_record = nft_voter_test + .get_voter_weight_record(&voter_weight_record_cookie.address) + .await; + + assert_eq!(voter_weight_record.voter_weight, 10); + + Ok(()) +} + +#[tokio::test] +async fn test_cast_nft_vote_power_holding_account_nonzero_balance_uses_collection_weight_multiplied_balance( +) -> Result<(), TransportError> { + // Arrange + let collection_weight = 11u64; + let holding_account_balance = 33; + + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: collection_weight, + size: 20, + }, + ) + .await?; + + let voter_cookie = nft_voter_test.bench.with_wallet().await; + + let voter_token_owner_record_cookie = nft_voter_test + .governance + .with_token_owner_record(&realm_cookie, &voter_cookie) + .await?; + + let voter_weight_record_cookie = nft_voter_test + .with_voter_weight_record(®istrar_cookie, &voter_cookie) + .await?; + + let proposal_cookie = nft_voter_test + .governance + .with_proposal(&realm_cookie) + .await?; + + let nft_cookie1 = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) + .await?; + + nft_voter_test + .with_governance_token_holding_account( + ®istrar_cookie, + &realm_cookie, + &nft_cookie1, + Some(holding_account_balance), + ) + .await?; + + nft_voter_test.bench.advance_clock().await; + + // Act + nft_voter_test + .cast_nft_vote( + ®istrar_cookie, + &voter_weight_record_cookie, + ®istrar_cookie + .max_voter_weight_record_cookie + .as_ref() + .unwrap(), + &proposal_cookie, + &voter_cookie, + &voter_token_owner_record_cookie, + &[&nft_cookie1], + None, + ) + .await?; + + // Assert + let voter_weight_record = nft_voter_test + .get_voter_weight_record(&voter_weight_record_cookie.address) + .await; + + assert_eq!( + voter_weight_record.voter_weight, + collection_weight * holding_account_balance + ); + + Ok(()) +} diff --git a/programs/nft-voter/tests/create_governance_token_holding_account.rs b/programs/nft-voter/tests/create_governance_token_holding_account.rs new file mode 100644 index 00000000..95c7ff59 --- /dev/null +++ b/programs/nft-voter/tests/create_governance_token_holding_account.rs @@ -0,0 +1,145 @@ +use gpl_nft_voter::error::NftVoterError; +use program_test::nft_voter_test::{ConfigureCollectionArgs, NftVoterTest}; +use program_test::tools::assert_nft_voter_err; +use solana_program::program_option::COption; +use solana_program_test::*; +use solana_sdk::transport::TransportError; +use spl_token::state::AccountState; + +mod program_test; + +#[tokio::test] +async fn test_create_governance_token_holding_account() -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = &mut nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let voter_cookie = nft_voter_test.bench.with_wallet().await; + + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &voter_cookie, + None, + ) + .await?; + + // Act + let governance_token_holding_account_cookie = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await?; + + // Assert + assert_eq_formatted( + 0, + governance_token_holding_account_cookie.account.amount, + "amount", + ); + assert_eq_formatted( + COption::None, + governance_token_holding_account_cookie.account.delegate, + "delegate", + ); + assert_eq_formatted( + 0, + governance_token_holding_account_cookie + .account + .delegated_amount, + "delegated_amount", + ); + assert_eq_formatted( + COption::None, + governance_token_holding_account_cookie + .account + .close_authority, + "close_authority", + ); + assert_eq_formatted( + realm_cookie.community_mint_cookie.address, + governance_token_holding_account_cookie.account.mint, + "mint", + ); + assert_eq_formatted( + registrar_cookie.account.governance_program_id, + governance_token_holding_account_cookie.account.owner, + "owner", + ); + assert_eq_formatted( + AccountState::Initialized, + governance_token_holding_account_cookie.account.state, + "state", + ); + + Ok(()) +} + +#[tokio::test] +async fn test_create_governance_token_holding_account_nft_is_not_part_of_configured_collection_errors( +) -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = &mut nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let voter_cookie = nft_voter_test.bench.with_wallet().await; + + // create the NFT with a different collection not configured for th realm + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2( + &nft_voter_test.token_metadata.with_nft_collection().await?, + &voter_cookie, + None, + ) + .await?; + + // Act + let error = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await + .err(); + + // Assert + assert_nft_voter_err(error.unwrap(), NftVoterError::InvalidNftCollection); + + Ok(()) +} + +fn assert_eq_formatted( + expected: T, + actual: T, + name: &str, +) -> () { + assert_eq!( + expected, actual, + "{} not equal: expected {:?} but got {:?}", + name, expected, actual + ); +} diff --git a/programs/nft-voter/tests/create_registrar.rs b/programs/nft-voter/tests/create_registrar.rs index 29a9cc30..bb430008 100644 --- a/programs/nft-voter/tests/create_registrar.rs +++ b/programs/nft-voter/tests/create_registrar.rs @@ -63,6 +63,7 @@ async fn test_create_registrar_with_realm_authority_must_sign_error() -> Result< let err = nft_voter_test .with_registrar_using_ix( &realm_cookie, + None, |i| i.accounts[4].is_signer = false, // realm_authority Some(&[]), ) @@ -91,6 +92,7 @@ async fn test_create_registrar_with_invalid_spl_gov_program_id_error() -> Result let err = nft_voter_test .with_registrar_using_ix( &realm_cookie, + None, |i| i.accounts[1].pubkey = governance_program_id, //governance_program_id None, ) @@ -115,6 +117,7 @@ async fn test_create_registrar_with_invalid_realm_error() -> Result<(), Transpor let err = nft_voter_test .with_registrar_using_ix( &realm_cookie, + None, |i| i.accounts[2].pubkey = Pubkey::new_unique(), // realm None, ) @@ -143,6 +146,7 @@ async fn test_create_registrar_with_invalid_governing_token_mint_error( let err = nft_voter_test .with_registrar_using_ix( &realm_cookie, + None, |i| i.accounts[3].pubkey = mint_cookie.address, // governing_token_mint None, ) diff --git a/programs/nft-voter/tests/deposit_governance_tokens.rs b/programs/nft-voter/tests/deposit_governance_tokens.rs new file mode 100644 index 00000000..6b0ed527 --- /dev/null +++ b/programs/nft-voter/tests/deposit_governance_tokens.rs @@ -0,0 +1,453 @@ +use anchor_spl::token::TokenAccount; +use gpl_nft_voter::state::DelegatorTokenOwnerRecord; +use program_test::nft_voter_test::{ConfigureCollectionArgs, NftVoterTest}; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_deposit_governance_tokens_first_deposit_creates_record() -> Result<(), TransportError> +{ + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let owner_cookie = nft_voter_test.bench.with_wallet_funded(100000000000).await; + + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &owner_cookie, + None, + ) + .await?; + + let governance_token_holding_account_cookie = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await?; + + let governing_token_source_account_cookie = nft_voter_test + .bench + .with_tokens( + &realm_cookie.community_mint_cookie, + &owner_cookie.address, + 1000, + ) + .await?; + + // Act + let token_owner_record_cookie = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + None, + ) + .await?; + + // Assert + assert_eq_formatted( + 0, + token_owner_record_cookie + .account + .governing_token_deposit_amount, + "amount", + ); + assert_eq_formatted( + realm_cookie.community_mint_cookie.address, + token_owner_record_cookie.account.governing_token_mint, + "governing_token_mint", + ); + assert_eq_formatted( + owner_cookie.address, + token_owner_record_cookie.account.governing_token_owner, + "governing_token_owner", + ); + assert_eq_formatted( + nft_cookie.mint_cookie.address, + token_owner_record_cookie.account.nft_mint, + "nft_mint", + ); + assert_eq_formatted( + realm_cookie.address, + token_owner_record_cookie.account.realm, + "realm", + ); + + Ok(()) +} + +#[tokio::test] +async fn test_deposit_governance_tokens_record_exists_doesnt_error() -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let owner_cookie = nft_voter_test.bench.with_wallet_funded(100000000000).await; + + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &owner_cookie, + None, + ) + .await?; + + let governance_token_holding_account_cookie = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await?; + + let governing_token_source_account_cookie = nft_voter_test + .bench + .with_tokens( + &realm_cookie.community_mint_cookie, + &owner_cookie.address, + 1000, + ) + .await?; + + // Act + let first_result = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + None, + ) + .await; + + let second_result = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + None, + ) + .await; + + // Assert + assert!(!second_result.is_err()); + assert_eq_formatted( + first_result.unwrap().address, + second_result.unwrap().address, + "record address", + ); + + Ok(()) +} + +#[tokio::test] +async fn test_deposit_governance_tokens_transfers_tokens_to_holding_account( +) -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let owner_cookie = nft_voter_test.bench.with_wallet_funded(100000000000).await; + + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &owner_cookie, + None, + ) + .await?; + + let governance_token_holding_account_cookie = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await?; + + let source_tokens: u64 = 1000; + let deposit_tokens: u64 = ((source_tokens as f64) * 0.5) as u64; + + let governing_token_source_account_cookie = nft_voter_test + .bench + .with_tokens( + &realm_cookie.community_mint_cookie, + &owner_cookie.address, + source_tokens, + ) + .await?; + + // Act + let token_owner_record_cookie = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + Some(deposit_tokens), + ) + .await?; + + // Assert + assert_eq_formatted( + deposit_tokens, + token_owner_record_cookie + .account + .governing_token_deposit_amount, + "deposit amount", + ); + assert_eq_formatted( + source_tokens - deposit_tokens, + nft_voter_test + .bench + .get_anchor_account::(governing_token_source_account_cookie.address) + .await + .amount, + "source remaining amount", + ); + assert_eq_formatted( + deposit_tokens, + nft_voter_test + .bench + .get_anchor_account::(governance_token_holding_account_cookie.address) + .await + .amount, + "holding account amount", + ); + + Ok(()) +} + +#[tokio::test] +async fn test_deposit_governance_tokens_multiple_deposits_holding_account_stores_cumulative( +) -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let owner_cookie = nft_voter_test.bench.with_wallet_funded(100000000000).await; + + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &owner_cookie, + None, + ) + .await?; + + let governance_token_holding_account_cookie = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await?; + + let source_tokens: u64 = 1000; + let first_deposit_tokens: u64 = ((source_tokens as f64) * 0.25) as u64; + let second_deposit_tokens: u64 = ((source_tokens as f64) * 0.1) as u64; + let total_deposit: u64 = first_deposit_tokens + second_deposit_tokens; + + let governing_token_source_account_cookie = nft_voter_test + .bench + .with_tokens( + &realm_cookie.community_mint_cookie, + &owner_cookie.address, + source_tokens, + ) + .await?; + + // Act + let first_deposit_record_cookie = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + Some(first_deposit_tokens), + ) + .await?; + + let second_deposit_record_cookie = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + Some(second_deposit_tokens), + ) + .await?; + + // Assert + assert_eq_formatted( + total_deposit, + nft_voter_test + .bench + .get_anchor_account::(governance_token_holding_account_cookie.address) + .await + .amount, + "holding account amount", + ); + assert_eq_formatted( + first_deposit_record_cookie.address, + second_deposit_record_cookie.address, + "deposit record address", + ); + assert_eq_formatted( + total_deposit, + nft_voter_test + .bench + .get_anchor_account::(first_deposit_record_cookie.address) + .await + .governing_token_deposit_amount, + "record deposit", + ); + + Ok(()) +} + +#[tokio::test] +async fn test_deposit_governance_tokens_source_insufficient_balance_errors( +) -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; + + let owner_cookie = nft_voter_test.bench.with_wallet_funded(100000000000).await; + + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2( + ®istrar_cookie + .collection_cookies + .as_ref() + .unwrap() + .first() + .unwrap(), + &owner_cookie, + None, + ) + .await?; + + let governance_token_holding_account_cookie = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await?; + + let source_tokens: u64 = 1000; + let deposit_tokens: u64 = source_tokens + 1; + + let governing_token_source_account_cookie = nft_voter_test + .bench + .with_tokens( + &realm_cookie.community_mint_cookie, + &owner_cookie.address, + source_tokens, + ) + .await?; + + // Act + let result = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + Some(deposit_tokens), + ) + .await; + + // Assert + assert!(result.is_err()); + + Ok(()) +} + +fn assert_eq_formatted( + expected: T, + actual: T, + name: &str, +) -> () { + assert_eq!( + expected, actual, + "{} not equal: expected {:?} but got {:?}", + name, expected, actual + ); +} diff --git a/programs/nft-voter/tests/program_test/nft_voter_test.rs b/programs/nft-voter/tests/program_test/nft_voter_test.rs index 3e9f7dd6..1779ae47 100644 --- a/programs/nft-voter/tests/program_test/nft_voter_test.rs +++ b/programs/nft-voter/tests/program_test/nft_voter_test.rs @@ -1,11 +1,14 @@ +use std::str::FromStr; use std::sync::Arc; use anchor_lang::prelude::{AccountMeta, Pubkey}; +use anchor_spl::token::TokenAccount; use gpl_nft_voter::state::max_voter_weight_record::{ get_max_voter_weight_record_address, MaxVoterWeightRecord, }; use gpl_nft_voter::state::*; +use gpl_nft_voter::tools::governance::find_nft_power_holding_account_address; use solana_sdk::transport::TransportError; use spl_governance::instruction::cast_vote; use spl_governance::state::vote_record::{self, Vote, VoteChoice}; @@ -27,6 +30,8 @@ use crate::program_test::program_test_bench::WalletCookie; use crate::program_test::token_metadata_test::{NftCollectionCookie, NftCookie, TokenMetadataTest}; use crate::program_test::tools::NopOverride; +use super::program_test_bench::TokenAccountCookie; + #[derive(Debug, PartialEq)] pub struct RegistrarCookie { pub address: Pubkey, @@ -34,6 +39,12 @@ pub struct RegistrarCookie { pub realm_authority: Keypair, pub max_collections: u8, + + /// only populated when using with_registrar_with_collection + pub collection_cookies: Option>, + + /// only populated when using with_registrar_with_collection + pub max_voter_weight_record_cookie: Option, } pub struct VoterWeightRecordCookie { @@ -41,6 +52,7 @@ pub struct VoterWeightRecordCookie { pub account: VoterWeightRecord, } +#[derive(Debug, PartialEq)] pub struct MaxVoterWeightRecordCookie { pub address: Pubkey, pub account: MaxVoterWeightRecord, @@ -61,6 +73,17 @@ impl Default for ConfigureCollectionArgs { } } +pub struct GovernanceTokenHoldingAccountCookie { + pub address: Pubkey, + pub account: TokenAccount, +} + +#[derive(Debug, PartialEq)] +pub struct NftVoterTokenOwnerRecordCookie { + pub address: Pubkey, + pub account: DelegatorTokenOwnerRecord, +} + #[derive(Debug, PartialEq)] pub struct NftVoteRecordCookie { pub address: Pubkey, @@ -122,7 +145,17 @@ impl NftVoterTest { &mut self, realm_cookie: &RealmCookie, ) -> Result { - self.with_registrar_using_ix(realm_cookie, NopOverride, None) + self.with_registrar_using_ix(realm_cookie, None, NopOverride, None) + .await + } + + #[allow(dead_code)] + pub async fn with_registrar_with_collection( + &mut self, + realm_cookie: &RealmCookie, + collection_config: ConfigureCollectionArgs, + ) -> Result { + self.with_registrar_using_ix(realm_cookie, Some(collection_config), NopOverride, None) .await } @@ -130,6 +163,7 @@ impl NftVoterTest { pub async fn with_registrar_using_ix( &mut self, realm_cookie: &RealmCookie, + collection_config: Option, instruction_override: F, signers_override: Option<&[&Keypair]>, ) -> Result { @@ -168,9 +202,9 @@ impl NftVoterTest { let signers = signers_override.unwrap_or(default_signers); self.bench - .process_transaction(&[create_registrar_ix], Some(signers)) - .await?; - + .process_transaction(&[create_registrar_ix], Some(signers)) + .await?; + let account = Registrar { governance_program_id: self.governance.program_id, realm: realm_cookie.address, @@ -179,12 +213,35 @@ impl NftVoterTest { reserved: [0; 128], }; - Ok(RegistrarCookie { + let mut registrar_cookie = RegistrarCookie { address: registrar_key, account, realm_authority: realm_cookie.get_realm_authority(), max_collections, - }) + max_voter_weight_record_cookie: None, + collection_cookies: None, + }; + + if let Some(collection_config) = collection_config { + + let nft_collection_cookie = self.token_metadata.with_nft_collection().await?; + let max_voter_weight_record = + self.with_max_voter_weight_record(®istrar_cookie).await?; + + self.with_collection( + ®istrar_cookie, + &nft_collection_cookie, + &max_voter_weight_record, + Some(collection_config), + ) + .await?; + + registrar_cookie.account = self.get_registrar_account(®istrar_cookie.address).await; + registrar_cookie.max_voter_weight_record_cookie = Some(max_voter_weight_record); + registrar_cookie.collection_cookies = Some(vec![nft_collection_cookie]); + } + + Ok(registrar_cookie) } #[allow(dead_code)] @@ -343,6 +400,13 @@ impl NftVoterTest { for nft_cookie in nft_cookies { account_metas.push(AccountMeta::new_readonly(nft_cookie.address, false)); account_metas.push(AccountMeta::new_readonly(nft_cookie.metadata, false)); + + let nft_power_holding_account_key = find_nft_power_holding_account_address( + ®istrar_cookie.account.realm, + ®istrar_cookie.account.governing_token_mint, + &nft_cookie.mint_cookie.address, + ); + account_metas.push(AccountMeta::new(nft_power_holding_account_key, false)); } let instructions = vec![Instruction { @@ -514,6 +578,13 @@ impl NftVoterTest { ); account_metas.push(AccountMeta::new(nft_vote_record_key, false)); + let nft_power_holding_account_key = find_nft_power_holding_account_address( + ®istrar_cookie.account.realm, + ®istrar_cookie.account.governing_token_mint, + &nft_cookie.mint_cookie.address, + ); + account_metas.push(AccountMeta::new(nft_power_holding_account_key, false)); + let account = NftVoteRecord { proposal: proposal_cookie.address, nft_mint: nft_cookie.mint_cookie.address, @@ -594,4 +665,175 @@ impl NftVoterTest { pub async fn get_voter_weight_record(&self, voter_weight_record: &Pubkey) -> VoterWeightRecord { self.bench.get_anchor_account(*voter_weight_record).await } + + #[allow(dead_code)] + pub async fn with_governance_token_holding_account( + &self, + registrar_cookie: &RegistrarCookie, + realm_cookie: &RealmCookie, + nft_cookie: &NftCookie, + initial_amount: Option, + ) -> Result { + self.with_governance_token_holding_account_using_ix( + registrar_cookie, + realm_cookie, + nft_cookie, + initial_amount, + NopOverride, + ) + .await + } + + #[allow(dead_code)] + pub async fn with_governance_token_holding_account_using_ix( + &self, + registrar_cookie: &RegistrarCookie, + realm_cookie: &RealmCookie, + nft_cookie: &NftCookie, + initial_amount: Option, + instruction_override: F, + ) -> Result { + let holding_account_key = find_nft_power_holding_account_address( + ®istrar_cookie.account.realm, + ®istrar_cookie.account.governing_token_mint, + &nft_cookie.mint_cookie.address, + ); + + let data = anchor_lang::InstructionData::data( + &gpl_nft_voter::instruction::CreateGovernanceTokenHoldingAccount {}, + ); + + let accounts = gpl_nft_voter::accounts::CreateGovernanceTokenHoldingAccount { + holding_account_info: holding_account_key, + governance_program_id: self.governance.program_id, + registrar: registrar_cookie.address, + realm_governing_token_mint: registrar_cookie.account.governing_token_mint, + payer: self.bench.payer.pubkey(), + nft_mint: nft_cookie.mint_cookie.address, + nft_metadata: nft_cookie.metadata, + associated_token_program: anchor_spl::associated_token::ID, + token_program: spl_token::id(), + system_program: solana_sdk::system_program::id(), + rent: Pubkey::from_str("SysvarRent111111111111111111111111111111111") + .map_err(|e| TransportError::Custom(e.to_string()))?, + }; + + let mut create_governing_token_holding_account_ix = Instruction { + program_id: gpl_nft_voter::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas(&accounts, None), + data, + }; + + instruction_override(&mut create_governing_token_holding_account_ix); + + self.bench + .process_transaction( + &[create_governing_token_holding_account_ix], + Some(&[&self.bench.payer]), + ) + .await?; + + if let Some(initial_amount) = initial_amount { + self.bench + .mint_tokens( + &realm_cookie.community_mint_cookie.address, + &realm_cookie.community_mint_cookie.mint_authority, + &holding_account_key, + initial_amount, + ) + .await?; + } + + let account = self.bench.get_anchor_account(holding_account_key).await; + + Ok(GovernanceTokenHoldingAccountCookie { + address: holding_account_key, + account, + }) + } + + #[allow(dead_code)] + pub async fn with_nft_voter_token_owner_record( + &self, + realm_cookie: &RealmCookie, + nft_cookie: &NftCookie, + governing_token_holding_account_cookie: &GovernanceTokenHoldingAccountCookie, + governing_token_owner_cookie: &WalletCookie, + governing_token_source_cookie: &TokenAccountCookie, + deposit_amount: Option, + ) -> Result { + self.with_nft_voter_token_owner_record_using_ix( + realm_cookie, + nft_cookie, + governing_token_holding_account_cookie, + governing_token_owner_cookie, + governing_token_source_cookie, + deposit_amount, + NopOverride, + ) + .await + } + + #[allow(dead_code)] + pub async fn with_nft_voter_token_owner_record_using_ix( + &self, + realm_cookie: &RealmCookie, + nft_cookie: &NftCookie, + governing_token_holding_account_cookie: &GovernanceTokenHoldingAccountCookie, + governing_token_owner_cookie: &WalletCookie, + governing_token_source_cookie: &TokenAccountCookie, + deposit_amount: Option, + instruction_override: F, + ) -> Result { + let token_owner_record_pubkey = DelegatorTokenOwnerRecord::find_address( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &nft_cookie.mint_cookie.address, + &governing_token_owner_cookie.address, + ); + + let data = anchor_lang::InstructionData::data( + &gpl_nft_voter::instruction::DepositGovernanceTokens { + amount: deposit_amount.unwrap_or(0), + }, + ); + + let accounts = gpl_nft_voter::accounts::DepositGovernanceTokens { + token_owner_record: token_owner_record_pubkey, + holding_account_info: governing_token_holding_account_cookie.address, + governance_program_id: self.governance.program_id, + realm: realm_cookie.address, + realm_governing_token_mint: realm_cookie.community_mint_cookie.address, + governing_token_owner: governing_token_owner_cookie.address, + nft_mint: nft_cookie.mint_cookie.address, + governing_token_source_account: governing_token_source_cookie.address, + system_program: solana_sdk::system_program::id(), + token_program: spl_token::id(), + }; + + let mut deposit_governing_token_ix = Instruction { + program_id: gpl_nft_voter::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas(&accounts, None), + data, + }; + + instruction_override(&mut deposit_governing_token_ix); + + self.bench + .process_transaction( + &[deposit_governing_token_ix], + Some(&[&governing_token_owner_cookie.signer]), + ) + .await?; + + let account = self + .bench + .get_anchor_account(token_owner_record_pubkey) + .await; + + Ok(NftVoterTokenOwnerRecordCookie { + address: token_owner_record_pubkey, + account, + }) + } } diff --git a/programs/nft-voter/tests/program_test/program_test_bench.rs b/programs/nft-voter/tests/program_test/program_test_bench.rs index 150a6009..ba6b4425 100644 --- a/programs/nft-voter/tests/program_test/program_test_bench.rs +++ b/programs/nft-voter/tests/program_test/program_test_bench.rs @@ -255,12 +255,17 @@ impl ProgramTestBench { #[allow(dead_code)] pub async fn with_wallet(&self) -> WalletCookie { let account_rent = self.rent.minimum_balance(0); + self.with_wallet_funded(account_rent).await + } + + #[allow(dead_code)] + pub async fn with_wallet_funded(&self, lamports: u64) -> WalletCookie { let account_keypair = Keypair::new(); let create_account_ix = system_instruction::create_account( &self.context.borrow().payer.pubkey(), &account_keypair.pubkey(), - account_rent, + lamports, 0, &system_program::id(), ); @@ -270,7 +275,7 @@ impl ProgramTestBench { .unwrap(); let account = Account { - lamports: account_rent, + lamports, data: vec![], owner: system_program::id(), executable: false, diff --git a/programs/nft-voter/tests/program_test/token_metadata_test.rs b/programs/nft-voter/tests/program_test/token_metadata_test.rs index 3ac0f97f..6fa52caf 100644 --- a/programs/nft-voter/tests/program_test/token_metadata_test.rs +++ b/programs/nft-voter/tests/program_test/token_metadata_test.rs @@ -13,6 +13,7 @@ pub struct NftCookie { pub mint_cookie: MintCookie, } +#[derive(Debug, PartialEq)] pub struct NftCollectionCookie { pub mint: Pubkey, pub metadata: Pubkey, @@ -150,7 +151,7 @@ impl TokenMetadataTest { amount, } = args.unwrap_or_default(); - // Crate NFT + // Create NFT let mint_cookie = self.bench.with_mint().await?; let nft_account_cookie = self .bench diff --git a/programs/nft-voter/tests/update_voter_weight_record.rs b/programs/nft-voter/tests/update_voter_weight_record.rs index d29809a2..f792ec7f 100644 --- a/programs/nft-voter/tests/update_voter_weight_record.rs +++ b/programs/nft-voter/tests/update_voter_weight_record.rs @@ -16,25 +16,15 @@ async fn test_update_voter_weight_record() -> Result<(), TransportError> { let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - let _collection_config_cookie = nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { - weight: 10, - size: 20, - }), - ) - .await?; + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { + weight: 10, + size: 20, + }, + ) + .await?; let voter_cookie = nft_voter_test.bench.with_wallet().await; @@ -44,7 +34,7 @@ async fn test_update_voter_weight_record() -> Result<(), TransportError> { let nft1_cookie = nft_voter_test .token_metadata - .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) + .with_nft_v2(®istrar_cookie.collection_cookies.as_ref().unwrap().first().unwrap(), &voter_cookie, None) .await?; nft_voter_test.bench.advance_clock().await; @@ -518,23 +508,13 @@ async fn test_update_voter_weight_record_with_no_nft_error() -> Result<(), Trans let realm_cookie = nft_voter_test.governance.with_realm().await?; - let registrar_cookie = nft_voter_test.with_registrar(&realm_cookie).await?; - - let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; - - let max_voter_weight_record_cookie = nft_voter_test - .with_max_voter_weight_record(®istrar_cookie) - .await?; - - let _collection_config_cookie = nft_voter_test - .with_collection( - ®istrar_cookie, - &nft_collection_cookie, - &max_voter_weight_record_cookie, - Some(ConfigureCollectionArgs { + let registrar_cookie = nft_voter_test + .with_registrar_with_collection( + &realm_cookie, + ConfigureCollectionArgs { weight: 10, size: 20, - }), + }, ) .await?; @@ -547,7 +527,7 @@ async fn test_update_voter_weight_record_with_no_nft_error() -> Result<(), Trans let nft1_cookie = nft_voter_test .token_metadata .with_nft_v2( - &nft_collection_cookie, + ®istrar_cookie.collection_cookies.as_ref().unwrap().first().unwrap(), &voter_cookie, Some(CreateNftArgs { amount: 0, diff --git a/programs/nft-voter/tests/withdraw_governance_tokens.rs b/programs/nft-voter/tests/withdraw_governance_tokens.rs new file mode 100644 index 00000000..7ade8339 --- /dev/null +++ b/programs/nft-voter/tests/withdraw_governance_tokens.rs @@ -0,0 +1,134 @@ +use program_test::nft_voter_test::NftVoterTest; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_withdraw_governance_tokens_nothing_deposited_errors() -> Result<(), TransportError> { + // Arrange + let mut nft_voter_test = NftVoterTest::start_new().await; + + let realm_cookie = nft_voter_test.governance.with_realm().await?; + + let registrar_cookie = &mut nft_voter_test.with_registrar(&realm_cookie).await?; + + let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; + let max_voter_weight_record = nft_voter_test + .with_max_voter_weight_record(®istrar_cookie) + .await?; + + nft_voter_test + .with_collection( + ®istrar_cookie, + &nft_collection_cookie, + &max_voter_weight_record, + None, + ) + .await?; + + let registrar_updated = nft_voter_test + .get_registrar_account(®istrar_cookie.address) + .await; + + registrar_cookie.account = registrar_updated; + + let owner_cookie = nft_voter_test.bench.with_wallet_funded(100000000000).await; + + let nft_collection_cookie = nft_voter_test.token_metadata.with_nft_collection().await?; + + let nft_cookie = nft_voter_test + .token_metadata + .with_nft_v2(&nft_collection_cookie, &owner_cookie, None) + .await?; + + let governance_token_holding_account_cookie = nft_voter_test + .with_governance_token_holding_account(®istrar_cookie, &realm_cookie, &nft_cookie, None) + .await?; + + let governing_token_source_account_cookie = nft_voter_test + .bench + .with_tokens( + &realm_cookie.community_mint_cookie, + &owner_cookie.address, + 1000, + ) + .await?; + + // Act + let token_owner_record_cookie = nft_voter_test + .with_nft_voter_token_owner_record( + &realm_cookie, + &nft_cookie, + &governance_token_holding_account_cookie, + &owner_cookie, + &governing_token_source_account_cookie, + None, + ) + .await?; + + // Assert + assert_eq_formatted( + 0, + token_owner_record_cookie + .account + .governing_token_deposit_amount, + "amount", + ); + assert_eq_formatted( + realm_cookie.community_mint_cookie.address, + token_owner_record_cookie.account.governing_token_mint, + "governing_token_mint", + ); + assert_eq_formatted( + owner_cookie.address, + token_owner_record_cookie.account.governing_token_owner, + "governing_token_owner", + ); + assert_eq_formatted( + nft_cookie.mint_cookie.address, + token_owner_record_cookie.account.nft_mint, + "nft_mint", + ); + assert_eq_formatted( + realm_cookie.address, + token_owner_record_cookie.account.realm, + "realm", + ); + + Ok(()) +} + +#[tokio::test] +async fn test_withdraw_governance_tokens_try_withdraw_zero_errors() -> Result<(), TransportError> { + todo!() +} + +#[tokio::test] +async fn test_withdraw_governance_tokens_try_withdraw_more_than_deposited_errors( +) -> Result<(), TransportError> { + todo!() +} + +#[tokio::test] +async fn test_withdraw_governance_tokens_nft_has_open_votes_errors() -> Result<(), TransportError> { + todo!() +} + +#[tokio::test] +async fn test_withdraw_governance_tokens_nft_has_open_proposals_errors( +) -> Result<(), TransportError> { + todo!() +} + +fn assert_eq_formatted( + expected: T, + actual: T, + name: &str, +) -> () { + assert_eq!( + expected, actual, + "{} not equal: expected {:?} but got {:?}", + name, expected, actual + ); +} diff --git a/programs/realm-voter/tests/program_test/program_test_bench.rs b/programs/realm-voter/tests/program_test/program_test_bench.rs index 150a6009..ba6b4425 100644 --- a/programs/realm-voter/tests/program_test/program_test_bench.rs +++ b/programs/realm-voter/tests/program_test/program_test_bench.rs @@ -255,12 +255,17 @@ impl ProgramTestBench { #[allow(dead_code)] pub async fn with_wallet(&self) -> WalletCookie { let account_rent = self.rent.minimum_balance(0); + self.with_wallet_funded(account_rent).await + } + + #[allow(dead_code)] + pub async fn with_wallet_funded(&self, lamports: u64) -> WalletCookie { let account_keypair = Keypair::new(); let create_account_ix = system_instruction::create_account( &self.context.borrow().payer.pubkey(), &account_keypair.pubkey(), - account_rent, + lamports, 0, &system_program::id(), ); @@ -270,7 +275,7 @@ impl ProgramTestBench { .unwrap(); let account = Account { - lamports: account_rent, + lamports, data: vec![], owner: system_program::id(), executable: false, diff --git a/run-release.sh b/run-release.sh index 70a8c5f7..8057f889 100755 --- a/run-release.sh +++ b/run-release.sh @@ -24,7 +24,8 @@ anchor build cp ./target/types/nft_voter.ts src/nftVoter/nft_voter.ts cp ./target/types/gateway.ts src/gateway/gateway.ts cp ./target/types/realm_voter.ts src/realmVoter/realm_voter.ts -yarn clean && yarn build && yarn publish +yarn clean && yarn build +# yarn clean && yarn build && yarn publish echo echo Remember to commit and push the version update as well as the changes diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts index a55c5854..bc5bbe82 100644 --- a/src/gateway/gateway.ts +++ b/src/gateway/gateway.ts @@ -8,32 +8,63 @@ export type Gateway = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Gateway Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] }, { "name": "governingTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the Civic Pass plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "gatekeeperNetwork", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The Identity.com Gateway gatekeeper network that this realm uses", + "(See the registry struct docs for details).", + "Gateway Token belongs to this gatekeeper network, so passing a particular key here is", + "essentially saying \"We trust this gatekeeper network\"." + ] }, { "name": "payer", @@ -59,22 +90,41 @@ export type Gateway = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Gateway Plugin Registrar to be updated" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- realm_authority is realm.authority" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "gatekeeperNetwork", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The new Identity.com Gateway gatekeeper network", + "(See the registry struct docs for details).", + "Gateway Token belongs to this gatekeeper network, so passing a particular key here is", + "essentially saying \"We trust this gatekeeper network\"." + ] } ], "args": [ @@ -121,17 +171,28 @@ export type Gateway = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The Gateway Registrar" + ] }, { "name": "inputVoterWeight", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An account that is either of type TokenOwnerRecordV2 or VoterWeightRecord", + "depending on whether the registrar includes a predecessor or not" + ] }, { "name": "gatewayToken", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "A gateway token from the gatekeeper network in the registrar.", + "Proves that the holder is permitted to take an action." + ] }, { "name": "voterWeightRecord", @@ -145,33 +206,59 @@ export type Gateway = { "accounts": [ { "name": "registrar", + "docs": [ + "Registrar which stores Civic Pass voting configuration for the given Realm" + ], "type": { "kind": "struct", "fields": [ { "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], "type": "publicKey" }, { "name": "realm", + "docs": [ + "Realm of the Registrar" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is used the mint is only used as identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], "type": "publicKey" }, { "name": "gatekeeperNetwork", + "docs": [ + "The Gatekeeper Network represents the \"Pass Type\" that a", + "user must present." + ], "type": "publicKey" }, { "name": "previousVoterWeightPluginProgramId", + "docs": [ + "If the plugin is one in a sequence, this is the previous plugin program ID", + "If set, then update_voter_weight_record will expect a voter_weight_record owned by this program" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -184,33 +271,66 @@ export type Gateway = { }, { "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" }, { "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], "type": "u64" }, { "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], "type": { "option": { "defined": "VoterWeightAction" @@ -219,12 +339,21 @@ export type Gateway = { }, { "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -263,6 +392,10 @@ export type Gateway = { }, { "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], "type": { "kind": "enum", "variants": [ @@ -354,32 +487,63 @@ export const IDL: Gateway = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Gateway Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] }, { "name": "governingTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the Civic Pass plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "gatekeeperNetwork", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The Identity.com Gateway gatekeeper network that this realm uses", + "(See the registry struct docs for details).", + "Gateway Token belongs to this gatekeeper network, so passing a particular key here is", + "essentially saying \"We trust this gatekeeper network\"." + ] }, { "name": "payer", @@ -405,22 +569,41 @@ export const IDL: Gateway = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Gateway Plugin Registrar to be updated" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- realm_authority is realm.authority" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "gatekeeperNetwork", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The new Identity.com Gateway gatekeeper network", + "(See the registry struct docs for details).", + "Gateway Token belongs to this gatekeeper network, so passing a particular key here is", + "essentially saying \"We trust this gatekeeper network\"." + ] } ], "args": [ @@ -467,17 +650,28 @@ export const IDL: Gateway = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The Gateway Registrar" + ] }, { "name": "inputVoterWeight", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An account that is either of type TokenOwnerRecordV2 or VoterWeightRecord", + "depending on whether the registrar includes a predecessor or not" + ] }, { "name": "gatewayToken", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "A gateway token from the gatekeeper network in the registrar.", + "Proves that the holder is permitted to take an action." + ] }, { "name": "voterWeightRecord", @@ -491,33 +685,59 @@ export const IDL: Gateway = { "accounts": [ { "name": "registrar", + "docs": [ + "Registrar which stores Civic Pass voting configuration for the given Realm" + ], "type": { "kind": "struct", "fields": [ { "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], "type": "publicKey" }, { "name": "realm", + "docs": [ + "Realm of the Registrar" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is used the mint is only used as identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], "type": "publicKey" }, { "name": "gatekeeperNetwork", + "docs": [ + "The Gatekeeper Network represents the \"Pass Type\" that a", + "user must present." + ], "type": "publicKey" }, { "name": "previousVoterWeightPluginProgramId", + "docs": [ + "If the plugin is one in a sequence, this is the previous plugin program ID", + "If set, then update_voter_weight_record will expect a voter_weight_record owned by this program" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -530,33 +750,66 @@ export const IDL: Gateway = { }, { "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" }, { "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], "type": "u64" }, { "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], "type": { "option": { "defined": "VoterWeightAction" @@ -565,12 +818,21 @@ export const IDL: Gateway = { }, { "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -609,6 +871,10 @@ export const IDL: Gateway = { }, { "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], "type": { "kind": "enum", "variants": [ diff --git a/src/nftVoter/nft_voter.ts b/src/nftVoter/nft_voter.ts index a6e82b72..8d87093d 100644 --- a/src/nftVoter/nft_voter.ts +++ b/src/nftVoter/nft_voter.ts @@ -8,27 +8,52 @@ export type NftVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] }, { "name": "governingTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the NFT plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "payer", @@ -59,7 +84,10 @@ export type NftVoter = { { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", @@ -69,7 +97,10 @@ export type NftVoter = { { "name": "realmGoverningTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] }, { "name": "payer", @@ -100,7 +131,10 @@ export type NftVoter = { { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", @@ -110,7 +144,10 @@ export type NftVoter = { { "name": "realmGoverningTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] }, { "name": "payer", @@ -131,7 +168,10 @@ export type NftVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] }, { "name": "voterWeightRecord", @@ -154,7 +194,10 @@ export type NftVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] }, { "name": "voterWeightRecord", @@ -164,7 +207,10 @@ export type NftVoter = { { "name": "governance", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Governance account the Proposal is for" + ] }, { "name": "proposal", @@ -174,12 +220,20 @@ export type NftVoter = { { "name": "governingTokenOwner", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "The token owner who cast the original vote" + ] }, { "name": "voteRecord", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The account is used to validate that it doesn't exist and if it doesn't then Anchor owner check throws error", + "The check is disabled here and performed inside the instruction", + "#[account(owner = registrar.governance_program_id)]" + ] }, { "name": "beneficiary", @@ -195,7 +249,10 @@ export type NftVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "Registrar for which we configure this Collection" + ] }, { "name": "realm", @@ -205,7 +262,10 @@ export type NftVoter = { { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "Authority of the Realm must sign and match Realm.authority" + ] }, { "name": "collection", @@ -235,7 +295,10 @@ export type NftVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting registrar" + ] }, { "name": "voterWeightRecord", @@ -245,12 +308,18 @@ export type NftVoter = { { "name": "governingTokenOwner", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "The token owner who casts the vote" + ] }, { "name": "payer", "isMut": true, - "isSigner": true + "isSigner": true, + "docs": [ + "The account which pays for the transaction" + ] }, { "name": "systemProgram", @@ -264,24 +333,100 @@ export type NftVoter = { "type": "publicKey" } ] + }, + { + "name": "createGovernanceTokenHoldingAccount", + "accounts": [ + { + "name": "holdingAccountInfo", + "isMut": true, + "isSigner": false + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmGoverningTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "nft", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ { "name": "nftVoteRecord", + "docs": [ + "NftVoteRecord exported to IDL without account_discriminator", + "TODO: Once we can support these accounts in Anchor via remaining_accounts then it should be possible to remove it" + ], "type": { "kind": "struct", "fields": [ { "name": "proposal", + "docs": [ + "Proposal which was voted on" + ], "type": "publicKey" }, { "name": "nftMint", + "docs": [ + "The mint of the NFT which was used for the vote" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The voter who casted this vote", + "It's a Realm member pubkey corresponding to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" } ] @@ -289,29 +434,56 @@ export type NftVoter = { }, { "name": "maxVoterWeightRecord", + "docs": [ + "MaxVoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide max voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the MaxVoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the MaxVoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "maxVoterWeight", + "docs": [ + "Max voter weight", + "The max voter weight provided by the addin for the given realm and governing_token_mint" + ], "type": "u64" }, { "name": "maxVoterWeightExpiry", + "docs": [ + "The slot when the max voting weight expires", + "It should be set to None if the weight never expires", + "If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -324,23 +496,41 @@ export type NftVoter = { }, { "name": "registrar", + "docs": [ + "Registrar which stores NFT voting configuration for the given Realm" + ], "type": { "kind": "struct", "fields": [ { "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], "type": "publicKey" }, { "name": "realm", + "docs": [ + "Realm of the Registrar" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is used the mint is only used as identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], "type": "publicKey" }, { "name": "collectionConfigs", + "docs": [ + "MPL Collection used for voting" + ], "type": { "vec": { "defined": "CollectionConfig" @@ -349,6 +539,9 @@ export type NftVoter = { }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -361,33 +554,66 @@ export type NftVoter = { }, { "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" }, { "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], "type": "u64" }, { "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], "type": { "option": { "defined": "VoterWeightAction" @@ -396,12 +622,21 @@ export type NftVoter = { }, { "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -416,23 +651,43 @@ export type NftVoter = { "types": [ { "name": "CollectionConfig", + "docs": [ + "Configuration of an NFT collection used for governance power" + ], "type": { "kind": "struct", "fields": [ { "name": "collection", + "docs": [ + "The NFT collection used for governance" + ], "type": "publicKey" }, { "name": "size", + "docs": [ + "The size of the NFT collection used to calculate max voter weight", + "Note: At the moment the size is not captured on Metaplex accounts", + "and it has to be manually updated on the Registrar" + ], "type": "u32" }, { "name": "weight", + "docs": [ + "Governance power weight of the collection", + "Each NFT in the collection has governance power = 1 * weight", + "Note: The weight is scaled accordingly to the governing_token_mint decimals", + "Ex: if the the mint has 2 decimal places then weight of 1 should be stored as 100" + ], "type": "u64" }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -445,6 +700,10 @@ export type NftVoter = { }, { "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], "type": { "kind": "enum", "variants": [ @@ -606,27 +865,52 @@ export const IDL: NftVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] }, { "name": "governingTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the NFT plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "payer", @@ -657,7 +941,10 @@ export const IDL: NftVoter = { { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", @@ -667,7 +954,10 @@ export const IDL: NftVoter = { { "name": "realmGoverningTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] }, { "name": "payer", @@ -698,7 +988,10 @@ export const IDL: NftVoter = { { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", @@ -708,7 +1001,10 @@ export const IDL: NftVoter = { { "name": "realmGoverningTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] }, { "name": "payer", @@ -729,7 +1025,10 @@ export const IDL: NftVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] }, { "name": "voterWeightRecord", @@ -752,7 +1051,10 @@ export const IDL: NftVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting Registrar" + ] }, { "name": "voterWeightRecord", @@ -762,7 +1064,10 @@ export const IDL: NftVoter = { { "name": "governance", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Governance account the Proposal is for" + ] }, { "name": "proposal", @@ -772,12 +1077,20 @@ export const IDL: NftVoter = { { "name": "governingTokenOwner", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "The token owner who cast the original vote" + ] }, { "name": "voteRecord", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The account is used to validate that it doesn't exist and if it doesn't then Anchor owner check throws error", + "The check is disabled here and performed inside the instruction", + "#[account(owner = registrar.governance_program_id)]" + ] }, { "name": "beneficiary", @@ -793,7 +1106,10 @@ export const IDL: NftVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "Registrar for which we configure this Collection" + ] }, { "name": "realm", @@ -803,7 +1119,10 @@ export const IDL: NftVoter = { { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "Authority of the Realm must sign and match Realm.authority" + ] }, { "name": "collection", @@ -833,7 +1152,10 @@ export const IDL: NftVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The NFT voting registrar" + ] }, { "name": "voterWeightRecord", @@ -843,12 +1165,18 @@ export const IDL: NftVoter = { { "name": "governingTokenOwner", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "The token owner who casts the vote" + ] }, { "name": "payer", "isMut": true, - "isSigner": true + "isSigner": true, + "docs": [ + "The account which pays for the transaction" + ] }, { "name": "systemProgram", @@ -862,24 +1190,100 @@ export const IDL: NftVoter = { "type": "publicKey" } ] + }, + { + "name": "createGovernanceTokenHoldingAccount", + "accounts": [ + { + "name": "holdingAccountInfo", + "isMut": true, + "isSigner": false + }, + { + "name": "governanceProgramId", + "isMut": false, + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] + }, + { + "name": "realm", + "isMut": false, + "isSigner": false + }, + { + "name": "realmGoverningTokenMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "nft", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ { "name": "nftVoteRecord", + "docs": [ + "NftVoteRecord exported to IDL without account_discriminator", + "TODO: Once we can support these accounts in Anchor via remaining_accounts then it should be possible to remove it" + ], "type": { "kind": "struct", "fields": [ { "name": "proposal", + "docs": [ + "Proposal which was voted on" + ], "type": "publicKey" }, { "name": "nftMint", + "docs": [ + "The mint of the NFT which was used for the vote" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The voter who casted this vote", + "It's a Realm member pubkey corresponding to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" } ] @@ -887,29 +1291,56 @@ export const IDL: NftVoter = { }, { "name": "maxVoterWeightRecord", + "docs": [ + "MaxVoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide max voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the MaxVoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the MaxVoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "maxVoterWeight", + "docs": [ + "Max voter weight", + "The max voter weight provided by the addin for the given realm and governing_token_mint" + ], "type": "u64" }, { "name": "maxVoterWeightExpiry", + "docs": [ + "The slot when the max voting weight expires", + "It should be set to None if the weight never expires", + "If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -922,23 +1353,41 @@ export const IDL: NftVoter = { }, { "name": "registrar", + "docs": [ + "Registrar which stores NFT voting configuration for the given Realm" + ], "type": { "kind": "struct", "fields": [ { "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], "type": "publicKey" }, { "name": "realm", + "docs": [ + "Realm of the Registrar" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is used the mint is only used as identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], "type": "publicKey" }, { "name": "collectionConfigs", + "docs": [ + "MPL Collection used for voting" + ], "type": { "vec": { "defined": "CollectionConfig" @@ -947,6 +1396,9 @@ export const IDL: NftVoter = { }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -959,33 +1411,66 @@ export const IDL: NftVoter = { }, { "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" }, { "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], "type": "u64" }, { "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], "type": { "option": { "defined": "VoterWeightAction" @@ -994,12 +1479,21 @@ export const IDL: NftVoter = { }, { "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -1014,23 +1508,43 @@ export const IDL: NftVoter = { "types": [ { "name": "CollectionConfig", + "docs": [ + "Configuration of an NFT collection used for governance power" + ], "type": { "kind": "struct", "fields": [ { "name": "collection", + "docs": [ + "The NFT collection used for governance" + ], "type": "publicKey" }, { "name": "size", + "docs": [ + "The size of the NFT collection used to calculate max voter weight", + "Note: At the moment the size is not captured on Metaplex accounts", + "and it has to be manually updated on the Registrar" + ], "type": "u32" }, { "name": "weight", + "docs": [ + "Governance power weight of the collection", + "Each NFT in the collection has governance power = 1 * weight", + "Note: The weight is scaled accordingly to the governing_token_mint decimals", + "Ex: if the the mint has 2 decimal places then weight of 1 should be stored as 100" + ], "type": "u64" }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -1043,6 +1557,10 @@ export const IDL: NftVoter = { }, { "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], "type": { "kind": "enum", "variants": [ diff --git a/src/realmVoter/realm_voter.ts b/src/realmVoter/realm_voter.ts index 95119463..d7cdfca8 100644 --- a/src/realmVoter/realm_voter.ts +++ b/src/realmVoter/realm_voter.ts @@ -8,27 +8,52 @@ export type RealmVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Realm Voter Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] }, { "name": "governingTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "payer", @@ -111,7 +136,10 @@ export type RealmVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The RealmVoter voting Registrar" + ] }, { "name": "voterWeightRecord", @@ -121,7 +149,10 @@ export type RealmVoter = { { "name": "tokenOwnerRecord", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "TokenOwnerRecord for any of the configured spl-governance instances" + ] } ], "args": [] @@ -132,7 +163,10 @@ export type RealmVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Registrar for the given realm and governing_token_mint" + ] }, { "name": "realm", @@ -142,12 +176,18 @@ export type RealmVoter = { { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "Authority of the Realm must sign and match realm.authority" + ] }, { "name": "maxVoterWeightRecord", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "MaxVoterWeightRecord for the given registrar.realm and registrar.governing_token_mint" + ] } ], "args": [ @@ -167,7 +207,10 @@ export type RealmVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "Registrar which we configure the provided spl-governance instance for" + ] }, { "name": "realm", @@ -177,12 +220,19 @@ export type RealmVoter = { { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "Authority of the Realm must sign the transaction and must match realm.authority" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The onus is entirely on the caller side to ensure the provided instance is correct", + "In future versions once we have the registry of spl-governance instances it could be validated against the registry" + ] } ], "args": [ @@ -198,29 +248,56 @@ export type RealmVoter = { "accounts": [ { "name": "maxVoterWeightRecord", + "docs": [ + "MaxVoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide max voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the MaxVoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the MaxVoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "maxVoterWeight", + "docs": [ + "Max voter weight", + "The max voter weight provided by the addin for the given realm and governing_token_mint" + ], "type": "u64" }, { "name": "maxVoterWeightExpiry", + "docs": [ + "The slot when the max voting weight expires", + "It should be set to None if the weight never expires", + "If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -233,23 +310,43 @@ export type RealmVoter = { }, { "name": "registrar", + "docs": [ + "Registrar which stores spl-governance configurations for the given Realm" + ], "type": { "kind": "struct", "fields": [ { "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], "type": "publicKey" }, { "name": "realm", + "docs": [ + "Realm of the Registrar" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is enabled the mint is only used as the identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], "type": "publicKey" }, { "name": "governanceProgramConfigs", + "docs": [ + "spl-governance instances used for governance power", + "Any DAO member of any DAO created using the configured spl-governances would be given 1 vote", + "TODO: Once we have on-chain spl-governance registry this configuration won't be needed any longer" + ], "type": { "vec": { "defined": "GovernanceProgramConfig" @@ -258,14 +355,26 @@ export type RealmVoter = { }, { "name": "realmMemberVoterWeight", + "docs": [ + "Vote weight assigned to a member of any of the Realms from the configured spl-governances" + ], "type": "u64" }, { "name": "maxVoterWeight", + "docs": [ + "Max voter weight (expressed in governing_token_mint decimal units) is used to establish the theoretical Max Attendance Quorum which is then used to calculate Approval Quorum", + "This manual configuration is a rough estimate because it's not practical to calculate on-chain the number of all DAO members for the given spl-governance instances", + "", + "Note: This is not a security vulnerability because the plugin is inherently not secure and used only to encourage DAO usage and registration of spl-governance instances" + ], "type": "u64" }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -278,33 +387,66 @@ export type RealmVoter = { }, { "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" }, { "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], "type": "u64" }, { "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], "type": { "option": { "defined": "VoterWeightAction" @@ -313,12 +455,21 @@ export type RealmVoter = { }, { "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -333,15 +484,24 @@ export type RealmVoter = { "types": [ { "name": "GovernanceProgramConfig", + "docs": [ + "Configuration of an spl-governance instance used to grant governance power" + ], "type": { "kind": "struct", "fields": [ { "name": "programId", + "docs": [ + "The program id of the configured spl-governance instance" + ], "type": "publicKey" }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -354,6 +514,9 @@ export type RealmVoter = { }, { "name": "CollectionItemChangeType", + "docs": [ + "Enum defining collection item change type" + ], "type": { "kind": "enum", "variants": [ @@ -368,6 +531,10 @@ export type RealmVoter = { }, { "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], "type": { "kind": "enum", "variants": [ @@ -439,27 +606,52 @@ export const IDL: RealmVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Realm Voter Registrar", + "There can only be a single registrar per governance Realm and governing mint of the Realm" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The program id of the spl-governance program the realm belongs to" + ] }, { "name": "realm", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "An spl-governance Realm", + "", + "Realm is validated in the instruction:", + "- Realm is owned by the governance_program_id", + "- governing_token_mint must be the community or council mint", + "- realm_authority is realm.authority" + ] }, { "name": "governingTokenMint", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Either the realm community mint or the council mint.", + "It must match Realm.community_mint or Realm.config.council_mint", + "", + "Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity", + "for the voting population and the tokens of that are no longer used" + ] }, { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "realm_authority must sign and match Realm.authority" + ] }, { "name": "payer", @@ -542,7 +734,10 @@ export const IDL: RealmVoter = { { "name": "registrar", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The RealmVoter voting Registrar" + ] }, { "name": "voterWeightRecord", @@ -552,7 +747,10 @@ export const IDL: RealmVoter = { { "name": "tokenOwnerRecord", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "TokenOwnerRecord for any of the configured spl-governance instances" + ] } ], "args": [] @@ -563,7 +761,10 @@ export const IDL: RealmVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The Registrar for the given realm and governing_token_mint" + ] }, { "name": "realm", @@ -573,12 +774,18 @@ export const IDL: RealmVoter = { { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "Authority of the Realm must sign and match realm.authority" + ] }, { "name": "maxVoterWeightRecord", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "MaxVoterWeightRecord for the given registrar.realm and registrar.governing_token_mint" + ] } ], "args": [ @@ -598,7 +805,10 @@ export const IDL: RealmVoter = { { "name": "registrar", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "Registrar which we configure the provided spl-governance instance for" + ] }, { "name": "realm", @@ -608,12 +818,19 @@ export const IDL: RealmVoter = { { "name": "realmAuthority", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "Authority of the Realm must sign the transaction and must match realm.authority" + ] }, { "name": "governanceProgramId", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "The onus is entirely on the caller side to ensure the provided instance is correct", + "In future versions once we have the registry of spl-governance instances it could be validated against the registry" + ] } ], "args": [ @@ -629,29 +846,56 @@ export const IDL: RealmVoter = { "accounts": [ { "name": "maxVoterWeightRecord", + "docs": [ + "MaxVoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide max voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the MaxVoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the MaxVoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "maxVoterWeight", + "docs": [ + "Max voter weight", + "The max voter weight provided by the addin for the given realm and governing_token_mint" + ], "type": "u64" }, { "name": "maxVoterWeightExpiry", + "docs": [ + "The slot when the max voting weight expires", + "It should be set to None if the weight never expires", + "If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -664,23 +908,43 @@ export const IDL: RealmVoter = { }, { "name": "registrar", + "docs": [ + "Registrar which stores spl-governance configurations for the given Realm" + ], "type": { "kind": "struct", "fields": [ { "name": "governanceProgramId", + "docs": [ + "spl-governance program the Realm belongs to" + ], "type": "publicKey" }, { "name": "realm", + "docs": [ + "Realm of the Registrar" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing token mint the Registrar is for", + "It can either be the Community or the Council mint of the Realm", + "When the plugin is enabled the mint is only used as the identity of the governing power (voting population)", + "and the actual token of the mint is not used" + ], "type": "publicKey" }, { "name": "governanceProgramConfigs", + "docs": [ + "spl-governance instances used for governance power", + "Any DAO member of any DAO created using the configured spl-governances would be given 1 vote", + "TODO: Once we have on-chain spl-governance registry this configuration won't be needed any longer" + ], "type": { "vec": { "defined": "GovernanceProgramConfig" @@ -689,14 +953,26 @@ export const IDL: RealmVoter = { }, { "name": "realmMemberVoterWeight", + "docs": [ + "Vote weight assigned to a member of any of the Realms from the configured spl-governances" + ], "type": "u64" }, { "name": "maxVoterWeight", + "docs": [ + "Max voter weight (expressed in governing_token_mint decimal units) is used to establish the theoretical Max Attendance Quorum which is then used to calculate Approval Quorum", + "This manual configuration is a rough estimate because it's not practical to calculate on-chain the number of all DAO members for the given spl-governance instances", + "", + "Note: This is not a security vulnerability because the plugin is inherently not secure and used only to encourage DAO usage and registration of spl-governance instances" + ], "type": "u64" }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -709,33 +985,66 @@ export const IDL: RealmVoter = { }, { "name": "voterWeightRecord", + "docs": [ + "VoterWeightRecord account as defined in spl-governance-addin-api", + "It's redefined here without account_discriminator for Anchor to treat it as native account", + "", + "The account is used as an api interface to provide voting power to the governance program from external addin contracts" + ], "type": { "kind": "struct", "fields": [ { "name": "realm", + "docs": [ + "The Realm the VoterWeightRecord belongs to" + ], "type": "publicKey" }, { "name": "governingTokenMint", + "docs": [ + "Governing Token Mint the VoterWeightRecord is associated with", + "Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only" + ], "type": "publicKey" }, { "name": "governingTokenOwner", + "docs": [ + "The owner of the governing token and voter", + "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" + ], "type": "publicKey" }, { "name": "voterWeight", + "docs": [ + "Voter's weight", + "The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter)" + ], "type": "u64" }, { "name": "voterWeightExpiry", + "docs": [ + "The slot when the voting weight expires", + "It should be set to None if the weight never expires", + "If the voter weight decays with time, for example for time locked based weights, then the expiry must be set", + "As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction", + "and the expiry set to the current slot to provide up to date weight" + ], "type": { "option": "u64" } }, { "name": "weightAction", + "docs": [ + "The governance action the voter's weight pertains to", + "It allows to provided voter's weight specific to the particular action the weight is evaluated for", + "When the action is provided then the governance program asserts the executing action is the same as specified by the addin" + ], "type": { "option": { "defined": "VoterWeightAction" @@ -744,12 +1053,21 @@ export const IDL: RealmVoter = { }, { "name": "weightActionTarget", + "docs": [ + "The target the voter's weight action pertains to", + "It allows to provided voter's weight specific to the target the weight is evaluated for", + "For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target", + "When the target is provided then the governance program asserts the target is the same as specified by the addin" + ], "type": { "option": "publicKey" } }, { "name": "reserved", + "docs": [ + "Reserved space for future versions" + ], "type": { "array": [ "u8", @@ -764,15 +1082,24 @@ export const IDL: RealmVoter = { "types": [ { "name": "GovernanceProgramConfig", + "docs": [ + "Configuration of an spl-governance instance used to grant governance power" + ], "type": { "kind": "struct", "fields": [ { "name": "programId", + "docs": [ + "The program id of the configured spl-governance instance" + ], "type": "publicKey" }, { "name": "reserved", + "docs": [ + "Reserved for future upgrades" + ], "type": { "array": [ "u8", @@ -785,6 +1112,9 @@ export const IDL: RealmVoter = { }, { "name": "CollectionItemChangeType", + "docs": [ + "Enum defining collection item change type" + ], "type": { "kind": "enum", "variants": [ @@ -799,6 +1129,10 @@ export const IDL: RealmVoter = { }, { "name": "VoterWeightAction", + "docs": [ + "VoterWeightAction enum as defined in spl-governance-addin-api", + "It's redefined here for Anchor to export it to IDL" + ], "type": { "kind": "enum", "variants": [