Governance: Chat program (#2319)
* chore: create test-sdk crate * chore: create test-sdk crate * chore: use shred test bench to run tests * chore: scaffold governance chat program * chore: move process_transaction to bench * chore: add with_proposal factory function * chore: move token utility functions to bench * chore: create test realm * chore: move create_token_account_with_transfer_authority to bench * chore: create test governance * chore: create test proposal * feat: implement process_post_message * chore: move get_clock to bench * chore: add clock and message author * chore: move get_borsh_account and get_account to bench * feat: add reaply_to * chore: update comments * chore: update comments and make clippy happy * chore: add reply to test * chore: assert is valid replyTo chat message * chore: add error test cases * chore: add not enough tokens error test * chore: update Reaction comment * feat: assert new account is not initialised * chore: upgrade chat version * chore: add license * chore: update cargo lock * chore: fix solana version * chore: update cargo.lock * chore: move chat program inside governance folder
This commit is contained in:
parent
cd696da5a4
commit
b3684bbfe9
|
@ -3659,6 +3659,48 @@ dependencies = [
|
|||
"solana-program",
|
||||
"solana-program-test",
|
||||
"solana-sdk",
|
||||
"spl-governance-test-sdk",
|
||||
"spl-token 3.2.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-governance-chat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"assert_matches",
|
||||
"base64 0.13.0",
|
||||
"bincode",
|
||||
"borsh",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-program",
|
||||
"solana-program-test",
|
||||
"solana-sdk",
|
||||
"spl-governance",
|
||||
"spl-governance-test-sdk",
|
||||
"spl-token 3.2.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spl-governance-test-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bincode",
|
||||
"borsh",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"solana-program",
|
||||
"solana-program-test",
|
||||
"solana-sdk",
|
||||
"spl-token 3.2.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
|
|
@ -11,6 +11,8 @@ members = [
|
|||
"feature-proposal/program",
|
||||
"feature-proposal/cli",
|
||||
"governance/program",
|
||||
"governance/test-sdk",
|
||||
"governance/chat/program",
|
||||
"libraries/math",
|
||||
"memo/program",
|
||||
"name-service/program",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# Governance chat
|
||||
|
||||
Governance chat is a program which allows voters to comment on proposals.
|
||||
All comments are public and stored on chain.
|
|
@ -0,0 +1,38 @@
|
|||
[package]
|
||||
name = "spl-governance-chat"
|
||||
version = "0.1.0"
|
||||
description = "Solana Program Library Governance Chat Program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
arrayref = "0.3.6"
|
||||
bincode = "1.3.2"
|
||||
borsh = "0.9.1"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serde = "1.0.127"
|
||||
serde_derive = "1.0.103"
|
||||
solana-program = "1.7.11"
|
||||
spl-token = { version = "3.2", path = "../../../token/program", features = [ "no-entrypoint" ] }
|
||||
spl-governance= { version = "1.1.0", path ="../../program", features = [ "no-entrypoint" ]}
|
||||
thiserror = "1.0"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
base64 = "0.13"
|
||||
proptest = "1.0"
|
||||
solana-program-test = "1.7.11"
|
||||
solana-sdk = "1.7.11"
|
||||
spl-governance-test-sdk = { version = "0.1.0", path ="../../test-sdk"}
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,22 @@
|
|||
//! Program entrypoint
|
||||
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
|
||||
|
||||
use crate::{error::GovernanceChatError, processor};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
|
||||
program_error::PrintProgramError, pubkey::Pubkey,
|
||||
};
|
||||
|
||||
entrypoint!(process_instruction);
|
||||
fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
instruction_data: &[u8],
|
||||
) -> ProgramResult {
|
||||
if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) {
|
||||
// catch the error so we can print it
|
||||
error.print::<GovernanceChatError>();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
//! Error types
|
||||
|
||||
use num_derive::FromPrimitive;
|
||||
use solana_program::{
|
||||
decode_error::DecodeError,
|
||||
msg,
|
||||
program_error::{PrintProgramError, ProgramError},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that may be returned by the GovernanceChat program
|
||||
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
|
||||
pub enum GovernanceChatError {
|
||||
/// Owner doesn't have enough governing tokens to comment on Proposal
|
||||
#[error("Owner doesn't have enough governing tokens to comment on Proposal")]
|
||||
NotEnoughTokensToCommentProposal = 900,
|
||||
|
||||
/// Account already initialized
|
||||
#[error("Account already initialized")]
|
||||
AccountAlreadyInitialized,
|
||||
}
|
||||
|
||||
impl PrintProgramError for GovernanceChatError {
|
||||
fn print<E>(&self) {
|
||||
msg!("GOVERNANCE-CHAT-ERROR: {}", &self.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GovernanceChatError> for ProgramError {
|
||||
fn from(e: GovernanceChatError) -> Self {
|
||||
ProgramError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DecodeError<T> for GovernanceChatError {
|
||||
fn type_of() -> &'static str {
|
||||
"Governance Chat Error"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
//! Program instructions
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
system_program,
|
||||
};
|
||||
|
||||
use crate::state::MessageBody;
|
||||
|
||||
/// Instructions supported by the GovernanceChat program
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
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)
|
||||
PostMessage {
|
||||
#[allow(dead_code)]
|
||||
/// Message body (text or reaction)
|
||||
body: MessageBody,
|
||||
},
|
||||
}
|
||||
|
||||
/// Creates PostMessage instruction
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn post_message(
|
||||
program_id: &Pubkey,
|
||||
// Accounts
|
||||
governance_program_id: &Pubkey,
|
||||
governance: &Pubkey,
|
||||
proposal: &Pubkey,
|
||||
token_owner_record: &Pubkey,
|
||||
governance_authority: &Pubkey,
|
||||
reply_to: Option<Pubkey>,
|
||||
chat_message: &Pubkey,
|
||||
payer: &Pubkey,
|
||||
// Args
|
||||
body: MessageBody,
|
||||
) -> Instruction {
|
||||
let mut accounts = vec![
|
||||
AccountMeta::new_readonly(*governance_program_id, false),
|
||||
AccountMeta::new_readonly(*governance, false),
|
||||
AccountMeta::new_readonly(*proposal, false),
|
||||
AccountMeta::new_readonly(*token_owner_record, false),
|
||||
AccountMeta::new_readonly(*governance_authority, true),
|
||||
AccountMeta::new(*chat_message, true),
|
||||
AccountMeta::new_readonly(*payer, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
];
|
||||
|
||||
if let Some(reply_to) = reply_to {
|
||||
accounts.push(AccountMeta::new_readonly(reply_to, false));
|
||||
}
|
||||
|
||||
let instruction = GovernanceChatInstruction::PostMessage { body };
|
||||
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: instruction.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
#![deny(missing_docs)]
|
||||
//! Governance Chat program
|
||||
|
||||
pub mod entrypoint;
|
||||
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;
|
|
@ -0,0 +1,114 @@
|
|||
//! Program processor
|
||||
|
||||
use crate::{
|
||||
error::GovernanceChatError,
|
||||
instruction::GovernanceChatInstruction,
|
||||
state::{assert_is_valid_chat_message, ChatMessage, GovernanceChatAccountType, MessageBody},
|
||||
tools::account::create_and_serialize_account,
|
||||
};
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
use solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
clock::Clock,
|
||||
entrypoint::ProgramResult,
|
||||
msg,
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
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,
|
||||
};
|
||||
|
||||
/// Processes an instruction
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
input: &[u8],
|
||||
) -> ProgramResult {
|
||||
let instruction = GovernanceChatInstruction::try_from_slice(input)
|
||||
.map_err(|_| ProgramError::InvalidInstructionData)?;
|
||||
|
||||
match instruction {
|
||||
GovernanceChatInstruction::PostMessage { body } => {
|
||||
msg!("GOVERNANCE-CHAT-INSTRUCTION: PostMessage");
|
||||
process_post_message(program_id, accounts, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes PostMessage instruction
|
||||
pub fn process_post_message(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
body: MessageBody,
|
||||
) -> 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 payer_info = next_account_info(account_info_iter)?; // 6
|
||||
let system_info = next_account_info(account_info_iter)?; // 7
|
||||
|
||||
let reply_to_info = next_account_info(account_info_iter); // 8
|
||||
|
||||
let reply_to_address = if let Ok(reply_to_info) = reply_to_info {
|
||||
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 token_owner_record_data = get_token_owner_record_data_for_realm(
|
||||
governance_program_info.key,
|
||||
token_owner_record_info,
|
||||
&governance_data.realm,
|
||||
)?;
|
||||
|
||||
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
|
||||
|
||||
// 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,
|
||||
proposal_info,
|
||||
governance_info.key,
|
||||
)?;
|
||||
|
||||
// The owner needs to have at least 1 governing token 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 {
|
||||
return Err(GovernanceChatError::NotEnoughTokensToCommentProposal.into());
|
||||
}
|
||||
|
||||
let clock = Clock::get()?;
|
||||
|
||||
let chat_message_data = ChatMessage {
|
||||
account_type: GovernanceChatAccountType::ChatMessage,
|
||||
proposal: *proposal_info.key,
|
||||
author: token_owner_record_data.governing_token_owner,
|
||||
posted_at: clock.unix_timestamp,
|
||||
reply_to: reply_to_address,
|
||||
body,
|
||||
};
|
||||
|
||||
create_and_serialize_account(
|
||||
payer_info,
|
||||
chat_message_info,
|
||||
&chat_message_data,
|
||||
program_id,
|
||||
system_info,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//! Program state
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, clock::UnixTimestamp, program_error::ProgramError, pubkey::Pubkey,
|
||||
};
|
||||
use spl_governance::tools::account::{assert_is_valid_account, AccountMaxSize};
|
||||
|
||||
/// Defines all GovernanceChat accounts types
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum GovernanceChatAccountType {
|
||||
/// Default uninitialized account state
|
||||
Uninitialized,
|
||||
|
||||
/// Chat message
|
||||
ChatMessage,
|
||||
}
|
||||
|
||||
/// Chat message body
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum MessageBody {
|
||||
/// Text message encoded as utf-8 string
|
||||
Text(String),
|
||||
|
||||
/// Emoticon encoded using utf-8 characters
|
||||
/// In the UI reactions are displayed together under the parent message (as opposed to hierarchical replies)
|
||||
Reaction(String),
|
||||
}
|
||||
|
||||
/// Chat message
|
||||
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct ChatMessage {
|
||||
/// Account type
|
||||
pub account_type: GovernanceChatAccountType,
|
||||
|
||||
/// The proposal the message is for
|
||||
pub proposal: Pubkey,
|
||||
|
||||
/// Author of the message
|
||||
pub author: Pubkey,
|
||||
|
||||
/// Message timestamp
|
||||
pub posted_at: UnixTimestamp,
|
||||
|
||||
/// Parent message
|
||||
pub reply_to: Option<Pubkey>,
|
||||
|
||||
/// Body of the message
|
||||
pub body: MessageBody,
|
||||
}
|
||||
|
||||
impl AccountMaxSize for ChatMessage {
|
||||
fn get_max_size(&self) -> Option<usize> {
|
||||
let body_size = match self.body.clone() {
|
||||
MessageBody::Text(body) => body.len(),
|
||||
MessageBody::Reaction(body) => body.len(),
|
||||
};
|
||||
|
||||
Some(body_size + 111)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether realm account exists, is initialized and owned by Governance program
|
||||
pub fn assert_is_valid_chat_message(
|
||||
program_id: &Pubkey,
|
||||
chat_message_info: &AccountInfo,
|
||||
) -> Result<(), ProgramError> {
|
||||
assert_is_valid_account(
|
||||
chat_message_info,
|
||||
GovernanceChatAccountType::ChatMessage,
|
||||
program_id,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_max_size() {
|
||||
let message = ChatMessage {
|
||||
account_type: GovernanceChatAccountType::ChatMessage,
|
||||
proposal: Pubkey::new_unique(),
|
||||
author: Pubkey::new_unique(),
|
||||
posted_at: 10,
|
||||
reply_to: Some(Pubkey::new_unique()),
|
||||
body: MessageBody::Text("message".to_string()),
|
||||
};
|
||||
let size = message.try_to_vec().unwrap().len();
|
||||
|
||||
assert_eq!(message.get_max_size(), Some(size));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//! General purpose account utility functions
|
||||
|
||||
use borsh::BorshSerialize;
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, program::invoke, program_error::ProgramError, pubkey::Pubkey,
|
||||
rent::Rent, system_instruction::create_account, system_program, sysvar::Sysvar,
|
||||
};
|
||||
use spl_governance::tools::account::AccountMaxSize;
|
||||
|
||||
use crate::error::GovernanceChatError;
|
||||
|
||||
/// Creates a new account and serializes data into it using AccountMaxSize to determine the account's size
|
||||
pub fn create_and_serialize_account<'a, T: BorshSerialize + AccountMaxSize>(
|
||||
payer_info: &AccountInfo<'a>,
|
||||
account_info: &AccountInfo<'a>,
|
||||
account_data: &T,
|
||||
program_id: &Pubkey,
|
||||
system_info: &AccountInfo<'a>,
|
||||
) -> Result<(), ProgramError> {
|
||||
// Assert the account is not initialized yet
|
||||
if !(account_info.data_is_empty() && *account_info.owner == system_program::id()) {
|
||||
return Err(GovernanceChatError::AccountAlreadyInitialized.into());
|
||||
}
|
||||
|
||||
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 rent = Rent::get()?;
|
||||
|
||||
let create_account_instruction = create_account(
|
||||
payer_info.key,
|
||||
account_info.key,
|
||||
rent.minimum_balance(account_size),
|
||||
account_size as u64,
|
||||
program_id,
|
||||
);
|
||||
|
||||
invoke(
|
||||
&create_account_instruction,
|
||||
&[
|
||||
payer_info.clone(),
|
||||
account_info.clone(),
|
||||
system_info.clone(),
|
||||
],
|
||||
)?;
|
||||
|
||||
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(())
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
//! Utility functions
|
||||
|
||||
pub mod account;
|
|
@ -0,0 +1,130 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use program_test::GovernanceChatProgramTest;
|
||||
use solana_program_test::tokio;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use spl_governance::error::GovernanceError;
|
||||
use spl_governance_chat::error::GovernanceChatError;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_message() {
|
||||
// Arrange
|
||||
let mut governance_chat_test = GovernanceChatProgramTest::start_new().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);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_reply_message() {
|
||||
// Arrange
|
||||
let mut governance_chat_test = GovernanceChatProgramTest::start_new().await;
|
||||
|
||||
let proposal_cookie = governance_chat_test.with_proposal().await;
|
||||
|
||||
let chat_message_cookie1 = governance_chat_test
|
||||
.with_chat_message(&proposal_cookie, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Act
|
||||
let chat_message_cookie2 = governance_chat_test
|
||||
.with_chat_message(&proposal_cookie, Some(chat_message_cookie1.address))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
let chat_message_data = governance_chat_test
|
||||
.get_message_account(&chat_message_cookie2.address)
|
||||
.await;
|
||||
|
||||
assert_eq!(chat_message_data, chat_message_cookie2.account);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_message_with_owner_or_delegate_must_sign_error() {
|
||||
// Arrange
|
||||
let mut governance_chat_test = GovernanceChatProgramTest::start_new().await;
|
||||
|
||||
let mut proposal_cookie = governance_chat_test.with_proposal().await;
|
||||
|
||||
proposal_cookie.token_owner = Keypair::new();
|
||||
|
||||
// Act
|
||||
let err = governance_chat_test
|
||||
.with_chat_message(&proposal_cookie, None)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_message_with_invalid_governance_for_proposal_error() {
|
||||
// Arrange
|
||||
let mut governance_chat_test = GovernanceChatProgramTest::start_new().await;
|
||||
|
||||
let proposal_cookie1 = governance_chat_test.with_proposal().await;
|
||||
|
||||
let mut proposal_cookie2 = governance_chat_test.with_proposal().await;
|
||||
|
||||
// Try to use proposal from a different realm
|
||||
proposal_cookie2.address = proposal_cookie1.address;
|
||||
|
||||
// Act
|
||||
let err = governance_chat_test
|
||||
.with_chat_message(&proposal_cookie2, None)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(err, GovernanceError::InvalidGovernanceForProposal.into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_message_with_not_enough_tokens_error() {
|
||||
// Arrange
|
||||
let mut governance_chat_test = GovernanceChatProgramTest::start_new().await;
|
||||
|
||||
let mut proposal_cookie = governance_chat_test.with_proposal().await;
|
||||
|
||||
let token_owner_record_cookie = governance_chat_test
|
||||
.with_token_owner_deposit(&proposal_cookie, 0)
|
||||
.await;
|
||||
|
||||
proposal_cookie.token_owner_record_address = token_owner_record_cookie.address;
|
||||
proposal_cookie.token_owner = token_owner_record_cookie.token_owner;
|
||||
|
||||
// Act
|
||||
let err = governance_chat_test
|
||||
.with_chat_message(&proposal_cookie, None)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
err,
|
||||
GovernanceChatError::NotEnoughTokensToCommentProposal.into()
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
use solana_program::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use spl_governance_chat::state::ChatMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChatMessageCookie {
|
||||
pub address: Pubkey,
|
||||
pub account: ChatMessage,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProposalCookie {
|
||||
pub address: Pubkey,
|
||||
pub realm_address: Pubkey,
|
||||
pub governance_address: Pubkey,
|
||||
pub token_owner_record_address: Pubkey,
|
||||
pub token_owner: Keypair,
|
||||
|
||||
pub governing_token_mint: Pubkey,
|
||||
pub governing_token_mint_authority: Keypair,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TokenOwnerRecordCookie {
|
||||
pub address: Pubkey,
|
||||
pub token_owner: Keypair,
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use solana_program::{program_error::ProgramError, pubkey::Pubkey};
|
||||
use solana_program_test::processor;
|
||||
|
||||
use solana_sdk::{signature::Keypair, signer::Signer};
|
||||
use spl_governance::{
|
||||
instruction::{
|
||||
create_account_governance, create_proposal, create_realm, deposit_governing_tokens,
|
||||
},
|
||||
state::{
|
||||
enums::{MintMaxVoteWeightSource, VoteThresholdPercentage},
|
||||
governance::{get_account_governance_address, GovernanceConfig},
|
||||
proposal::get_proposal_address,
|
||||
realm::get_realm_address,
|
||||
token_owner_record::get_token_owner_record_address,
|
||||
},
|
||||
};
|
||||
use spl_governance_chat::{
|
||||
instruction::post_message,
|
||||
processor::process_instruction,
|
||||
state::{ChatMessage, GovernanceChatAccountType, MessageBody},
|
||||
};
|
||||
use spl_governance_test_sdk::{ProgramTestBench, TestBenchProgram};
|
||||
|
||||
use crate::program_test::cookies::{ChatMessageCookie, ProposalCookie};
|
||||
|
||||
use self::cookies::TokenOwnerRecordCookie;
|
||||
|
||||
pub mod cookies;
|
||||
|
||||
pub struct GovernanceChatProgramTest {
|
||||
pub bench: ProgramTestBench,
|
||||
pub program_id: Pubkey,
|
||||
pub governance_program_id: Pubkey,
|
||||
}
|
||||
|
||||
impl GovernanceChatProgramTest {
|
||||
pub async fn start_new() -> Self {
|
||||
let program_id = Pubkey::from_str("GovernanceChat11111111111111111111111111111").unwrap();
|
||||
|
||||
let chat_program = TestBenchProgram {
|
||||
program_name: "spl_governance_chat",
|
||||
program_id: program_id,
|
||||
process_instruction: processor!(process_instruction),
|
||||
};
|
||||
|
||||
let governance_program_id =
|
||||
Pubkey::from_str("Governance111111111111111111111111111111111").unwrap();
|
||||
let governance_program = TestBenchProgram {
|
||||
program_name: "spl_governance",
|
||||
program_id: governance_program_id,
|
||||
process_instruction: processor!(spl_governance::processor::process_instruction),
|
||||
};
|
||||
|
||||
let bench = ProgramTestBench::start_new(&[chat_program, governance_program]).await;
|
||||
|
||||
Self {
|
||||
bench,
|
||||
program_id,
|
||||
governance_program_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_proposal(&mut self) -> ProposalCookie {
|
||||
// Create Realm
|
||||
let name = self.bench.get_unique_name("realm");
|
||||
|
||||
let realm_address = get_realm_address(&self.governance_program_id, &name);
|
||||
|
||||
let governing_token_mint_keypair = Keypair::new();
|
||||
let governing_token_mint_authority = Keypair::new();
|
||||
|
||||
self.bench
|
||||
.create_mint(
|
||||
&governing_token_mint_keypair,
|
||||
&governing_token_mint_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let realm_authority = Keypair::new();
|
||||
|
||||
let create_realm_ix = create_realm(
|
||||
&self.governance_program_id,
|
||||
&realm_authority.pubkey(),
|
||||
&governing_token_mint_keypair.pubkey(),
|
||||
&self.bench.payer.pubkey(),
|
||||
None,
|
||||
name.clone(),
|
||||
1,
|
||||
MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION,
|
||||
);
|
||||
|
||||
self.bench
|
||||
.process_transaction(&[create_realm_ix], None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create TokenOwnerRecord
|
||||
let token_owner = Keypair::new();
|
||||
let token_source = Keypair::new();
|
||||
|
||||
let transfer_authority = Keypair::new();
|
||||
|
||||
self.bench
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&governing_token_mint_keypair.pubkey(),
|
||||
&governing_token_mint_authority,
|
||||
100,
|
||||
&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(),
|
||||
&governing_token_mint_keypair.pubkey(),
|
||||
);
|
||||
|
||||
self.bench
|
||||
.process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create Governance
|
||||
let governed_account_address = Pubkey::new_unique();
|
||||
|
||||
let governance_config = GovernanceConfig {
|
||||
min_community_tokens_to_create_proposal: 5,
|
||||
min_council_tokens_to_create_proposal: 2,
|
||||
min_instruction_hold_up_time: 10,
|
||||
max_voting_time: 10,
|
||||
vote_threshold_percentage: VoteThresholdPercentage::YesVote(60),
|
||||
vote_weight_source: spl_governance::state::enums::VoteWeightSource::Deposit,
|
||||
proposal_cool_off_time: 0,
|
||||
};
|
||||
|
||||
let token_owner_record_address = get_token_owner_record_address(
|
||||
&self.governance_program_id,
|
||||
&realm_address,
|
||||
&governing_token_mint_keypair.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
);
|
||||
|
||||
let create_account_governance_ix = create_account_governance(
|
||||
&self.governance_program_id,
|
||||
&realm_address,
|
||||
&governed_account_address,
|
||||
&token_owner_record_address,
|
||||
&self.bench.payer.pubkey(),
|
||||
governance_config,
|
||||
);
|
||||
|
||||
self.bench
|
||||
.process_transaction(&[create_account_governance_ix], None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create Proposal
|
||||
|
||||
let governance_address = get_account_governance_address(
|
||||
&self.governance_program_id,
|
||||
&realm_address,
|
||||
&governed_account_address,
|
||||
);
|
||||
|
||||
let proposal_name = "Proposal #1".to_string();
|
||||
let description_link = "Proposal Description".to_string();
|
||||
let proposal_index = 0;
|
||||
|
||||
let create_proposal_ix = create_proposal(
|
||||
&self.governance_program_id,
|
||||
&governance_address,
|
||||
&token_owner_record_address,
|
||||
&token_owner.pubkey(),
|
||||
&self.bench.payer.pubkey(),
|
||||
&realm_address,
|
||||
proposal_name,
|
||||
description_link.clone(),
|
||||
&governing_token_mint_keypair.pubkey(),
|
||||
proposal_index,
|
||||
);
|
||||
|
||||
self.bench
|
||||
.process_transaction(&[create_proposal_ix], Some(&[&token_owner]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let proposal_address = get_proposal_address(
|
||||
&self.governance_program_id,
|
||||
&governance_address,
|
||||
&governing_token_mint_keypair.pubkey(),
|
||||
&proposal_index.to_le_bytes(),
|
||||
);
|
||||
|
||||
ProposalCookie {
|
||||
address: proposal_address,
|
||||
realm_address,
|
||||
governance_address,
|
||||
token_owner_record_address,
|
||||
token_owner,
|
||||
governing_token_mint: governing_token_mint_keypair.pubkey(),
|
||||
governing_token_mint_authority: governing_token_mint_authority,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_token_owner_deposit(
|
||||
&mut self,
|
||||
proposal_cookie: &ProposalCookie,
|
||||
deposit_amount: u64,
|
||||
) -> TokenOwnerRecordCookie {
|
||||
let token_owner = Keypair::new();
|
||||
let token_source = Keypair::new();
|
||||
|
||||
let transfer_authority = Keypair::new();
|
||||
|
||||
self.bench
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&proposal_cookie.governing_token_mint,
|
||||
&proposal_cookie.governing_token_mint_authority,
|
||||
deposit_amount,
|
||||
&token_owner,
|
||||
&transfer_authority.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let deposit_governing_tokens_ix = deposit_governing_tokens(
|
||||
&self.governance_program_id,
|
||||
&proposal_cookie.realm_address,
|
||||
&token_source.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
&self.bench.payer.pubkey(),
|
||||
&proposal_cookie.governing_token_mint,
|
||||
);
|
||||
|
||||
self.bench
|
||||
.process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_owner_record_address = get_token_owner_record_address(
|
||||
&self.governance_program_id,
|
||||
&proposal_cookie.realm_address,
|
||||
&proposal_cookie.governing_token_mint,
|
||||
&token_owner.pubkey(),
|
||||
);
|
||||
TokenOwnerRecordCookie {
|
||||
address: token_owner_record_address,
|
||||
token_owner,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_chat_message(
|
||||
&mut self,
|
||||
proposal_cookie: &ProposalCookie,
|
||||
reply_to: Option<Pubkey>,
|
||||
) -> Result<ChatMessageCookie, ProgramError> {
|
||||
let message_account = Keypair::new();
|
||||
let message_body = MessageBody::Text("My comment".to_string());
|
||||
|
||||
let post_message_ix = post_message(
|
||||
&self.program_id,
|
||||
&self.governance_program_id,
|
||||
&proposal_cookie.governance_address,
|
||||
&proposal_cookie.address,
|
||||
&proposal_cookie.token_owner_record_address,
|
||||
&proposal_cookie.token_owner.pubkey(),
|
||||
reply_to,
|
||||
&message_account.pubkey(),
|
||||
&self.bench.payer.pubkey(),
|
||||
message_body.clone(),
|
||||
);
|
||||
|
||||
let clock = self.bench.get_clock().await;
|
||||
|
||||
let message = ChatMessage {
|
||||
account_type: GovernanceChatAccountType::ChatMessage,
|
||||
proposal: proposal_cookie.address,
|
||||
author: proposal_cookie.token_owner.pubkey(),
|
||||
posted_at: clock.unix_timestamp,
|
||||
reply_to,
|
||||
body: message_body,
|
||||
};
|
||||
|
||||
self.bench
|
||||
.process_transaction(
|
||||
&[post_message_ix],
|
||||
Some(&[&proposal_cookie.token_owner, &message_account]),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ChatMessageCookie {
|
||||
address: message_account.pubkey(),
|
||||
account: message,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_message_account(&mut self, message_address: &Pubkey) -> ChatMessage {
|
||||
self.bench
|
||||
.get_borsh_account::<ChatMessage>(message_address)
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ base64 = "0.13"
|
|||
proptest = "1.0"
|
||||
solana-program-test = "1.7.11"
|
||||
solana-sdk = "1.7.11"
|
||||
spl-governance-test-sdk = { version = "0.1.0", path ="../test-sdk"}
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -33,7 +33,7 @@ async fn test_cancel_proposal() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let clock = governance_test.get_clock().await;
|
||||
let clock = governance_test.bench.get_clock().await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
|
|
|
@ -37,7 +37,7 @@ async fn test_cast_vote() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let clock = governance_test.get_clock().await;
|
||||
let clock = governance_test.bench.get_clock().await;
|
||||
|
||||
// Act
|
||||
let vote_record_cookie = governance_test
|
||||
|
|
|
@ -3,11 +3,12 @@ mod program_test;
|
|||
|
||||
use solana_program_test::*;
|
||||
|
||||
use program_test::{tools::ProgramInstructionError, *};
|
||||
use program_test::*;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use spl_governance::{
|
||||
error::GovernanceError, tools::bpf_loader_upgradeable::get_program_upgrade_authority,
|
||||
};
|
||||
use spl_governance_test_sdk::tools::ProgramInstructionError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_program_governance() {
|
||||
|
|
|
@ -193,6 +193,7 @@ async fn test_deposit_initial_community_tokens_with_owner_must_sign_error() {
|
|||
let token_source = Keypair::new();
|
||||
|
||||
governance_test
|
||||
.bench
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&realm_cookie.account.community_mint,
|
||||
|
@ -209,15 +210,16 @@ async fn test_deposit_initial_community_tokens_with_owner_must_sign_error() {
|
|||
&token_source.pubkey(),
|
||||
&token_owner.pubkey(),
|
||||
&transfer_authority.pubkey(),
|
||||
&governance_test.context.payer.pubkey(),
|
||||
&governance_test.bench.context.payer.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
instruction.accounts[3] = AccountMeta::new_readonly(token_owner.pubkey(), false);
|
||||
|
||||
// // Act
|
||||
// Act
|
||||
|
||||
let error = governance_test
|
||||
.bench
|
||||
.process_transaction(&[instruction], Some(&[&transfer_authority]))
|
||||
.await
|
||||
.err()
|
||||
|
@ -239,6 +241,7 @@ async fn test_deposit_initial_community_tokens_with_invalid_owner_error() {
|
|||
let invalid_owner = Keypair::new();
|
||||
|
||||
governance_test
|
||||
.bench
|
||||
.create_token_account_with_transfer_authority(
|
||||
&token_source,
|
||||
&realm_cookie.account.community_mint,
|
||||
|
@ -255,13 +258,14 @@ async fn test_deposit_initial_community_tokens_with_invalid_owner_error() {
|
|||
&token_source.pubkey(),
|
||||
&invalid_owner.pubkey(),
|
||||
&transfer_authority.pubkey(),
|
||||
&governance_test.context.payer.pubkey(),
|
||||
&governance_test.bench.context.payer.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
// // Act
|
||||
|
||||
let error = governance_test
|
||||
.bench
|
||||
.process_transaction(&[instruction], Some(&[&transfer_authority, &invalid_owner]))
|
||||
.await
|
||||
.err()
|
||||
|
@ -282,6 +286,7 @@ async fn test_deposit_community_tokens_with_malicious_holding_account_error() {
|
|||
.await;
|
||||
|
||||
governance_test
|
||||
.bench
|
||||
.mint_tokens(
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.community_mint_authority,
|
||||
|
@ -296,7 +301,7 @@ async fn test_deposit_community_tokens_with_malicious_holding_account_error() {
|
|||
&token_owner_record_cookie.token_source,
|
||||
&token_owner_record_cookie.token_owner.pubkey(),
|
||||
&token_owner_record_cookie.token_owner.pubkey(),
|
||||
&governance_test.context.payer.pubkey(),
|
||||
&governance_test.bench.context.payer.pubkey(),
|
||||
&realm_cookie.account.community_mint,
|
||||
);
|
||||
|
||||
|
@ -306,6 +311,7 @@ async fn test_deposit_community_tokens_with_malicious_holding_account_error() {
|
|||
// Act
|
||||
|
||||
let err = governance_test
|
||||
.bench
|
||||
.process_transaction(
|
||||
&[deposit_ix],
|
||||
Some(&[&token_owner_record_cookie.token_owner]),
|
||||
|
|
|
@ -67,7 +67,7 @@ async fn test_finalize_vote_to_succeeded() {
|
|||
)
|
||||
.await;
|
||||
|
||||
let clock = governance_test.get_clock().await;
|
||||
let clock = governance_test.bench.get_clock().await;
|
||||
|
||||
// Act
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ async fn test_execute_flag_instruction_error() {
|
|||
.advance_clock_by_min_timespan(proposal_instruction_cookie.account.hold_up_time as u64)
|
||||
.await;
|
||||
|
||||
let clock = governance_test.get_clock().await;
|
||||
let clock = governance_test.bench.get_clock().await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
|
|
|
@ -129,6 +129,7 @@ async fn test_relinquish_active_yes_vote() {
|
|||
assert_eq!(0, token_owner_record.total_votes_count);
|
||||
|
||||
let vote_record_account = governance_test
|
||||
.bench
|
||||
.get_account(&vote_record_cookie.address)
|
||||
.await;
|
||||
|
||||
|
@ -195,6 +196,7 @@ async fn test_relinquish_active_no_vote() {
|
|||
assert_eq!(0, token_owner_record.total_votes_count);
|
||||
|
||||
let vote_record_account = governance_test
|
||||
.bench
|
||||
.get_account(&vote_record_cookie.address)
|
||||
.await;
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ async fn test_remove_instruction() {
|
|||
assert_eq!(proposal_account.instructions_executed_count, 0);
|
||||
|
||||
let proposal_instruction_account = governance_test
|
||||
.bench
|
||||
.get_account(&proposal_instruction_cookie.address)
|
||||
.await;
|
||||
|
||||
|
@ -192,6 +193,7 @@ async fn test_remove_front_instruction() {
|
|||
assert_eq!(proposal_account.instructions_next_index, 2);
|
||||
|
||||
let proposal_instruction_account = governance_test
|
||||
.bench
|
||||
.get_account(&proposal_instruction_cookie.address)
|
||||
.await;
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ async fn test_remove_signatory() {
|
|||
assert_eq!(ProposalState::Draft, proposal_account.state);
|
||||
|
||||
let signatory_account = governance_test
|
||||
.bench
|
||||
.get_account(&signatory_record_cookie.address)
|
||||
.await;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
mod program_test;
|
||||
|
||||
use program_test::{tools::ProgramInstructionError, *};
|
||||
use program_test::*;
|
||||
use solana_program_test::tokio;
|
||||
use solana_sdk::{signature::Keypair, signer::Signer};
|
||||
use spl_governance::{
|
||||
|
@ -10,6 +10,7 @@ use spl_governance::{
|
|||
instruction::{set_governance_config, Vote},
|
||||
state::enums::VoteThresholdPercentage,
|
||||
};
|
||||
use spl_governance_test_sdk::tools::ProgramInstructionError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_governance_config() {
|
||||
|
@ -105,6 +106,7 @@ async fn test_set_governance_config_with_governance_must_sign_error() {
|
|||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.bench
|
||||
.process_transaction(&[set_governance_config_ix], None)
|
||||
.await
|
||||
.err()
|
||||
|
@ -135,6 +137,7 @@ async fn test_set_governance_config_with_fake_governance_signer_error() {
|
|||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.bench
|
||||
.process_transaction(&[set_governance_config_ix], Some(&[&governance_signer]))
|
||||
.await
|
||||
.err()
|
||||
|
|
|
@ -116,6 +116,7 @@ async fn test_set_community_governance_delegate_with_owner_must_sign_error() {
|
|||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.bench
|
||||
.process_transaction(&[instruction], None)
|
||||
.await
|
||||
.err()
|
||||
|
|
|
@ -38,7 +38,7 @@ async fn test_sign_off_proposal() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let clock = governance_test.get_clock().await;
|
||||
let clock = governance_test.bench.get_clock().await;
|
||||
|
||||
// Act
|
||||
governance_test
|
||||
|
|
|
@ -117,6 +117,7 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() {
|
|||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.bench
|
||||
.process_transaction(&[instruction], None)
|
||||
.await
|
||||
.err()
|
||||
|
@ -160,6 +161,7 @@ async fn test_withdraw_community_tokens_with_token_owner_record_address_mismatch
|
|||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.bench
|
||||
.process_transaction(&[instruction], Some(&[&hacker_record_cookie.token_owner]))
|
||||
.await
|
||||
.err()
|
||||
|
@ -283,6 +285,7 @@ async fn test_withdraw_tokens_with_malicious_holding_account_error() {
|
|||
// Try to maliciously withdraw from other token account owned by realm
|
||||
|
||||
let realm_token_account_cookie = governance_test
|
||||
.bench
|
||||
.with_token_account(
|
||||
&realm_cookie.account.community_mint,
|
||||
&realm_cookie.address,
|
||||
|
@ -303,6 +306,7 @@ async fn test_withdraw_tokens_with_malicious_holding_account_error() {
|
|||
|
||||
// Act
|
||||
let err = governance_test
|
||||
.bench
|
||||
.process_transaction(
|
||||
&[instruction],
|
||||
Some(&[&token_owner_record_cookie.token_owner]),
|
||||
|
|
|
@ -6,7 +6,7 @@ use spl_governance::state::{
|
|||
vote_record::VoteRecord,
|
||||
};
|
||||
|
||||
use crate::tools::clone_keypair;
|
||||
use spl_governance_test_sdk::tools::clone_keypair;
|
||||
|
||||
pub trait AccountCookie {
|
||||
fn get_address(&self) -> Pubkey;
|
||||
|
@ -145,8 +145,3 @@ pub struct ProposalInstructionCookie {
|
|||
pub account: ProposalInstruction,
|
||||
pub instruction: Instruction,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TokenAccountCookie {
|
||||
pub address: Pubkey,
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "spl-governance-test-sdk"
|
||||
version = "0.1.0"
|
||||
description = "Solana Program Library Governance Program Test SDK"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
arrayref = "0.3.6"
|
||||
bincode = "1.3.2"
|
||||
borsh = "0.9.1"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serde = "1.0.127"
|
||||
serde_derive = "1.0.103"
|
||||
solana-program = "1.7.11"
|
||||
solana-program-test = "1.7.11"
|
||||
solana-sdk = "1.7.11"
|
||||
spl-token = { version = "3.2", path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
|
@ -0,0 +1,6 @@
|
|||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TokenAccountCookie {
|
||||
pub address: Pubkey,
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
use std::borrow::Borrow;
|
||||
|
||||
use borsh::BorshDeserialize;
|
||||
use cookies::TokenAccountCookie;
|
||||
use solana_program::{
|
||||
borsh::try_from_slice_unchecked, clock::Clock, instruction::Instruction,
|
||||
program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, rent::Rent,
|
||||
system_instruction, sysvar,
|
||||
};
|
||||
use solana_program_test::{ProgramTest, ProgramTestContext};
|
||||
use solana_sdk::{
|
||||
account::Account, process_instruction::ProcessInstructionWithContext, signature::Keypair,
|
||||
signer::Signer, transaction::Transaction,
|
||||
};
|
||||
|
||||
use bincode::deserialize;
|
||||
|
||||
use tools::clone_keypair;
|
||||
|
||||
use crate::tools::map_transaction_error;
|
||||
|
||||
pub mod cookies;
|
||||
pub mod tools;
|
||||
|
||||
/// Specification of a program which is loaded into the test bench
|
||||
#[derive(Clone)]
|
||||
pub struct TestBenchProgram<'a> {
|
||||
pub program_name: &'a str,
|
||||
pub program_id: Pubkey,
|
||||
pub process_instruction: Option<ProcessInstructionWithContext>,
|
||||
}
|
||||
|
||||
/// Program's test bench which captures test context, rent and payer and common utility functions
|
||||
pub struct ProgramTestBench {
|
||||
pub context: ProgramTestContext,
|
||||
pub rent: Rent,
|
||||
pub payer: Keypair,
|
||||
pub next_id: u8,
|
||||
}
|
||||
|
||||
impl ProgramTestBench {
|
||||
pub async fn start_new(programs: &[TestBenchProgram<'_>]) -> Self {
|
||||
let mut program_test = ProgramTest::default();
|
||||
|
||||
for program in programs {
|
||||
program_test.add_program(
|
||||
program.program_name,
|
||||
program.program_id,
|
||||
program.process_instruction,
|
||||
)
|
||||
}
|
||||
|
||||
let mut context = program_test.start_with_context().await;
|
||||
let rent = context.banks_client.get_rent().await.unwrap();
|
||||
|
||||
let payer = clone_keypair(&context.payer);
|
||||
|
||||
Self {
|
||||
context,
|
||||
rent,
|
||||
payer,
|
||||
next_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_unique_name(&mut self, prefix: &str) -> String {
|
||||
self.next_id += 1;
|
||||
|
||||
format!("{}.{}", prefix, self.next_id)
|
||||
}
|
||||
|
||||
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
|
||||
.context
|
||||
.banks_client
|
||||
.get_recent_blockhash()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
transaction.sign(&all_signers, recent_blockhash);
|
||||
|
||||
self.context
|
||||
.banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.map_err(map_transaction_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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.context.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_empty_token_account(
|
||||
&mut self,
|
||||
token_account_keypair: &Keypair,
|
||||
token_mint: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
) {
|
||||
let create_account_instruction = system_instruction::create_account(
|
||||
&self.context.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();
|
||||
|
||||
self.process_transaction(
|
||||
&[create_account_instruction, initialize_account_instruction],
|
||||
Some(&[token_account_keypair]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn with_token_account(
|
||||
&mut self,
|
||||
token_mint: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
token_mint_authority: &Keypair,
|
||||
amount: u64,
|
||||
) -> TokenAccountCookie {
|
||||
let token_account_keypair = Keypair::new();
|
||||
|
||||
self.create_empty_token_account(&token_account_keypair, token_mint, owner)
|
||||
.await;
|
||||
|
||||
self.mint_tokens(
|
||||
token_mint,
|
||||
token_mint_authority,
|
||||
&token_account_keypair.pubkey(),
|
||||
amount,
|
||||
)
|
||||
.await;
|
||||
|
||||
TokenAccountCookie {
|
||||
address: token_account_keypair.pubkey(),
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
#[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.context.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();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_clock(&mut self) -> Clock {
|
||||
self.get_bincode_account::<Clock>(&sysvar::clock::id())
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_bincode_account<T: serde::de::DeserializeOwned>(
|
||||
&mut self,
|
||||
address: &Pubkey,
|
||||
) -> T {
|
||||
self.context
|
||||
.banks_client
|
||||
.get_account(*address)
|
||||
.await
|
||||
.unwrap()
|
||||
.map(|a| deserialize::<T>(a.data.borrow()).unwrap())
|
||||
.unwrap_or_else(|| panic!("GET-TEST-ACCOUNT-ERROR: Account {}", address))
|
||||
}
|
||||
|
||||
/// TODO: Add to SDK
|
||||
pub async fn get_borsh_account<T: BorshDeserialize>(&mut self, address: &Pubkey) -> T {
|
||||
self.get_account(address)
|
||||
.await
|
||||
.map(|a| try_from_slice_unchecked(&a.data).unwrap())
|
||||
.unwrap_or_else(|| panic!("GET-TEST-ACCOUNT-ERROR: Account {} not found", address))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_account(&mut self, address: &Pubkey) -> Option<Account> {
|
||||
self.context
|
||||
.banks_client
|
||||
.get_account(*address)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ use std::convert::TryFrom;
|
|||
use solana_program::{instruction::InstructionError, program_error::ProgramError};
|
||||
use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError};
|
||||
|
||||
/// TODO: Add to SDK
|
||||
/// TODO: Add to Solana SDK
|
||||
/// Instruction errors not mapped in the sdk
|
||||
pub enum ProgramInstructionError {
|
||||
/// Incorrect authority provided
|
Loading…
Reference in New Issue