diff --git a/Cargo.lock b/Cargo.lock index 6fb54efe047..16893b129f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3628,7 +3628,6 @@ dependencies = [ "base64 0.13.0", "bincode", "borsh", - "lazy_static", "num-derive", "num-traits", "proptest", @@ -3645,7 +3644,7 @@ dependencies = [ [[package]] name = "spl-governance-chat" -version = "0.1.0" +version = "0.2.0" dependencies = [ "arrayref", "assert_matches", @@ -3663,6 +3662,7 @@ dependencies = [ "spl-governance", "spl-governance-test-sdk", "spl-governance-tools", + "spl-governance-voter-weight-addin", "spl-token 3.3.0", "thiserror", ] @@ -3674,6 +3674,7 @@ dependencies = [ "arrayref", "bincode", "borsh", + "lazy_static", "num-derive", "num-traits", "serde", diff --git a/governance/chat/program/Cargo.toml b/governance/chat/program/Cargo.toml index d5303b470d7..7912ee5bcfb 100644 --- a/governance/chat/program/Cargo.toml +++ b/governance/chat/program/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spl-governance-chat" -version = "0.1.0" +version = "0.2.0" description = "Solana Program Library Governance Chat Program" authors = ["Solana Maintainers "] repository = "https://github.com/solana-labs/solana-program-library" @@ -21,7 +21,7 @@ serde = "1.0.127" serde_derive = "1.0.103" solana-program = "1.9.5" spl-token = { version = "3.3", path = "../../../token/program", features = [ "no-entrypoint" ] } -spl-governance= { version = "2.1.3", path ="../../program", features = [ "no-entrypoint" ]} +spl-governance= { version = "2.1.5", path ="../../program", features = [ "no-entrypoint" ]} spl-governance-tools= { version = "0.1.0", path ="../../tools"} thiserror = "1.0" @@ -33,6 +33,7 @@ proptest = "1.0" solana-program-test = "1.9.5" solana-sdk = "1.9.5" spl-governance-test-sdk = { version = "0.1.0", path ="../../test-sdk"} +spl-governance-voter-weight-addin = { version = "0.1.0", path ="../../voter-weight-addin/program"} [lib] diff --git a/governance/chat/program/src/instruction.rs b/governance/chat/program/src/instruction.rs index b5d90411611..369804fb377 100644 --- a/governance/chat/program/src/instruction.rs +++ b/governance/chat/program/src/instruction.rs @@ -6,6 +6,7 @@ use solana_program::{ pubkey::Pubkey, system_program, }; +use spl_governance::instruction::with_voter_weight_accounts; use crate::state::MessageBody; @@ -16,18 +17,25 @@ pub enum GovernanceChatInstruction { /// Posts a message with a comment for a Proposal /// /// 0. `[]` Governance program id - /// 1. `[]` Governance account the Proposal is for - /// 2. `[]` Proposal account - /// 3. `[]` TokenOwnerRecord account for the message author - /// 4. `[signer]` Governance Authority (TokenOwner or Governance Delegate) - /// 5. `[writable, signer]` ChatMessage account - /// 6. `[signer]` Payer - /// 7. `[]` System program - /// 8. `[]` ReplyTo Message account (optional) + /// 1. `[]` Realm account of the Proposal + /// 2. `[]` Governance account the Proposal is for + /// 3. `[]` Proposal account + /// 4. `[]` TokenOwnerRecord account for the message author + /// 5. `[signer]` Governance Authority (TokenOwner or Governance Delegate) + /// 6. `[writable, signer]` ChatMessage account + /// 7. `[signer]` Payer + /// 8. `[]` System program + /// 9. `[]` ReplyTo Message account (optional) + /// 10. `[]` Optional Voter Weight Record PostMessage { #[allow(dead_code)] /// Message body (text or reaction) body: MessageBody, + + #[allow(dead_code)] + /// Indicates whether the message is a reply to another message + /// If yes then ReplyTo Message account has to be provided + is_reply: bool, }, } @@ -37,6 +45,7 @@ pub fn post_message( program_id: &Pubkey, // Accounts governance_program_id: &Pubkey, + realm: &Pubkey, governance: &Pubkey, proposal: &Pubkey, token_owner_record: &Pubkey, @@ -44,11 +53,13 @@ pub fn post_message( reply_to: Option, chat_message: &Pubkey, payer: &Pubkey, + voter_weight_record: Option, // Args body: MessageBody, ) -> Instruction { let mut accounts = vec![ AccountMeta::new_readonly(*governance_program_id, false), + AccountMeta::new_readonly(*realm, false), AccountMeta::new_readonly(*governance, false), AccountMeta::new_readonly(*proposal, false), AccountMeta::new_readonly(*token_owner_record, false), @@ -58,11 +69,21 @@ pub fn post_message( AccountMeta::new_readonly(system_program::id(), false), ]; - if let Some(reply_to) = reply_to { + let is_reply = if let Some(reply_to) = reply_to { accounts.push(AccountMeta::new_readonly(reply_to, false)); - } + true + } else { + false + }; + + with_voter_weight_accounts( + governance_program_id, + &mut accounts, + realm, + voter_weight_record, + ); - let instruction = GovernanceChatInstruction::PostMessage { body }; + let instruction = GovernanceChatInstruction::PostMessage { body, is_reply }; Instruction { program_id: *program_id, diff --git a/governance/chat/program/src/processor.rs b/governance/chat/program/src/processor.rs index 90c7a1f3959..17dc33376de 100644 --- a/governance/chat/program/src/processor.rs +++ b/governance/chat/program/src/processor.rs @@ -17,8 +17,8 @@ use solana_program::{ sysvar::Sysvar, }; use spl_governance::state::{ - governance::get_governance_data, proposal::get_proposal_data_for_governance, - token_owner_record::get_token_owner_record_data_for_realm, + governance::get_governance_data_for_realm, proposal::get_proposal_data_for_governance, + realm::get_realm_data, token_owner_record::get_token_owner_record_data_for_realm, }; use spl_governance_tools::account::create_and_serialize_account; @@ -28,13 +28,15 @@ pub fn process_instruction( accounts: &[AccountInfo], input: &[u8], ) -> ProgramResult { + msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); + let instruction = GovernanceChatInstruction::try_from_slice(input) .map_err(|_| ProgramError::InvalidInstructionData)?; match instruction { - GovernanceChatInstruction::PostMessage { body } => { + GovernanceChatInstruction::PostMessage { body, is_reply } => { msg!("GOVERNANCE-CHAT-INSTRUCTION: PostMessage"); - process_post_message(program_id, accounts, body) + process_post_message(program_id, accounts, body, is_reply) } } } @@ -44,33 +46,38 @@ pub fn process_post_message( program_id: &Pubkey, accounts: &[AccountInfo], body: MessageBody, + is_reply: bool, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let governance_program_info = next_account_info(account_info_iter)?; // 0 - let governance_info = next_account_info(account_info_iter)?; // 1 - let proposal_info = next_account_info(account_info_iter)?; // 2 - let token_owner_record_info = next_account_info(account_info_iter)?; // 3 - let governance_authority_info = next_account_info(account_info_iter)?; // 4 - - let chat_message_info = next_account_info(account_info_iter)?; // 5 + let realm_info = next_account_info(account_info_iter)?; // 1 + let governance_info = next_account_info(account_info_iter)?; // 2 + let proposal_info = next_account_info(account_info_iter)?; // 3 + let token_owner_record_info = next_account_info(account_info_iter)?; // 4 + let governance_authority_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 chat_message_info = next_account_info(account_info_iter)?; // 6 - let reply_to_info = next_account_info(account_info_iter); // 8 + let payer_info = next_account_info(account_info_iter)?; // 7 + let system_info = next_account_info(account_info_iter)?; // 8 - let reply_to_address = if let Ok(reply_to_info) = reply_to_info { + let reply_to_address = if is_reply { + let reply_to_info = next_account_info(account_info_iter)?; // 9 assert_is_valid_chat_message(program_id, reply_to_info)?; Some(*reply_to_info.key) } else { None }; - let governance_data = get_governance_data(governance_program_info.key, governance_info)?; + let governance_program_id = governance_program_info.key; + let realm_data = get_realm_data(governance_program_id, realm_info)?; + + let governance_data = + get_governance_data_for_realm(governance_program_id, governance_info, realm_info.key)?; let token_owner_record_data = get_token_owner_record_data_for_realm( - governance_program_info.key, + governance_program_id, token_owner_record_info, &governance_data.realm, )?; @@ -79,15 +86,22 @@ pub fn process_post_message( // deserialize proposal to assert it belongs to the given governance and hence belongs to the same realm as the token owner let _proposal_data = get_proposal_data_for_governance( - governance_program_info.key, + governance_program_id, proposal_info, governance_info.key, )?; - // The owner needs to have at least 1 governing token to comment on proposals + let voter_weight = token_owner_record_data.resolve_voter_weight( + governance_program_id, + account_info_iter, // 10 + realm_info.key, + &realm_data, + )?; + + // The owner needs to have at least voter weight of 1 to comment on proposals // Note: It can be either community or council token and is irrelevant to the proposal's governing token // Note: 1 is currently hardcoded but if different level is required then it should be added to realm config - if token_owner_record_data.governing_token_deposit_amount < 1 { + if voter_weight < 1 { return Err(GovernanceChatError::NotEnoughTokensToCommentProposal.into()); } diff --git a/governance/chat/program/tests/process_post_message.rs b/governance/chat/program/tests/process_post_message.rs index 49f6875511d..fa0945a23ea 100644 --- a/governance/chat/program/tests/process_post_message.rs +++ b/governance/chat/program/tests/process_post_message.rs @@ -128,3 +128,24 @@ async fn test_post_message_with_not_enough_tokens_error() { GovernanceChatError::NotEnoughTokensToCommentProposal.into() ); } + +#[tokio::test] +async fn test_post_message_with_voter_weight_addin() { + // Arrange + let mut governance_chat_test = GovernanceChatProgramTest::start_with_voter_weight_addin().await; + + let proposal_cookie = governance_chat_test.with_proposal().await; + + // Act + let chat_message_cookie = governance_chat_test + .with_chat_message(&proposal_cookie, None) + .await + .unwrap(); + + // Assert + let chat_message_data = governance_chat_test + .get_message_account(&chat_message_cookie.address) + .await; + + assert_eq!(chat_message_data, chat_message_cookie.account); +} diff --git a/governance/chat/program/tests/program_test/cookies.rs b/governance/chat/program/tests/program_test/cookies.rs index db26774d3c1..deac7b87743 100644 --- a/governance/chat/program/tests/program_test/cookies.rs +++ b/governance/chat/program/tests/program_test/cookies.rs @@ -18,6 +18,8 @@ pub struct ProposalCookie { pub governing_token_mint: Pubkey, pub governing_token_mint_authority: Keypair, + + pub voter_weight_record: Option, } #[derive(Debug)] diff --git a/governance/chat/program/tests/program_test/mod.rs b/governance/chat/program/tests/program_test/mod.rs index 4e63b3b2c39..dbde55114d2 100644 --- a/governance/chat/program/tests/program_test/mod.rs +++ b/governance/chat/program/tests/program_test/mod.rs @@ -6,7 +6,8 @@ use solana_program_test::{processor, ProgramTest}; use solana_sdk::{signature::Keypair, signer::Signer}; use spl_governance::{ instruction::{ - create_account_governance, create_proposal, create_realm, deposit_governing_tokens, + create_account_governance, create_proposal, create_realm, create_token_owner_record, + deposit_governing_tokens, }, state::{ enums::{MintMaxVoteWeightSource, VoteThresholdPercentage}, @@ -21,7 +22,8 @@ use spl_governance_chat::{ processor::process_instruction, state::{ChatMessage, GovernanceChatAccountType, MessageBody}, }; -use spl_governance_test_sdk::ProgramTestBench; +use spl_governance_test_sdk::{addins::ensure_voter_weight_addin_is_built, ProgramTestBench}; +use spl_governance_voter_weight_addin::instruction::deposit_voter_weight; use crate::program_test::cookies::{ChatMessageCookie, ProposalCookie}; @@ -33,11 +35,26 @@ pub struct GovernanceChatProgramTest { pub bench: ProgramTestBench, pub program_id: Pubkey, pub governance_program_id: Pubkey, + pub voter_weight_addin_id: Option, } impl GovernanceChatProgramTest { + #[allow(dead_code)] pub async fn start_new() -> Self { + Self::start_impl(false).await + } + + #[allow(dead_code)] + pub async fn start_with_voter_weight_addin() -> Self { + ensure_voter_weight_addin_is_built(); + + Self::start_impl(true).await + } + + #[allow(dead_code)] + async fn start_impl(use_voter_weight_addin: bool) -> Self { let mut program_test = ProgramTest::default(); + let program_id = Pubkey::from_str("GovernanceChat11111111111111111111111111111").unwrap(); program_test.add_program( "spl_governance_chat", @@ -52,12 +69,27 @@ impl GovernanceChatProgramTest { governance_program_id, processor!(spl_governance::processor::process_instruction), ); + + let voter_weight_addin_id = if use_voter_weight_addin { + let voter_weight_addin_id = + Pubkey::from_str("VoterWeight11111111111111111111111111111111").unwrap(); + program_test.add_program( + "spl_governance_voter_weight_addin", + voter_weight_addin_id, + None, + ); + Some(voter_weight_addin_id) + } else { + None + }; + let bench = ProgramTestBench::start_new(program_test).await; Self { bench, program_id, governance_program_id, + voter_weight_addin_id, } } @@ -86,7 +118,7 @@ impl GovernanceChatProgramTest { &governing_token_mint_keypair.pubkey(), &self.bench.payer.pubkey(), None, - None, + self.voter_weight_addin_id, name.clone(), 1, MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION, @@ -100,36 +132,51 @@ impl GovernanceChatProgramTest { // Create TokenOwnerRecord let token_owner = Keypair::new(); let token_source = Keypair::new(); - - let transfer_authority = Keypair::new(); let amount = 100; - self.bench - .create_token_account_with_transfer_authority( - &token_source, - &governing_token_mint_keypair.pubkey(), - &governing_token_mint_authority, + if self.voter_weight_addin_id.is_none() { + let transfer_authority = Keypair::new(); + + self.bench + .create_token_account_with_transfer_authority( + &token_source, + &governing_token_mint_keypair.pubkey(), + &governing_token_mint_authority, + amount, + &token_owner, + &transfer_authority.pubkey(), + ) + .await; + + let deposit_governing_tokens_ix = deposit_governing_tokens( + &self.governance_program_id, + &realm_address, + &token_source.pubkey(), + &token_owner.pubkey(), + &token_owner.pubkey(), + &self.bench.payer.pubkey(), amount, - &token_owner, - &transfer_authority.pubkey(), - ) - .await; - - let deposit_governing_tokens_ix = deposit_governing_tokens( - &self.governance_program_id, - &realm_address, - &token_source.pubkey(), - &token_owner.pubkey(), - &token_owner.pubkey(), - &self.bench.payer.pubkey(), - amount, - &governing_token_mint_keypair.pubkey(), - ); + &governing_token_mint_keypair.pubkey(), + ); + + self.bench + .process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner])) + .await + .unwrap(); + } else { + let deposit_governing_tokens_ix = create_token_owner_record( + &self.governance_program_id, + &realm_address, + &token_owner.pubkey(), + &governing_token_mint_keypair.pubkey(), + &self.bench.payer.pubkey(), + ); - self.bench - .process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner])) - .await - .unwrap(); + self.bench + .process_transaction(&[deposit_governing_tokens_ix], None) + .await + .unwrap(); + } // Create Governance let governed_account_address = Pubkey::new_unique(); @@ -151,6 +198,29 @@ impl GovernanceChatProgramTest { &token_owner.pubkey(), ); + let voter_weight_record = if self.voter_weight_addin_id.is_some() { + let voter_weight_record = Keypair::new(); + let deposit_voter_weight_ix = deposit_voter_weight( + &self.voter_weight_addin_id.unwrap(), + &self.governance_program_id, + &realm_address, + &governing_token_mint_keypair.pubkey(), + &token_owner_record_address, + &voter_weight_record.pubkey(), + &self.bench.payer.pubkey(), + amount, + ); + + self.bench + .process_transaction(&[deposit_voter_weight_ix], Some(&[&voter_weight_record])) + .await + .unwrap(); + + Some(voter_weight_record.pubkey()) + } else { + None + }; + let create_account_governance_ix = create_account_governance( &self.governance_program_id, &realm_address, @@ -158,7 +228,7 @@ impl GovernanceChatProgramTest { &token_owner_record_address, &self.bench.payer.pubkey(), &token_owner.pubkey(), - None, + voter_weight_record, governance_config, ); @@ -187,7 +257,7 @@ impl GovernanceChatProgramTest { &token_owner_record_address, &token_owner.pubkey(), &self.bench.payer.pubkey(), - None, + voter_weight_record, &realm_address, proposal_name, description_link.clone(), @@ -218,6 +288,7 @@ impl GovernanceChatProgramTest { token_owner, governing_token_mint: governing_token_mint_keypair.pubkey(), governing_token_mint_authority: governing_token_mint_authority, + voter_weight_record, } } @@ -283,6 +354,7 @@ impl GovernanceChatProgramTest { let post_message_ix = post_message( &self.program_id, &self.governance_program_id, + &proposal_cookie.realm_address, &proposal_cookie.governance_address, &proposal_cookie.address, &proposal_cookie.token_owner_record_address, @@ -290,6 +362,7 @@ impl GovernanceChatProgramTest { reply_to, &message_account.pubkey(), &self.bench.payer.pubkey(), + proposal_cookie.voter_weight_record, message_body.clone(), ); diff --git a/governance/program/Cargo.toml b/governance/program/Cargo.toml index c1b7bcdd029..5c8662f350a 100644 --- a/governance/program/Cargo.toml +++ b/governance/program/Cargo.toml @@ -27,7 +27,6 @@ thiserror = "1.0" [dev-dependencies] assert_matches = "1.5.0" base64 = "0.13" -lazy_static = "1.4.0" proptest = "1.0" solana-program-test = "1.9.5" solana-sdk = "1.9.5" diff --git a/governance/program/src/processor/mod.rs b/governance/program/src/processor/mod.rs index 4c864ddf483..e3b00e71d09 100644 --- a/governance/program/src/processor/mod.rs +++ b/governance/program/src/processor/mod.rs @@ -67,6 +67,7 @@ pub fn process_instruction( accounts: &[AccountInfo], input: &[u8], ) -> ProgramResult { + msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); // Use try_from_slice_unchecked to support forward compatibility of newer UI with older program let instruction: GovernanceInstruction = try_from_slice_unchecked(input).map_err(|_| ProgramError::InvalidInstructionData)?; diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index 6d54a309ef0..7a171ab25cd 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -54,7 +54,6 @@ use spl_governance::{ tools::bpf_loader_upgradeable::get_program_data_address, }; -pub mod addins; pub mod cookies; use crate::program_test::cookies::{ @@ -62,18 +61,16 @@ use crate::program_test::cookies::{ }; use spl_governance_test_sdk::{ + addins::ensure_voter_weight_addin_is_built, cookies::WalletCookie, tools::{clone_keypair, NopOverride}, ProgramTestBench, }; -use self::{ - addins::ensure_voter_weight_addin_is_built, - cookies::{ - GovernanceCookie, GovernedAccountCookie, GovernedMintCookie, GovernedProgramCookie, - GovernedTokenCookie, NativeTreasuryCookie, ProgramMetadataCookie, ProposalCookie, - ProposalInstructionCookie, RealmCookie, TokenOwnerRecordCookie, VoteRecordCookie, - }, +use self::cookies::{ + GovernanceCookie, GovernedAccountCookie, GovernedMintCookie, GovernedProgramCookie, + GovernedTokenCookie, NativeTreasuryCookie, ProgramMetadataCookie, ProposalCookie, + ProposalInstructionCookie, RealmCookie, TokenOwnerRecordCookie, VoteRecordCookie, }; /// Yes/No Vote diff --git a/governance/test-sdk/Cargo.toml b/governance/test-sdk/Cargo.toml index 41146ead6be..5df3b1a4c46 100644 --- a/governance/test-sdk/Cargo.toml +++ b/governance/test-sdk/Cargo.toml @@ -11,6 +11,7 @@ edition = "2018" arrayref = "0.3.6" bincode = "1.3.2" borsh = "0.9.1" +lazy_static = "1.4.0" num-derive = "0.3" num-traits = "0.2" serde = "1.0.127" @@ -20,3 +21,5 @@ solana-program-test = "1.9.5" solana-sdk = "1.9.5" spl-token = { version = "3.3", path = "../../token/program", features = [ "no-entrypoint" ] } thiserror = "1.0" + + diff --git a/governance/program/tests/program_test/addins.rs b/governance/test-sdk/src/addins.rs similarity index 100% rename from governance/program/tests/program_test/addins.rs rename to governance/test-sdk/src/addins.rs diff --git a/governance/test-sdk/src/lib.rs b/governance/test-sdk/src/lib.rs index 596a74d9ece..8aa2f2b60be 100644 --- a/governance/test-sdk/src/lib.rs +++ b/governance/test-sdk/src/lib.rs @@ -16,6 +16,7 @@ use tools::clone_keypair; use crate::tools::map_transaction_error; +pub mod addins; pub mod cookies; pub mod tools; diff --git a/governance/voter-weight-addin/program/src/instruction.rs b/governance/voter-weight-addin/program/src/instruction.rs index b94ebe599ba..f378b695794 100644 --- a/governance/voter-weight-addin/program/src/instruction.rs +++ b/governance/voter-weight-addin/program/src/instruction.rs @@ -1,6 +1,11 @@ //! Program instructions use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; /// Instructions supported by the VoterWeightInstruction addin program #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] @@ -40,3 +45,36 @@ pub enum VoterWeightAddinInstruction { /// 4. `[writable]` VoterWeightRecord Withdraw {}, } + +/// Creates Deposit instruction +#[allow(clippy::too_many_arguments)] +pub fn deposit_voter_weight( + program_id: &Pubkey, + // Accounts + governance_program_id: &Pubkey, + realm: &Pubkey, + governing_token_mint: &Pubkey, + token_owner_record: &Pubkey, + voter_weight_record: &Pubkey, + payer: &Pubkey, + // Args + amount: u64, +) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*governance_program_id, false), + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(*governing_token_mint, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new(*voter_weight_record, true), + AccountMeta::new_readonly(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let instruction = VoterWeightAddinInstruction::Deposit { amount }; + + Instruction { + program_id: *program_id, + accounts, + data: instruction.try_to_vec().unwrap(), + } +}