Governance: Implement Realms
Implemented instructions: - CreateRealm - DepositGoverningTokens - WithdrawGoverningTokens -SetVoteAuthority Co-authored-by: Jon Cinque <jon.cinque@gmail.com> Co-authored-by: Michael Vines <mvines@gmail.com>
This commit is contained in:
parent
79f31e320f
commit
addc6bf4b4
|
@ -3813,12 +3813,18 @@ dependencies = [
|
|||
name = "spl-governance"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"assert_matches",
|
||||
"bincode",
|
||||
"borsh 0.8.2",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-program",
|
||||
"solana-program-test",
|
||||
"solana-sdk",
|
||||
"spl-token 3.1.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
|
|
@ -7,23 +7,26 @@ repository = "https://github.com/solana-labs/solana-program-library"
|
|||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
test-bpf = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
thiserror = "1.0"
|
||||
arrayref = "0.3.6"
|
||||
bincode = "1.3.2"
|
||||
borsh = "0.8.1"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serde = "1.0.121"
|
||||
serde_derive = "1.0.103"
|
||||
solana-program = "1.6.7"
|
||||
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -8,17 +8,53 @@ use solana_program::{
|
|||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that may be returned by the Governance program.
|
||||
/// Errors that may be returned by the Governance program
|
||||
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
|
||||
pub enum GovernanceError {
|
||||
/// Invalid instruction passed to program.
|
||||
/// Invalid instruction passed to program
|
||||
#[error("Invalid instruction passed to program")]
|
||||
InvalidInstruction,
|
||||
|
||||
/// Realm with the given name and governing mints already exists
|
||||
#[error("Realm with the given name and governing mints already exists")]
|
||||
RealmAlreadyExists,
|
||||
|
||||
/// Invalid Governing Token Mint
|
||||
#[error("Invalid Governing Token Mint")]
|
||||
InvalidGoverningTokenMint,
|
||||
|
||||
/// Governing Token Owner must sign transaction
|
||||
#[error("Governing Token Owner must sign transaction")]
|
||||
GoverningTokenOwnerMustSign,
|
||||
|
||||
/// Governing Token Owner or Vote Authority must sign transaction
|
||||
#[error("Governing Token Owner or Vote Authority must sign transaction")]
|
||||
GoverningTokenOwnerOrVoteAuthrotiyMustSign,
|
||||
|
||||
/// All active votes must be relinquished to withdraw governing tokens
|
||||
#[error("All active votes must be relinquished to withdraw governing tokens")]
|
||||
CannotWithdrawGoverningTokensWhenActiveVotesExist,
|
||||
|
||||
/// Invalid Voter account address
|
||||
#[error("Invalid Voter account address")]
|
||||
InvalidVoterAccountAddress,
|
||||
|
||||
/// ---- Account Tools Errors -----
|
||||
|
||||
/// Invalid account owner
|
||||
#[error("Invalid account owner")]
|
||||
InvalidAccountOwner,
|
||||
|
||||
/// ---- Token Tools Errors -----
|
||||
|
||||
/// Invalid Token account owner
|
||||
#[error("Invalid Token account owner")]
|
||||
InvalidTokenAccountOwner,
|
||||
}
|
||||
|
||||
impl PrintProgramError for GovernanceError {
|
||||
fn print<E>(&self) {
|
||||
msg!(&self.to_string());
|
||||
msg!("GOVERNANCE-ERROR: {}", &self.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
//! Program instructions
|
||||
|
||||
use solana_program::{instruction::Instruction, pubkey::Pubkey};
|
||||
|
||||
use crate::state::enums::GoverningTokenType;
|
||||
use crate::{
|
||||
id,
|
||||
state::{
|
||||
enums::GoverningTokenType,
|
||||
realm::{get_governing_token_holding_address, get_realm_address},
|
||||
single_signer_instruction::InstructionData,
|
||||
voter_record::get_voter_record_address,
|
||||
},
|
||||
};
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
system_program, sysvar,
|
||||
};
|
||||
|
||||
/// Yes/No Vote
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum Vote {
|
||||
/// Yes vote
|
||||
Yes,
|
||||
|
@ -15,7 +27,7 @@ pub enum Vote {
|
|||
}
|
||||
|
||||
/// Instructions supported by the Governance program
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
#[repr(C)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum GovernanceInstruction {
|
||||
|
@ -33,6 +45,7 @@ pub enum GovernanceInstruction {
|
|||
/// 8. `[writable]` Council Token Holding account - optional. . PDA seeds: ['governance',realm,council_mint]
|
||||
/// The account will be created with the Realm PDA as its owner
|
||||
CreateRealm {
|
||||
#[allow(dead_code)]
|
||||
/// UTF-8 encoded Governance Realm name
|
||||
name: String,
|
||||
},
|
||||
|
@ -45,10 +58,11 @@ pub enum GovernanceInstruction {
|
|||
/// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint]
|
||||
/// 2. `[writable]` Governing Token Source account. All tokens from the account will be transferred to the Holding account
|
||||
/// 3. `[signer]` Governing Token Owner account
|
||||
/// 4. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||
/// 5. `[signer]` Payer
|
||||
/// 6. `[]` System
|
||||
/// 7. `[]` SPL Token
|
||||
/// 4. `[signer]` Governing Token Transfer authority
|
||||
/// 5. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||
/// 6. `[signer]` Payer
|
||||
/// 7. `[]` System
|
||||
/// 8. `[]` SPL Token
|
||||
DepositGoverningTokens {},
|
||||
|
||||
/// Withdraws governing tokens (Community or Council) from Governance Realm and downgrades your voter weight within the Realm
|
||||
|
@ -65,8 +79,9 @@ pub enum GovernanceInstruction {
|
|||
|
||||
/// Sets vote authority for the given Realm and Governing Token Mint (Community or Council)
|
||||
/// The vote authority would have voting rights and could vote on behalf of the Governing Token Owner
|
||||
/// Note: This doesn't take voting rights from the Token Owner who still can vote and change vote_authority
|
||||
///
|
||||
/// 0. `[signer]` Governing Token Owner
|
||||
/// 0. `[signer]` Current Vote authority or Governing Token owner
|
||||
/// 1. `[writable]` Voter Record
|
||||
SetVoteAuthority {
|
||||
#[allow(dead_code)]
|
||||
|
@ -77,9 +92,13 @@ pub enum GovernanceInstruction {
|
|||
/// Governing Token Mint the vote authority is granted over
|
||||
governing_token_mint: Pubkey,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Governing Token Owner the vote authority is set for
|
||||
governing_token_owner: Pubkey,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// New vote authority
|
||||
vote_authority: Pubkey,
|
||||
new_vote_authority: Option<Pubkey>,
|
||||
},
|
||||
|
||||
/// Creates Program Governance account which governs an upgradable program
|
||||
|
@ -93,16 +112,20 @@ pub enum GovernanceInstruction {
|
|||
/// 6. `[]` System account
|
||||
/// 7. `[]` Bpf_upgrade_loader account
|
||||
CreateProgramGovernance {
|
||||
#[allow(dead_code)]
|
||||
/// Voting threshold in % required to tip the vote
|
||||
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
|
||||
vote_threshold: u8,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
|
||||
min_instruction_hold_up_time: u64,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Time limit in slots for proposal to be open for voting
|
||||
max_voting_time: u64,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Minimum % of tokens for a governance token owner to be able to create proposal
|
||||
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
|
||||
token_threshold_to_create_proposal: u8,
|
||||
|
@ -120,12 +143,15 @@ pub enum GovernanceInstruction {
|
|||
/// 6. '[]` Token program account
|
||||
/// 7. `[]` Rent sysvar
|
||||
CreateProposal {
|
||||
#[allow(dead_code)]
|
||||
/// Link to gist explaining proposal
|
||||
description_link: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// UTF-8 encoded name of the proposal
|
||||
name: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// The Governing token (Community or Council) which will be used for voting on the Proposal
|
||||
governing_token_type: GoverningTokenType,
|
||||
},
|
||||
|
@ -158,12 +184,15 @@ pub enum GovernanceInstruction {
|
|||
/// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account
|
||||
/// 2. `[signer]` Admin account
|
||||
AddSingleSignerInstruction {
|
||||
#[allow(dead_code)]
|
||||
/// Slot waiting time between vote period ending and this being eligible for execution
|
||||
hold_up_time: u64,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Instruction
|
||||
instruction: Instruction,
|
||||
instruction: InstructionData,
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Position in instruction array
|
||||
position: u8,
|
||||
},
|
||||
|
@ -183,6 +212,7 @@ pub enum GovernanceInstruction {
|
|||
/// 1. `[writable]` Proposal SingleSignerInstruction account
|
||||
/// 2. `[signer]` Admin account
|
||||
UpdateInstructionHoldUpTime {
|
||||
#[allow(dead_code)]
|
||||
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
|
||||
hold_up_time: u64,
|
||||
},
|
||||
|
@ -215,6 +245,7 @@ pub enum GovernanceInstruction {
|
|||
/// 3. `[signer]` Vote Authority account
|
||||
/// 4. `[]` Governance account
|
||||
Vote {
|
||||
#[allow(dead_code)]
|
||||
/// Yes/No vote
|
||||
vote: Vote,
|
||||
},
|
||||
|
@ -243,3 +274,147 @@ pub enum GovernanceInstruction {
|
|||
/// 5+ Any extra accounts that are part of the instruction, in order
|
||||
Execute,
|
||||
}
|
||||
|
||||
/// Creates CreateRealm instruction
|
||||
pub fn create_realm(
|
||||
// Accounts
|
||||
community_token_mint: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
council_token_mint: Option<Pubkey>,
|
||||
// Args
|
||||
name: String,
|
||||
) -> Instruction {
|
||||
let realm_address = get_realm_address(&name);
|
||||
let community_token_holding_address =
|
||||
get_governing_token_holding_address(&realm_address, &community_token_mint);
|
||||
|
||||
let mut accounts = vec![
|
||||
AccountMeta::new(realm_address, false),
|
||||
AccountMeta::new_readonly(*community_token_mint, false),
|
||||
AccountMeta::new(community_token_holding_address, false),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
];
|
||||
|
||||
if let Some(council_token_mint) = council_token_mint {
|
||||
let council_token_holding_address =
|
||||
get_governing_token_holding_address(&realm_address, &council_token_mint);
|
||||
|
||||
accounts.push(AccountMeta::new_readonly(council_token_mint, false));
|
||||
accounts.push(AccountMeta::new(council_token_holding_address, false));
|
||||
}
|
||||
|
||||
let instruction = GovernanceInstruction::CreateRealm { name };
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates DepositGoverningTokens instruction
|
||||
pub fn deposit_governing_tokens(
|
||||
// Accounts
|
||||
realm: &Pubkey,
|
||||
governing_token_source: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
governing_token_transfer_authority: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
// Args
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Instruction {
|
||||
let vote_record_address =
|
||||
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
|
||||
|
||||
let governing_token_holding_address =
|
||||
get_governing_token_holding_address(realm, governing_token_mint);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*realm, false),
|
||||
AccountMeta::new(governing_token_holding_address, false),
|
||||
AccountMeta::new(*governing_token_source, false),
|
||||
AccountMeta::new_readonly(*governing_token_owner, true),
|
||||
AccountMeta::new_readonly(*governing_token_transfer_authority, true),
|
||||
AccountMeta::new(vote_record_address, false),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::DepositGoverningTokens {};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates WithdrawGoverningTokens instruction
|
||||
pub fn withdraw_governing_tokens(
|
||||
// Accounts
|
||||
realm: &Pubkey,
|
||||
governing_token_destination: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
// Args
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Instruction {
|
||||
let vote_record_address =
|
||||
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
|
||||
|
||||
let governing_token_holding_address =
|
||||
get_governing_token_holding_address(realm, governing_token_mint);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*realm, false),
|
||||
AccountMeta::new(governing_token_holding_address, false),
|
||||
AccountMeta::new(*governing_token_destination, false),
|
||||
AccountMeta::new_readonly(*governing_token_owner, true),
|
||||
AccountMeta::new(vote_record_address, false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::WithdrawGoverningTokens {};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates SetVoteAuthority instruction
|
||||
pub fn set_vote_authority(
|
||||
// Accounts
|
||||
vote_authority: &Pubkey,
|
||||
// Args
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
new_vote_authority: &Option<Pubkey>,
|
||||
) -> Instruction {
|
||||
let vote_record_address =
|
||||
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*vote_authority, true),
|
||||
AccountMeta::new(vote_record_address, false),
|
||||
];
|
||||
|
||||
let instruction = GovernanceInstruction::SetVoteAuthority {
|
||||
realm: *realm,
|
||||
governing_token_mint: *governing_token_mint,
|
||||
governing_token_owner: *governing_token_owner,
|
||||
new_vote_authority: *new_vote_authority,
|
||||
};
|
||||
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,12 @@ pub mod error;
|
|||
pub mod instruction;
|
||||
pub mod processor;
|
||||
pub mod state;
|
||||
pub mod tools;
|
||||
|
||||
// Export current sdk types for downstream users building with a different sdk version
|
||||
pub use solana_program;
|
||||
|
||||
solana_program::declare_id!("Governance111111111111111111111111111111111");
|
||||
solana_program::declare_id!("GovernancerdmUu324nahyv33G5poQdLUEZ1nEytDeP");
|
||||
|
||||
/// Seed prefix for Governance PDAs
|
||||
pub const PROGRAM_AUTHORITY_SEED: &[u8] = b"governance";
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
//! Instruction processor
|
||||
|
||||
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
|
||||
|
||||
use crate::error::GovernanceError;
|
||||
|
||||
/// Processes an instruction
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
_accounts: &[AccountInfo],
|
||||
_input: &[u8],
|
||||
) -> ProgramResult {
|
||||
Err(GovernanceError::InvalidInstruction.into())
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
//! Program processor
|
||||
|
||||
mod process_create_realm;
|
||||
mod process_deposit_governing_tokens;
|
||||
mod process_set_vote_authority;
|
||||
mod process_withdraw_governing_tokens;
|
||||
|
||||
use crate::instruction::GovernanceInstruction;
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
use process_create_realm::*;
|
||||
use process_deposit_governing_tokens::*;
|
||||
use process_set_vote_authority::*;
|
||||
use process_withdraw_governing_tokens::*;
|
||||
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
/// Processes an instruction
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
input: &[u8],
|
||||
) -> ProgramResult {
|
||||
let instruction = GovernanceInstruction::try_from_slice(input)
|
||||
.map_err(|_| ProgramError::InvalidInstructionData)?;
|
||||
|
||||
msg!("Instruction: {:?}", instruction);
|
||||
|
||||
match instruction {
|
||||
GovernanceInstruction::CreateRealm { name } => {
|
||||
process_create_realm(program_id, accounts, name)
|
||||
}
|
||||
|
||||
GovernanceInstruction::DepositGoverningTokens {} => {
|
||||
process_deposit_governing_tokens(program_id, accounts)
|
||||
}
|
||||
|
||||
GovernanceInstruction::WithdrawGoverningTokens {} => {
|
||||
process_withdraw_governing_tokens(program_id, accounts)
|
||||
}
|
||||
|
||||
GovernanceInstruction::SetVoteAuthority {
|
||||
realm,
|
||||
governing_token_mint,
|
||||
governing_token_owner,
|
||||
new_vote_authority,
|
||||
} => process_set_vote_authority(
|
||||
accounts,
|
||||
&realm,
|
||||
&governing_token_mint,
|
||||
&governing_token_owner,
|
||||
&new_vote_authority,
|
||||
),
|
||||
_ => todo!("Instruction not implemented yet"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
//! Program state processor
|
||||
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
state::{
|
||||
enums::GovernanceAccountType,
|
||||
realm::{get_governing_token_holding_address_seeds, get_realm_address_seeds, Realm},
|
||||
},
|
||||
tools::{account::create_and_serialize_account_signed, token::create_spl_token_account_signed},
|
||||
};
|
||||
|
||||
/// Processes CreateRealm instruction
|
||||
pub fn process_create_realm(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
name: String,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let governance_token_mint_info = next_account_info(account_info_iter)?; // 1
|
||||
let governance_token_holding_info = next_account_info(account_info_iter)?; // 2
|
||||
let payer_info = next_account_info(account_info_iter)?; // 3
|
||||
let system_info = next_account_info(account_info_iter)?; // 4
|
||||
let spl_token_info = next_account_info(account_info_iter)?; // 5
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
|
||||
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
if !realm_info.data_is_empty() {
|
||||
return Err(GovernanceError::RealmAlreadyExists.into());
|
||||
}
|
||||
|
||||
create_spl_token_account_signed(
|
||||
payer_info,
|
||||
governance_token_holding_info,
|
||||
&get_governing_token_holding_address_seeds(realm_info.key, governance_token_mint_info.key),
|
||||
governance_token_mint_info,
|
||||
realm_info,
|
||||
program_id,
|
||||
system_info,
|
||||
spl_token_info,
|
||||
rent_sysvar_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
let council_token_mint_address = if let Ok(council_token_mint_info) =
|
||||
next_account_info(account_info_iter)
|
||||
// 7
|
||||
{
|
||||
let council_token_holding_info = next_account_info(account_info_iter)?; //8
|
||||
|
||||
create_spl_token_account_signed(
|
||||
payer_info,
|
||||
council_token_holding_info,
|
||||
&get_governing_token_holding_address_seeds(realm_info.key, council_token_mint_info.key),
|
||||
council_token_mint_info,
|
||||
realm_info,
|
||||
program_id,
|
||||
system_info,
|
||||
spl_token_info,
|
||||
rent_sysvar_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
Some(*council_token_mint_info.key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let realm_data = Realm {
|
||||
account_type: GovernanceAccountType::Realm,
|
||||
community_mint: *governance_token_mint_info.key,
|
||||
council_mint: council_token_mint_address,
|
||||
name: name.clone(),
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed::<Realm>(
|
||||
payer_info,
|
||||
&realm_info,
|
||||
&realm_data,
|
||||
&get_realm_address_seeds(&name),
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
sysvar::Sysvar,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
state::{
|
||||
enums::{GovernanceAccountType, GoverningTokenType},
|
||||
realm::deserialize_realm,
|
||||
voter_record::{deserialize_voter_record, get_voter_record_address_seeds, VoterRecord},
|
||||
},
|
||||
tools::{
|
||||
account::create_and_serialize_account_signed,
|
||||
token::{
|
||||
get_amount_from_token_account, get_mint_from_token_account,
|
||||
get_owner_from_token_account, transfer_spl_tokens,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// Processes DepositGoverningTokens instruction
|
||||
pub fn process_deposit_governing_tokens(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
|
||||
let governing_token_source_info = next_account_info(account_info_iter)?; // 2
|
||||
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
|
||||
let governing_token_transfer_authority_info = next_account_info(account_info_iter)?; // 4
|
||||
let voter_record_info = next_account_info(account_info_iter)?; // 5
|
||||
let payer_info = next_account_info(account_info_iter)?; // 6
|
||||
let system_info = next_account_info(account_info_iter)?; // 7
|
||||
let spl_token_info = next_account_info(account_info_iter)?; // 8
|
||||
let rent_sysvar_info = next_account_info(account_info_iter)?; // 9
|
||||
|
||||
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||
|
||||
let realm_data = deserialize_realm(realm_info)?;
|
||||
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
|
||||
|
||||
let governing_token_type = if governing_token_mint == realm_data.community_mint {
|
||||
GoverningTokenType::Community
|
||||
} else if Some(governing_token_mint) == realm_data.council_mint {
|
||||
GoverningTokenType::Council
|
||||
} else {
|
||||
return Err(GovernanceError::InvalidGoverningTokenMint.into());
|
||||
};
|
||||
|
||||
let amount = get_amount_from_token_account(governing_token_source_info)?;
|
||||
|
||||
transfer_spl_tokens(
|
||||
&governing_token_source_info,
|
||||
&governing_token_holding_info,
|
||||
&governing_token_transfer_authority_info,
|
||||
amount,
|
||||
spl_token_info,
|
||||
)?;
|
||||
|
||||
let voter_record_address_seeds = get_voter_record_address_seeds(
|
||||
realm_info.key,
|
||||
&governing_token_mint,
|
||||
governing_token_owner_info.key,
|
||||
);
|
||||
|
||||
if voter_record_info.data_is_empty() {
|
||||
// Deposited tokens can only be withdrawn by the owner so let's make sure the owner signed the transaction
|
||||
let governing_token_owner = get_owner_from_token_account(&governing_token_source_info)?;
|
||||
|
||||
if !(governing_token_owner == *governing_token_owner_info.key
|
||||
&& governing_token_owner_info.is_signer)
|
||||
{
|
||||
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
|
||||
let voter_record_data = VoterRecord {
|
||||
account_type: GovernanceAccountType::VoterRecord,
|
||||
realm: *realm_info.key,
|
||||
token_owner: *governing_token_owner_info.key,
|
||||
token_deposit_amount: amount,
|
||||
token_type: governing_token_type,
|
||||
vote_authority: None,
|
||||
active_votes_count: 0,
|
||||
total_votes_count: 0,
|
||||
};
|
||||
|
||||
create_and_serialize_account_signed(
|
||||
payer_info,
|
||||
voter_record_info,
|
||||
&voter_record_data,
|
||||
&voter_record_address_seeds,
|
||||
program_id,
|
||||
system_info,
|
||||
rent,
|
||||
)?;
|
||||
} else {
|
||||
let mut voter_record_data =
|
||||
deserialize_voter_record(voter_record_info, &voter_record_address_seeds)?;
|
||||
|
||||
voter_record_data.token_deposit_amount = voter_record_data
|
||||
.token_deposit_amount
|
||||
.checked_add(amount)
|
||||
.unwrap();
|
||||
|
||||
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
state::voter_record::{deserialize_voter_record, get_voter_record_address_seeds},
|
||||
tools::asserts::assert_is_signed_by_owner_or_vote_authority,
|
||||
};
|
||||
|
||||
/// Processes SetVoteAuthority instruction
|
||||
pub fn process_set_vote_authority(
|
||||
accounts: &[AccountInfo],
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
new_vote_authority: &Option<Pubkey>,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let vote_authority_info = next_account_info(account_info_iter)?; // 0
|
||||
let voter_record_info = next_account_info(account_info_iter)?; // 1
|
||||
|
||||
let mut voter_record_data = deserialize_voter_record(
|
||||
voter_record_info,
|
||||
&get_voter_record_address_seeds(realm, &governing_token_mint, governing_token_owner),
|
||||
)?;
|
||||
|
||||
assert_is_signed_by_owner_or_vote_authority(&voter_record_data, &vote_authority_info)?;
|
||||
|
||||
voter_record_data.vote_authority = *new_vote_authority;
|
||||
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
//! Program state processor
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
state::{
|
||||
realm::{deserialize_realm, get_realm_address_seeds},
|
||||
voter_record::{deserialize_voter_record, get_voter_record_address_seeds},
|
||||
},
|
||||
tools::token::{get_mint_from_token_account, transfer_spl_tokens_signed},
|
||||
};
|
||||
|
||||
/// Processes WithdrawGoverningTokens instruction
|
||||
pub fn process_withdraw_governing_tokens(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
let realm_info = next_account_info(account_info_iter)?; // 0
|
||||
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
|
||||
let governing_token_destination_info = next_account_info(account_info_iter)?; // 2
|
||||
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
|
||||
let voter_record_info = next_account_info(account_info_iter)?; // 4
|
||||
let spl_token_info = next_account_info(account_info_iter)?; // 5
|
||||
|
||||
if !governing_token_owner_info.is_signer {
|
||||
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
|
||||
let realm_data = deserialize_realm(realm_info)?;
|
||||
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
|
||||
|
||||
let voter_record_address_seeds = get_voter_record_address_seeds(
|
||||
realm_info.key,
|
||||
&governing_token_mint,
|
||||
governing_token_owner_info.key,
|
||||
);
|
||||
|
||||
let mut voter_record_data =
|
||||
deserialize_voter_record(voter_record_info, &voter_record_address_seeds)?;
|
||||
|
||||
if voter_record_data.active_votes_count > 0 {
|
||||
return Err(GovernanceError::CannotWithdrawGoverningTokensWhenActiveVotesExist.into());
|
||||
}
|
||||
|
||||
transfer_spl_tokens_signed(
|
||||
&governing_token_holding_info,
|
||||
&governing_token_destination_info,
|
||||
&realm_info,
|
||||
&get_realm_address_seeds(&realm_data.name),
|
||||
program_id,
|
||||
voter_record_data.token_deposit_amount,
|
||||
spl_token_info,
|
||||
)?;
|
||||
|
||||
voter_record_data.token_deposit_amount = 0;
|
||||
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
//! State enumerations
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
|
||||
/// Defines all Governance accounts types
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum GovernanceAccountType {
|
||||
/// Default uninitialized account state
|
||||
Uninitialized,
|
||||
|
@ -34,7 +36,7 @@ impl Default for GovernanceAccountType {
|
|||
|
||||
/// Vote with number of votes
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum VoteWeight {
|
||||
/// Yes vote
|
||||
Yes(u64),
|
||||
|
@ -45,7 +47,7 @@ pub enum VoteWeight {
|
|||
|
||||
/// Governing Token type
|
||||
#[repr(C)]
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum GoverningTokenType {
|
||||
/// Community token
|
||||
Community,
|
||||
|
@ -55,7 +57,7 @@ pub enum GoverningTokenType {
|
|||
|
||||
/// What state a Proposal is in
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum ProposalState {
|
||||
/// Draft - Proposal enters Draft state when it's created
|
||||
Draft,
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
//! Program Governance Account
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
use super::enums::GovernanceAccountType;
|
||||
|
||||
/// Program Governance Account
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct ProgramGovernance {
|
||||
/// Account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
use solana_program::{epoch_schedule::Slot, pubkey::Pubkey};
|
||||
|
||||
use super::enums::{GovernanceAccountType, GoverningTokenType, ProposalState};
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
|
||||
/// Governance Proposal
|
||||
#[repr(C)]
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct Proposal {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
//! Proposal Vote Record Account
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
use super::enums::{GovernanceAccountType, VoteWeight};
|
||||
|
||||
/// Proposal Vote Record
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct ProposalVoteRecord {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
//! Realm Account
|
||||
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
id,
|
||||
tools::account::{deserialize_account, AccountMaxSize},
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
};
|
||||
|
||||
use super::enums::GovernanceAccountType;
|
||||
|
||||
/// Governance Realm Account
|
||||
/// Account PDA seeds" ['governance', name]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct Realm {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
@ -20,3 +31,50 @@ pub struct Realm {
|
|||
/// Governance Realm name
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for Realm {}
|
||||
|
||||
impl IsInitialized for Realm {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::Realm
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes account and checks owner program
|
||||
pub fn deserialize_realm(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
|
||||
deserialize_account::<Realm>(realm_info, &id())
|
||||
}
|
||||
|
||||
/// Returns Realm PDA seeds
|
||||
pub fn get_realm_address_seeds(name: &str) -> [&[u8]; 2] {
|
||||
[PROGRAM_AUTHORITY_SEED, &name.as_bytes()]
|
||||
}
|
||||
|
||||
/// Returns Realm PDA address
|
||||
pub fn get_realm_address(name: &str) -> Pubkey {
|
||||
Pubkey::find_program_address(&get_realm_address_seeds(&name), &id()).0
|
||||
}
|
||||
|
||||
/// Returns Realm Token Holding PDA seeds
|
||||
pub fn get_governing_token_holding_address_seeds<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governing_token_mint: &'a Pubkey,
|
||||
) -> [&'a [u8]; 3] {
|
||||
[
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
realm.as_ref(),
|
||||
governing_token_mint.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns Realm Token Holding PDA address
|
||||
pub fn get_governing_token_holding_address(
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_governing_token_holding_address_seeds(realm, governing_token_mint),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
//! SingleSignerInstruction Account
|
||||
|
||||
use solana_program::instruction::Instruction;
|
||||
|
||||
use super::enums::GovernanceAccountType;
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
|
||||
/// Account for an instruction to be executed for Proposal
|
||||
#[repr(C)]
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct SingleSignerInstruction {
|
||||
/// Governance Account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
@ -17,8 +16,13 @@ pub struct SingleSignerInstruction {
|
|||
/// Instruction to execute
|
||||
/// The instruction will be signed by Governance PDA the Proposal belongs to
|
||||
// For example for ProgramGovernance the instruction to upgrade program will be signed by ProgramGovernance PDA
|
||||
pub instruction: Instruction,
|
||||
pub instruction: InstructionData,
|
||||
|
||||
/// Executed flag
|
||||
pub executed: bool,
|
||||
}
|
||||
|
||||
/// Temp. placeholder until I get Borsh serialization for Instruction working
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
#[repr(C)]
|
||||
pub struct InstructionData {}
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
//! Voter Record Account
|
||||
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use crate::{
|
||||
error::GovernanceError,
|
||||
id,
|
||||
tools::account::{deserialize_account, AccountMaxSize},
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
};
|
||||
|
||||
use super::enums::{GovernanceAccountType, GoverningTokenType};
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
/// Governance Voter Record
|
||||
/// Account PDA seeds: ['governance', realm, token_mint, token_owner ]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct VoterRecord {
|
||||
/// Governance account type
|
||||
pub account_type: GovernanceAccountType,
|
||||
|
@ -26,8 +37,8 @@ pub struct VoterRecord {
|
|||
pub token_deposit_amount: u64,
|
||||
|
||||
/// A single account that is allowed to operate governance with the deposited governing tokens
|
||||
/// It's delegated to by the token owner
|
||||
pub vote_authority: Pubkey,
|
||||
/// It's delegated to by the governing token owner or current vote_authority
|
||||
pub vote_authority: Option<Pubkey>,
|
||||
|
||||
/// The number of active votes cast by voter
|
||||
pub active_votes_count: u8,
|
||||
|
@ -35,3 +46,81 @@ pub struct VoterRecord {
|
|||
/// The total number of votes cast by the voter
|
||||
pub total_votes_count: u8,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for VoterRecord {
|
||||
fn get_max_size(&self) -> Option<usize> {
|
||||
Some(109)
|
||||
}
|
||||
}
|
||||
|
||||
impl IsInitialized for VoterRecord {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.account_type == GovernanceAccountType::VoterRecord
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns VoteRecord PDA address
|
||||
pub fn get_voter_record_address(
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
governing_token_owner: &Pubkey,
|
||||
) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&get_voter_record_address_seeds(realm, governing_token_mint, governing_token_owner),
|
||||
&id(),
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
/// Returns VoterRecord PDA seeds
|
||||
pub fn get_voter_record_address_seeds<'a>(
|
||||
realm: &'a Pubkey,
|
||||
governing_token_mint: &'a Pubkey,
|
||||
governing_token_owner: &'a Pubkey,
|
||||
) -> [&'a [u8]; 4] {
|
||||
[
|
||||
PROGRAM_AUTHORITY_SEED,
|
||||
realm.as_ref(),
|
||||
governing_token_mint.as_ref(),
|
||||
governing_token_owner.as_ref(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Deserializes VoterRecord and checks account PDA and owner program
|
||||
pub fn deserialize_voter_record(
|
||||
voter_record_info: &AccountInfo,
|
||||
voter_record_seeds: &[&[u8]],
|
||||
) -> Result<VoterRecord, ProgramError> {
|
||||
let (voter_record_address, _) = Pubkey::find_program_address(voter_record_seeds, &id());
|
||||
|
||||
if voter_record_address != *voter_record_info.key {
|
||||
return Err(GovernanceError::InvalidVoterAccountAddress.into());
|
||||
}
|
||||
|
||||
deserialize_account::<VoterRecord>(voter_record_info, &id())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use solana_program::borsh::get_packed_len;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_max_size() {
|
||||
let vote_record = VoterRecord {
|
||||
account_type: GovernanceAccountType::VoterRecord,
|
||||
realm: Pubkey::new_unique(),
|
||||
token_type: GoverningTokenType::Community,
|
||||
token_owner: Pubkey::new_unique(),
|
||||
token_deposit_amount: 10,
|
||||
vote_authority: Some(Pubkey::new_unique()),
|
||||
active_votes_count: 1,
|
||||
total_votes_count: 1,
|
||||
};
|
||||
|
||||
let size = get_packed_len::<VoterRecord>();
|
||||
|
||||
assert_eq!(vote_record.get_max_size(), Some(size));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
//! General purpose account utility functions
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, borsh::try_from_slice_unchecked, msg, program::invoke_signed,
|
||||
program_error::ProgramError, program_pack::IsInitialized, pubkey::Pubkey, rent::Rent,
|
||||
system_instruction::create_account,
|
||||
};
|
||||
|
||||
use crate::error::GovernanceError;
|
||||
|
||||
/// Trait for accounts to return their max size
|
||||
pub trait AccountMaxSize {
|
||||
/// Returns max account size or None if max size is not known and actual instance size should be used
|
||||
fn get_max_size(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new account and serializes data into it using the provided seeds to invoke signed CPI call
|
||||
/// Note: This functions also checks the provided account PDA matches the supplied seeds
|
||||
pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSize>(
|
||||
payer_info: &AccountInfo<'a>,
|
||||
account_info: &AccountInfo<'a>,
|
||||
account_data: &T,
|
||||
account_address_seeds: &[&[u8]],
|
||||
program_id: &Pubkey,
|
||||
system_info: &AccountInfo<'a>,
|
||||
rent: &Rent,
|
||||
) -> Result<(), ProgramError> {
|
||||
// Get PDA and assert it's the same as the requested account address
|
||||
let (account_address, bump_seed) =
|
||||
Pubkey::find_program_address(account_address_seeds, program_id);
|
||||
|
||||
if account_address != *account_info.key {
|
||||
msg!(
|
||||
"Create account with PDA: {:?} was requested while PDA: {:?} was expected",
|
||||
account_info.key,
|
||||
account_address
|
||||
);
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
let (serialized_data, account_size) = if let Some(max_size) = account_data.get_max_size() {
|
||||
(None, max_size)
|
||||
} else {
|
||||
let serialized_data = account_data.try_to_vec()?;
|
||||
let account_size = serialized_data.len();
|
||||
(Some(serialized_data), account_size)
|
||||
};
|
||||
|
||||
let create_account_instruction = create_account(
|
||||
payer_info.key,
|
||||
account_info.key,
|
||||
rent.minimum_balance(account_size),
|
||||
account_size as u64,
|
||||
program_id,
|
||||
);
|
||||
|
||||
let mut signers_seeds = account_address_seeds.to_vec();
|
||||
let bump = &[bump_seed];
|
||||
signers_seeds.push(bump);
|
||||
|
||||
invoke_signed(
|
||||
&create_account_instruction,
|
||||
&[
|
||||
payer_info.clone(),
|
||||
account_info.clone(),
|
||||
system_info.clone(),
|
||||
],
|
||||
&[&signers_seeds[..]],
|
||||
)?;
|
||||
|
||||
if let Some(serialized_data) = serialized_data {
|
||||
account_info
|
||||
.data
|
||||
.borrow_mut()
|
||||
.copy_from_slice(&serialized_data);
|
||||
} else {
|
||||
account_data.serialize(&mut *account_info.data.borrow_mut())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deserializes account and checks it's initialized and owned by the specified program
|
||||
pub fn deserialize_account<T: BorshDeserialize + IsInitialized>(
|
||||
account_info: &AccountInfo,
|
||||
owner_program_id: &Pubkey,
|
||||
) -> Result<T, ProgramError> {
|
||||
if account_info.owner != owner_program_id {
|
||||
return Err(GovernanceError::InvalidAccountOwner.into());
|
||||
}
|
||||
|
||||
let account: T = try_from_slice_unchecked(&account_info.data.borrow())?;
|
||||
if !account.is_initialized() {
|
||||
Err(ProgramError::UninitializedAccount)
|
||||
} else {
|
||||
Ok(account)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//! Governance asserts
|
||||
|
||||
use solana_program::{account_info::AccountInfo, program_error::ProgramError};
|
||||
|
||||
use crate::{error::GovernanceError, state::voter_record::VoterRecord};
|
||||
|
||||
/// Checks wether the provided vote authority can set new vote authority
|
||||
pub fn assert_is_signed_by_owner_or_vote_authority(
|
||||
voter_record: &VoterRecord,
|
||||
vote_authority_info: &AccountInfo,
|
||||
) -> Result<(), ProgramError> {
|
||||
if vote_authority_info.is_signer {
|
||||
if &voter_record.token_owner == vote_authority_info.key {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(vote_authority) = voter_record.vote_authority {
|
||||
if &vote_authority == vote_authority_info.key {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Err(GovernanceError::GoverningTokenOwnerOrVoteAuthrotiyMustSign.into())
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
//! Utility functions
|
||||
|
||||
pub mod account;
|
||||
|
||||
pub mod token;
|
||||
|
||||
pub mod asserts;
|
|
@ -0,0 +1,211 @@
|
|||
//! General purpose SPL token utility functions
|
||||
|
||||
use arrayref::array_ref;
|
||||
use solana_program::{
|
||||
account_info::AccountInfo,
|
||||
entrypoint::ProgramResult,
|
||||
msg,
|
||||
program::{invoke, invoke_signed},
|
||||
program_error::ProgramError,
|
||||
program_pack::Pack,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
system_instruction,
|
||||
};
|
||||
|
||||
use crate::error::GovernanceError;
|
||||
|
||||
/// Creates and initializes SPL token account with PDA using the provided PDA seeds
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_spl_token_account_signed<'a>(
|
||||
payer_info: &AccountInfo<'a>,
|
||||
token_account_info: &AccountInfo<'a>,
|
||||
token_account_address_seeds: &[&[u8]],
|
||||
token_mint_info: &AccountInfo<'a>,
|
||||
token_account_owner_info: &AccountInfo<'a>,
|
||||
program_id: &Pubkey,
|
||||
system_info: &AccountInfo<'a>,
|
||||
spl_token_info: &AccountInfo<'a>,
|
||||
rent_sysvar_info: &AccountInfo<'a>,
|
||||
rent: &Rent,
|
||||
) -> Result<(), ProgramError> {
|
||||
let create_account_instruction = system_instruction::create_account(
|
||||
payer_info.key,
|
||||
token_account_info.key,
|
||||
1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())),
|
||||
spl_token::state::Account::get_packed_len() as u64,
|
||||
&spl_token::id(),
|
||||
);
|
||||
|
||||
let (account_address, bump_seed) =
|
||||
Pubkey::find_program_address(token_account_address_seeds, program_id);
|
||||
|
||||
if account_address != *token_account_info.key {
|
||||
msg!(
|
||||
"Create SPL Token Account with PDA: {:?} was requested while PDA: {:?} was expected",
|
||||
token_account_info.key,
|
||||
account_address
|
||||
);
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
let mut signers_seeds = token_account_address_seeds.to_vec();
|
||||
let bump = &[bump_seed];
|
||||
signers_seeds.push(bump);
|
||||
|
||||
invoke_signed(
|
||||
&create_account_instruction,
|
||||
&[
|
||||
payer_info.clone(),
|
||||
token_account_info.clone(),
|
||||
system_info.clone(),
|
||||
],
|
||||
&[&signers_seeds[..]],
|
||||
)?;
|
||||
|
||||
let initialize_account_instruction = spl_token::instruction::initialize_account(
|
||||
&spl_token::id(),
|
||||
token_account_info.key,
|
||||
token_mint_info.key,
|
||||
token_account_owner_info.key,
|
||||
)?;
|
||||
|
||||
invoke(
|
||||
&initialize_account_instruction,
|
||||
&[
|
||||
payer_info.clone(),
|
||||
token_account_info.clone(),
|
||||
token_account_owner_info.clone(),
|
||||
token_mint_info.clone(),
|
||||
spl_token_info.clone(),
|
||||
rent_sysvar_info.clone(),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfers SPL Tokens
|
||||
pub fn transfer_spl_tokens<'a>(
|
||||
source_info: &AccountInfo<'a>,
|
||||
destination_info: &AccountInfo<'a>,
|
||||
authority_info: &AccountInfo<'a>,
|
||||
amount: u64,
|
||||
spl_token_info: &AccountInfo<'a>,
|
||||
) -> ProgramResult {
|
||||
let transfer_instruction = spl_token::instruction::transfer(
|
||||
&spl_token::id(),
|
||||
source_info.key,
|
||||
destination_info.key,
|
||||
authority_info.key,
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
invoke(
|
||||
&transfer_instruction,
|
||||
&[
|
||||
spl_token_info.clone(),
|
||||
authority_info.clone(),
|
||||
source_info.clone(),
|
||||
destination_info.clone(),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfers SPL Tokens from a token account owned by the provided PDA authority with seeds
|
||||
pub fn transfer_spl_tokens_signed<'a>(
|
||||
source_info: &AccountInfo<'a>,
|
||||
destination_info: &AccountInfo<'a>,
|
||||
authority_info: &AccountInfo<'a>,
|
||||
authority_seeds: &[&[u8]],
|
||||
program_id: &Pubkey,
|
||||
amount: u64,
|
||||
spl_token_info: &AccountInfo<'a>,
|
||||
) -> ProgramResult {
|
||||
let (authority_address, bump_seed) = Pubkey::find_program_address(authority_seeds, program_id);
|
||||
|
||||
if authority_address != *authority_info.key {
|
||||
msg!(
|
||||
"Transfer SPL Token with Authority PDA: {:?} was requested while PDA: {:?} was expected",
|
||||
authority_info.key,
|
||||
authority_address
|
||||
);
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
let transfer_instruction = spl_token::instruction::transfer(
|
||||
&spl_token::id(),
|
||||
source_info.key,
|
||||
destination_info.key,
|
||||
authority_info.key,
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut signers_seeds = authority_seeds.to_vec();
|
||||
let bump = &[bump_seed];
|
||||
signers_seeds.push(bump);
|
||||
|
||||
invoke_signed(
|
||||
&transfer_instruction,
|
||||
&[
|
||||
spl_token_info.clone(),
|
||||
authority_info.clone(),
|
||||
source_info.clone(),
|
||||
destination_info.clone(),
|
||||
],
|
||||
&[&signers_seeds[..]],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computationally cheap method to get amount from a token account
|
||||
/// It reads amount without deserializing full account data
|
||||
pub fn get_amount_from_token_account(
|
||||
token_account_info: &AccountInfo,
|
||||
) -> Result<u64, ProgramError> {
|
||||
if token_account_info.owner != &spl_token::id() {
|
||||
return Err(GovernanceError::InvalidTokenAccountOwner.into());
|
||||
}
|
||||
|
||||
// TokeAccount layout: mint(32), owner(32), amount(8), ...
|
||||
let data = token_account_info.try_borrow_data()?;
|
||||
let amount = array_ref![data, 64, 8];
|
||||
Ok(u64::from_le_bytes(*amount))
|
||||
}
|
||||
|
||||
/// Computationally cheap method to get mint from a token account
|
||||
/// It reads mint without deserializing full account data
|
||||
pub fn get_mint_from_token_account(
|
||||
token_account_info: &AccountInfo,
|
||||
) -> Result<Pubkey, ProgramError> {
|
||||
if token_account_info.owner != &spl_token::id() {
|
||||
return Err(GovernanceError::InvalidTokenAccountOwner.into());
|
||||
}
|
||||
|
||||
// TokeAccount layout: mint(32), owner(32), amount(8), ...
|
||||
let data = token_account_info.try_borrow_data().unwrap();
|
||||
let mint_data = array_ref![data, 0, 32];
|
||||
Ok(Pubkey::new_from_array(*mint_data))
|
||||
}
|
||||
|
||||
/// Computationally cheap method to get owner from a token account
|
||||
/// It reads owner without deserializing full account data
|
||||
pub fn get_owner_from_token_account(
|
||||
token_account_info: &AccountInfo,
|
||||
) -> Result<Pubkey, ProgramError> {
|
||||
if token_account_info.owner != &spl_token::id() {
|
||||
return Err(GovernanceError::InvalidTokenAccountOwner.into());
|
||||
}
|
||||
|
||||
// TokeAccount layout: mint(32), owner(32), amount(8)
|
||||
let data = token_account_info.try_borrow_data().unwrap();
|
||||
let owner_data = array_ref![data, 32, 32];
|
||||
Ok(Pubkey::new_from_array(*owner_data))
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_realm_created() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
|
||||
// Act
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
// Assert
|
||||
let realm_account = governance_test
|
||||
.get_realm_account(&realm_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(realm_cookie.account, realm_account);
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program::instruction::AccountMeta;
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use spl_governance::{error::GovernanceError, instruction::deposit_governing_tokens};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_community_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
// Act
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(voter_record_cookie.account, voter_record);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&voter_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount,
|
||||
source_account.amount
|
||||
);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.community_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(voter_record.token_deposit_amount, holding_account.amount);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_council_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
|
||||
|
||||
// Act
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(voter_record_cookie.account, voter_record);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&voter_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount,
|
||||
source_account.amount
|
||||
);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&council_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(voter_record.token_deposit_amount, holding_account.amount);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_subsequent_community_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let deposit_amount = 5;
|
||||
let total_deposit_amount = voter_record_cookie.account.token_deposit_amount + deposit_amount;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_community_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(total_deposit_amount, voter_record.token_deposit_amount);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.community_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(total_deposit_amount, holding_account.amount);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_subsequent_council_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
|
||||
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let deposit_amount = 5;
|
||||
let total_deposit_amount = voter_record_cookie.account.token_deposit_amount + deposit_amount;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_council_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(total_deposit_amount, voter_record.token_deposit_amount);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&council_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(total_deposit_amount, holding_account.amount);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_community_tokens_with_owner_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner = Keypair::new();
|
||||
let transfer_authority = Keypair::new();
|
||||
let token_source = Keypair::new();
|
||||
|
||||
governance_test
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.community_mint_authority,
|
||||
10,
|
||||
&token_owner,
|
||||
&transfer_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut instruction = deposit_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&token_source.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
&transfer_authority.pubkey(),
|
||||
&governance_test.payer.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
instruction.accounts[3] = AccountMeta::new_readonly(token_owner.pubkey(), false);
|
||||
|
||||
// // Act
|
||||
|
||||
let error = governance_test
|
||||
.process_transaction(&[instruction], Some(&[&transfer_authority]))
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn test_deposit_initial_community_tokens_with_invalid_owner_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let token_owner = Keypair::new();
|
||||
let transfer_authority = Keypair::new();
|
||||
let token_source = Keypair::new();
|
||||
|
||||
let invalid_owner = Keypair::new();
|
||||
|
||||
governance_test
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.community_mint_authority,
|
||||
10,
|
||||
&token_owner,
|
||||
&transfer_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let instruction = deposit_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&token_source.pubkey(),
|
||||
&invalid_owner.pubkey(),
|
||||
&transfer_authority.pubkey(),
|
||||
&governance_test.payer.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
// // Act
|
||||
|
||||
let error = governance_test
|
||||
.process_transaction(&[instruction], Some(&[&transfer_authority, &invalid_owner]))
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program::instruction::AccountMeta;
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use spl_governance::{error::GovernanceError, instruction::set_vote_authority};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_community_vote_authority() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_community_vote_authority(&realm_cookie, &mut voter_record_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Some(voter_record_cookie.vote_authority.pubkey()),
|
||||
voter_record.vote_authority
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_vote_authority_to_none() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
governance_test
|
||||
.with_community_vote_authority(&realm_cookie, &mut voter_record_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.set_vote_authority(
|
||||
&realm_cookie,
|
||||
&voter_record_cookie,
|
||||
&voter_record_cookie.token_owner,
|
||||
&realm_cookie.account.community_mint,
|
||||
&None,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(None, voter_record.vote_authority);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_council_vote_authority() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut voter_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.with_council_vote_authority(&realm_cookie, &mut voter_record_cookie)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Some(voter_record_cookie.vote_authority.pubkey()),
|
||||
voter_record.vote_authority
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_community_vote_authority_with_owner_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let hacker_vote_authority = Keypair::new();
|
||||
|
||||
let mut instruction = set_vote_authority(
|
||||
&voter_record_cookie.token_owner.pubkey(),
|
||||
&realm_cookie.address,
|
||||
&realm_cookie.account.community_mint,
|
||||
&voter_record_cookie.token_owner.pubkey(),
|
||||
&Some(hacker_vote_authority.pubkey()),
|
||||
);
|
||||
|
||||
instruction.accounts[0] =
|
||||
AccountMeta::new_readonly(voter_record_cookie.token_owner.pubkey(), false);
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.process_transaction(&[instruction], None)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::GoverningTokenOwnerOrVoteAuthrotiyMustSign.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_community_vote_authority_signed_by_vote_authority() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
let mut voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
governance_test
|
||||
.with_community_vote_authority(&realm_cookie, &mut voter_record_cookie)
|
||||
.await;
|
||||
|
||||
let new_vote_authority = Keypair::new();
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.set_vote_authority(
|
||||
&realm_cookie,
|
||||
&voter_record_cookie,
|
||||
&voter_record_cookie.vote_authority,
|
||||
&realm_cookie.account.community_mint,
|
||||
&Some(new_vote_authority.pubkey()),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Some(new_vote_authority.pubkey()),
|
||||
voter_record.vote_authority
|
||||
);
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use solana_program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::Signer;
|
||||
|
||||
use spl_governance::{
|
||||
error::GovernanceError, instruction::withdraw_governing_tokens,
|
||||
state::voter_record::get_voter_record_address,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_community_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.withdraw_community_tokens(&realm_cookie, &voter_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(0, voter_record.token_deposit_amount);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.community_token_holding_account)
|
||||
.await;
|
||||
|
||||
assert_eq!(0, holding_account.amount);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&voter_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
voter_record_cookie.token_source_amount,
|
||||
source_account.amount
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_council_tokens() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_council_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
.withdraw_council_tokens(&realm_cookie, &voter_record_cookie)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let voter_record = governance_test
|
||||
.get_voter_record_account(&voter_record_cookie.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(0, voter_record.token_deposit_amount);
|
||||
|
||||
let holding_account = governance_test
|
||||
.get_token_account(&realm_cookie.council_token_holding_account.unwrap())
|
||||
.await;
|
||||
|
||||
assert_eq!(0, holding_account.amount);
|
||||
|
||||
let source_account = governance_test
|
||||
.get_token_account(&voter_record_cookie.token_source)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
voter_record_cookie.token_source_amount,
|
||||
source_account.amount
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_community_tokens_with_owner_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let hacker_token_destination = Pubkey::new_unique();
|
||||
|
||||
let mut instruction = withdraw_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&hacker_token_destination,
|
||||
&voter_record_cookie.token_owner.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
instruction.accounts[3] =
|
||||
AccountMeta::new_readonly(voter_record_cookie.token_owner.pubkey(), false);
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.process_transaction(&[instruction], None)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
|
||||
assert_eq!(err, GovernanceError::GoverningTokenOwnerMustSign.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_community_tokens_with_voter_record_address_mismatch_error() {
|
||||
// Arrange
|
||||
let mut governance_test = GovernanceProgramTest::start_new().await;
|
||||
let realm_cookie = governance_test.with_realm().await;
|
||||
|
||||
let voter_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let vote_record_address = get_voter_record_address(
|
||||
&realm_cookie.address,
|
||||
&realm_cookie.account.community_mint,
|
||||
&voter_record_cookie.token_owner.pubkey(),
|
||||
);
|
||||
|
||||
let hacker_record_cookie = governance_test
|
||||
.with_initial_community_token_deposit(&realm_cookie)
|
||||
.await;
|
||||
|
||||
let mut instruction = withdraw_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&hacker_record_cookie.token_source,
|
||||
&hacker_record_cookie.token_owner.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
instruction.accounts[4] = AccountMeta::new(vote_record_address, false);
|
||||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.process_transaction(&[instruction], Some(&[&hacker_record_cookie.token_owner]))
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
|
||||
assert_eq!(err, GovernanceError::InvalidVoterAccountAddress.into());
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use solana_program::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use spl_governance::state::{realm::Realm, voter_record::VoterRecord};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RealmCookie {
|
||||
pub address: Pubkey,
|
||||
|
||||
pub account: Realm,
|
||||
|
||||
pub community_mint_authority: Keypair,
|
||||
|
||||
pub community_token_holding_account: Pubkey,
|
||||
|
||||
pub council_mint_authority: Option<Keypair>,
|
||||
|
||||
pub council_token_holding_account: Option<Pubkey>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VoterRecordCookie {
|
||||
pub address: Pubkey,
|
||||
|
||||
pub account: VoterRecord,
|
||||
|
||||
pub token_source: Pubkey,
|
||||
|
||||
pub token_source_amount: u64,
|
||||
|
||||
pub token_owner: Keypair,
|
||||
|
||||
pub vote_authority: Keypair,
|
||||
}
|
|
@ -0,0 +1,633 @@
|
|||
use borsh::BorshDeserialize;
|
||||
use solana_program::{
|
||||
borsh::try_from_slice_unchecked,
|
||||
instruction::Instruction,
|
||||
program_error::ProgramError,
|
||||
program_pack::{IsInitialized, Pack},
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
system_instruction,
|
||||
};
|
||||
|
||||
use solana_program_test::ProgramTest;
|
||||
use solana_program_test::*;
|
||||
|
||||
use solana_sdk::{
|
||||
signature::{Keypair, Signer},
|
||||
transaction::Transaction,
|
||||
};
|
||||
use spl_governance::{
|
||||
instruction::{
|
||||
create_realm, deposit_governing_tokens, set_vote_authority, withdraw_governing_tokens,
|
||||
},
|
||||
processor::process_instruction,
|
||||
state::{
|
||||
enums::{GovernanceAccountType, GoverningTokenType},
|
||||
realm::{get_governing_token_holding_address, get_realm_address, Realm},
|
||||
voter_record::{get_voter_record_address, VoterRecord},
|
||||
},
|
||||
};
|
||||
|
||||
pub mod cookies;
|
||||
use self::cookies::{RealmCookie, VoterRecordCookie};
|
||||
|
||||
pub mod tools;
|
||||
use self::tools::map_transaction_error;
|
||||
|
||||
pub struct GovernanceProgramTest {
|
||||
pub banks_client: BanksClient,
|
||||
pub payer: Keypair,
|
||||
pub rent: Rent,
|
||||
}
|
||||
|
||||
impl GovernanceProgramTest {
|
||||
pub async fn start_new() -> Self {
|
||||
let program_test = ProgramTest::new(
|
||||
"spl_governance",
|
||||
spl_governance::id(),
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, _) = program_test.start().await;
|
||||
|
||||
let rent = banks_client.get_rent().await.unwrap();
|
||||
|
||||
Self {
|
||||
banks_client,
|
||||
payer,
|
||||
rent,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_transaction(
|
||||
&mut self,
|
||||
instructions: &[Instruction],
|
||||
signers: Option<&[&Keypair]>,
|
||||
) -> Result<(), ProgramError> {
|
||||
let mut transaction =
|
||||
Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
|
||||
|
||||
let mut all_signers = vec![&self.payer];
|
||||
|
||||
if let Some(signers) = signers {
|
||||
all_signers.extend_from_slice(signers);
|
||||
}
|
||||
|
||||
let recent_blockhash = self.banks_client.get_recent_blockhash().await.unwrap();
|
||||
|
||||
transaction.sign(&all_signers, recent_blockhash);
|
||||
|
||||
self.banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.map_err(map_transaction_error)
|
||||
}
|
||||
|
||||
pub async fn get_account<T: BorshDeserialize>(&mut self, address: &Pubkey) -> T {
|
||||
let raw_account = self
|
||||
.banks_client
|
||||
.get_account(*address)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("GET-TEST-ACCOUNT-ERROR: Account not found");
|
||||
|
||||
try_from_slice_unchecked(&raw_account.data).unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_realm(&mut self) -> RealmCookie {
|
||||
let name = "Realm".to_string();
|
||||
|
||||
let realm_address = get_realm_address(&name);
|
||||
|
||||
let community_token_mint_keypair = Keypair::new();
|
||||
let community_token_mint_authority = Keypair::new();
|
||||
|
||||
let community_token_holding_address = get_governing_token_holding_address(
|
||||
&realm_address,
|
||||
&community_token_mint_keypair.pubkey(),
|
||||
);
|
||||
|
||||
self.create_mint(
|
||||
&community_token_mint_keypair,
|
||||
&community_token_mint_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let council_token_mint_keypair = Keypair::new();
|
||||
let council_token_mint_authority = Keypair::new();
|
||||
|
||||
let council_token_holding_address = get_governing_token_holding_address(
|
||||
&realm_address,
|
||||
&council_token_mint_keypair.pubkey(),
|
||||
);
|
||||
|
||||
self.create_mint(
|
||||
&council_token_mint_keypair,
|
||||
&council_token_mint_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let create_proposal_instruction = create_realm(
|
||||
&community_token_mint_keypair.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
Some(council_token_mint_keypair.pubkey()),
|
||||
name.clone(),
|
||||
);
|
||||
|
||||
self.process_transaction(&[create_proposal_instruction], None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let account = Realm {
|
||||
account_type: GovernanceAccountType::Realm,
|
||||
community_mint: community_token_mint_keypair.pubkey(),
|
||||
council_mint: Some(council_token_mint_keypair.pubkey()),
|
||||
name,
|
||||
};
|
||||
|
||||
RealmCookie {
|
||||
address: realm_address,
|
||||
account,
|
||||
|
||||
community_mint_authority: community_token_mint_authority,
|
||||
community_token_holding_account: community_token_holding_address,
|
||||
|
||||
council_token_holding_account: Some(council_token_holding_address),
|
||||
council_mint_authority: Some(council_token_mint_authority),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_initial_community_token_deposit(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
) -> VoterRecordCookie {
|
||||
self.with_initial_governing_token_deposit(
|
||||
&realm_cookie.address,
|
||||
GoverningTokenType::Community,
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.community_mint_authority,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_community_token_deposit(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &VoterRecordCookie,
|
||||
amount: u64,
|
||||
) {
|
||||
self.with_governing_token_deposit(
|
||||
&realm_cookie.address,
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.community_mint_authority,
|
||||
voter_record_cookie,
|
||||
amount,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_council_token_deposit(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &VoterRecordCookie,
|
||||
amount: u64,
|
||||
) {
|
||||
self.with_governing_token_deposit(
|
||||
&realm_cookie.address,
|
||||
&realm_cookie.account.council_mint.unwrap(),
|
||||
&realm_cookie.council_mint_authority.as_ref().unwrap(),
|
||||
voter_record_cookie,
|
||||
amount,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_initial_council_token_deposit(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
) -> VoterRecordCookie {
|
||||
self.with_initial_governing_token_deposit(
|
||||
&realm_cookie.address,
|
||||
GoverningTokenType::Council,
|
||||
&realm_cookie.account.council_mint.unwrap(),
|
||||
&realm_cookie.council_mint_authority.as_ref().unwrap(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_initial_governing_token_deposit(
|
||||
&mut self,
|
||||
realm_address: &Pubkey,
|
||||
governing_token_type: GoverningTokenType,
|
||||
governing_mint: &Pubkey,
|
||||
governing_mint_authority: &Keypair,
|
||||
) -> VoterRecordCookie {
|
||||
let token_owner = Keypair::new();
|
||||
let token_source = Keypair::new();
|
||||
|
||||
let source_amount = 100;
|
||||
let transfer_authority = Keypair::new();
|
||||
|
||||
self.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
governing_mint,
|
||||
governing_mint_authority,
|
||||
source_amount,
|
||||
&token_owner,
|
||||
&transfer_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let deposit_governing_tokens_instruction = deposit_governing_tokens(
|
||||
realm_address,
|
||||
&token_source.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
governing_mint,
|
||||
);
|
||||
|
||||
self.process_transaction(
|
||||
&[deposit_governing_tokens_instruction],
|
||||
Some(&[&token_owner]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let voter_record_address =
|
||||
get_voter_record_address(realm_address, &governing_mint, &token_owner.pubkey());
|
||||
|
||||
let account = VoterRecord {
|
||||
account_type: GovernanceAccountType::VoterRecord,
|
||||
realm: *realm_address,
|
||||
token_type: governing_token_type,
|
||||
token_owner: token_owner.pubkey(),
|
||||
token_deposit_amount: source_amount,
|
||||
vote_authority: None,
|
||||
active_votes_count: 0,
|
||||
total_votes_count: 0,
|
||||
};
|
||||
|
||||
let vote_authority = Keypair::from_base58_string(&token_owner.to_base58_string());
|
||||
|
||||
VoterRecordCookie {
|
||||
address: voter_record_address,
|
||||
account,
|
||||
|
||||
token_source_amount: source_amount,
|
||||
token_source: token_source.pubkey(),
|
||||
token_owner,
|
||||
vote_authority,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn with_governing_token_deposit(
|
||||
&mut self,
|
||||
realm: &Pubkey,
|
||||
governing_token_mint: &Pubkey,
|
||||
governing_token_mint_authority: &Keypair,
|
||||
voter_record_cookie: &VoterRecordCookie,
|
||||
amount: u64,
|
||||
) {
|
||||
self.mint_tokens(
|
||||
governing_token_mint,
|
||||
governing_token_mint_authority,
|
||||
&voter_record_cookie.token_source,
|
||||
amount,
|
||||
)
|
||||
.await;
|
||||
|
||||
let deposit_governing_tokens_instruction = deposit_governing_tokens(
|
||||
realm,
|
||||
&voter_record_cookie.token_source,
|
||||
&voter_record_cookie.token_owner.pubkey(),
|
||||
&voter_record_cookie.token_owner.pubkey(),
|
||||
&self.payer.pubkey(),
|
||||
governing_token_mint,
|
||||
);
|
||||
|
||||
self.process_transaction(
|
||||
&[deposit_governing_tokens_instruction],
|
||||
Some(&[&voter_record_cookie.token_owner]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_community_vote_authority(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &mut VoterRecordCookie,
|
||||
) {
|
||||
self.with_governing_token_vote_authority(
|
||||
&realm_cookie,
|
||||
&realm_cookie.account.community_mint,
|
||||
voter_record_cookie,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_council_vote_authority(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &mut VoterRecordCookie,
|
||||
) {
|
||||
self.with_governing_token_vote_authority(
|
||||
&realm_cookie,
|
||||
&realm_cookie.account.council_mint.unwrap(),
|
||||
voter_record_cookie,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_governing_token_vote_authority(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
governing_token_mint: &Pubkey,
|
||||
voter_record_cookie: &mut VoterRecordCookie,
|
||||
) {
|
||||
let new_vote_authority = Keypair::new();
|
||||
|
||||
self.set_vote_authority(
|
||||
realm_cookie,
|
||||
voter_record_cookie,
|
||||
&voter_record_cookie.token_owner,
|
||||
governing_token_mint,
|
||||
&Some(new_vote_authority.pubkey()),
|
||||
)
|
||||
.await;
|
||||
|
||||
voter_record_cookie.vote_authority = new_vote_authority;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn set_vote_authority(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &VoterRecordCookie,
|
||||
signing_vote_authority: &Keypair,
|
||||
governing_token_mint: &Pubkey,
|
||||
new_vote_authority: &Option<Pubkey>,
|
||||
) {
|
||||
let set_vote_authority_instruction = set_vote_authority(
|
||||
&signing_vote_authority.pubkey(),
|
||||
&realm_cookie.address,
|
||||
governing_token_mint,
|
||||
&voter_record_cookie.token_owner.pubkey(),
|
||||
new_vote_authority,
|
||||
);
|
||||
|
||||
self.process_transaction(
|
||||
&[set_vote_authority_instruction],
|
||||
Some(&[&signing_vote_authority]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn withdraw_community_tokens(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &VoterRecordCookie,
|
||||
) -> Result<(), ProgramError> {
|
||||
self.withdraw_governing_tokens(
|
||||
realm_cookie,
|
||||
voter_record_cookie,
|
||||
&realm_cookie.account.community_mint,
|
||||
&voter_record_cookie.token_owner,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn withdraw_council_tokens(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &VoterRecordCookie,
|
||||
) -> Result<(), ProgramError> {
|
||||
self.withdraw_governing_tokens(
|
||||
realm_cookie,
|
||||
voter_record_cookie,
|
||||
&realm_cookie.account.council_mint.unwrap(),
|
||||
&voter_record_cookie.token_owner,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn withdraw_governing_tokens(
|
||||
&mut self,
|
||||
realm_cookie: &RealmCookie,
|
||||
voter_record_cookie: &VoterRecordCookie,
|
||||
governing_token_mint: &Pubkey,
|
||||
|
||||
governing_token_owner: &Keypair,
|
||||
) -> Result<(), ProgramError> {
|
||||
let deposit_governing_tokens_instruction = withdraw_governing_tokens(
|
||||
&realm_cookie.address,
|
||||
&voter_record_cookie.token_source,
|
||||
&governing_token_owner.pubkey(),
|
||||
governing_token_mint,
|
||||
);
|
||||
|
||||
self.process_transaction(
|
||||
&[deposit_governing_tokens_instruction],
|
||||
Some(&[&governing_token_owner]),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_voter_record_account(&mut self, address: &Pubkey) -> VoterRecord {
|
||||
self.get_account::<VoterRecord>(address).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_realm_account(&mut self, root_governance_address: &Pubkey) -> Realm {
|
||||
self.get_account::<Realm>(root_governance_address).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn get_packed_account<T: Pack + IsInitialized>(&mut self, address: &Pubkey) -> T {
|
||||
let raw_account = self
|
||||
.banks_client
|
||||
.get_account(*address)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
T::unpack(&raw_account.data).unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_token_account(&mut self, address: &Pubkey) -> spl_token::state::Account {
|
||||
self.get_packed_account(address).await
|
||||
}
|
||||
|
||||
pub async fn create_mint(&mut self, mint_keypair: &Keypair, mint_authority: &Pubkey) {
|
||||
let mint_rent = self.rent.minimum_balance(spl_token::state::Mint::LEN);
|
||||
|
||||
let instructions = [
|
||||
system_instruction::create_account(
|
||||
&self.payer.pubkey(),
|
||||
&mint_keypair.pubkey(),
|
||||
mint_rent,
|
||||
spl_token::state::Mint::LEN as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
spl_token::instruction::initialize_mint(
|
||||
&spl_token::id(),
|
||||
&mint_keypair.pubkey(),
|
||||
&mint_authority,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
.unwrap(),
|
||||
];
|
||||
|
||||
self.process_transaction(&instructions, Some(&[&mint_keypair]))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_token_account(
|
||||
&mut self,
|
||||
token_account_keypair: &Keypair,
|
||||
token_mint: &Pubkey,
|
||||
token_mint_authority: &Keypair,
|
||||
amount: u64,
|
||||
owner: &Pubkey,
|
||||
) {
|
||||
let create_account_instruction = system_instruction::create_account(
|
||||
&self.payer.pubkey(),
|
||||
&token_account_keypair.pubkey(),
|
||||
self.rent
|
||||
.minimum_balance(spl_token::state::Account::get_packed_len()),
|
||||
spl_token::state::Account::get_packed_len() as u64,
|
||||
&spl_token::id(),
|
||||
);
|
||||
|
||||
let initialize_account_instruction = spl_token::instruction::initialize_account(
|
||||
&spl_token::id(),
|
||||
&token_account_keypair.pubkey(),
|
||||
token_mint,
|
||||
&owner,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mint_instruction = spl_token::instruction::mint_to(
|
||||
&spl_token::id(),
|
||||
token_mint,
|
||||
&token_account_keypair.pubkey(),
|
||||
&token_mint_authority.pubkey(),
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.process_transaction(
|
||||
&[
|
||||
create_account_instruction,
|
||||
initialize_account_instruction,
|
||||
mint_instruction,
|
||||
],
|
||||
Some(&[&token_account_keypair, &token_mint_authority]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_token_account_with_transfer_authority(
|
||||
&mut self,
|
||||
token_account_keypair: &Keypair,
|
||||
token_mint: &Pubkey,
|
||||
token_mint_authority: &Keypair,
|
||||
amount: u64,
|
||||
owner: &Keypair,
|
||||
transfer_authority: &Pubkey,
|
||||
) {
|
||||
let create_account_instruction = system_instruction::create_account(
|
||||
&self.payer.pubkey(),
|
||||
&token_account_keypair.pubkey(),
|
||||
self.rent
|
||||
.minimum_balance(spl_token::state::Account::get_packed_len()),
|
||||
spl_token::state::Account::get_packed_len() as u64,
|
||||
&spl_token::id(),
|
||||
);
|
||||
|
||||
let initialize_account_instruction = spl_token::instruction::initialize_account(
|
||||
&spl_token::id(),
|
||||
&token_account_keypair.pubkey(),
|
||||
token_mint,
|
||||
&owner.pubkey(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mint_instruction = spl_token::instruction::mint_to(
|
||||
&spl_token::id(),
|
||||
token_mint,
|
||||
&token_account_keypair.pubkey(),
|
||||
&token_mint_authority.pubkey(),
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let approve_instruction = spl_token::instruction::approve(
|
||||
&spl_token::id(),
|
||||
&token_account_keypair.pubkey(),
|
||||
transfer_authority,
|
||||
&owner.pubkey(),
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.process_transaction(
|
||||
&[
|
||||
create_account_instruction,
|
||||
initialize_account_instruction,
|
||||
mint_instruction,
|
||||
approve_instruction,
|
||||
],
|
||||
Some(&[&token_account_keypair, &token_mint_authority, &owner]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn mint_tokens(
|
||||
&mut self,
|
||||
token_mint: &Pubkey,
|
||||
token_mint_authority: &Keypair,
|
||||
token_account: &Pubkey,
|
||||
amount: u64,
|
||||
) {
|
||||
let mint_instruction = spl_token::instruction::mint_to(
|
||||
&spl_token::id(),
|
||||
&token_mint,
|
||||
&token_account,
|
||||
&token_mint_authority.pubkey(),
|
||||
&[],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.process_transaction(&[mint_instruction], Some(&[&token_mint_authority]))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
use solana_program::{instruction::InstructionError, program_error::ProgramError};
|
||||
use solana_sdk::{transaction::TransactionError, transport::TransportError};
|
||||
|
||||
pub fn map_transaction_error(transport_error: TransportError) -> ProgramError {
|
||||
match transport_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => ProgramError::Custom(error_index),
|
||||
_ => panic!("TEST-ERROR: {:?}", transport_error),
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue