From baa1f064c12e931d1ac3eca60aa89c9c45575965 Mon Sep 17 00:00:00 2001 From: Vladimir Guguiev <1524432+vovacodes@users.noreply.github.com> Date: Sat, 2 Dec 2023 23:59:49 +0100 Subject: [PATCH] feat(program-config): add ProgramConfig and instructions that work with it --- Cargo.lock | 1 + programs/squads_multisig_program/Cargo.toml | 1 + .../src/instructions/mod.rs | 4 + .../src/instructions/multisig_create.rs | 97 ++++ .../src/instructions/program_config.rs | 80 +++ .../src/instructions/program_config_init.rs | 59 ++ .../transaction_accounts_close.rs | 4 + programs/squads_multisig_program/src/lib.rs | 52 +- .../squads_multisig_program/src/state/mod.rs | 2 + .../src/state/program_config.rs | 38 ++ .../src/state/proposal.rs | 1 + .../src/state/seeds.rs | 1 + sdk/multisig/idl/squads_multisig_program.json | 281 ++++++++++ .../src/generated/accounts/ProgramConfig.ts | 192 +++++++ sdk/multisig/src/generated/accounts/index.ts | 3 + .../src/generated/instructions/index.ts | 5 + .../instructions/multisigCreateV2.ts | 129 +++++ .../instructions/programConfigInit.ts | 108 ++++ .../instructions/programConfigSetAuthority.ts | 102 ++++ .../programConfigSetMultisigCreationFee.ts | 104 ++++ .../instructions/programConfigSetTreasury.ts | 102 ++++ .../generated/types/ProgramConfigInitArgs.ts | 29 + .../types/ProgramConfigSetAuthorityArgs.ts | 23 + ...ProgramConfigSetMultisigCreationFeeArgs.ts | 21 + .../types/ProgramConfigSetTreasuryArgs.ts | 23 + sdk/multisig/src/generated/types/index.ts | 4 + sdk/multisig/src/instructions/index.ts | 1 + .../src/instructions/multisigCreate.ts | 1 + .../src/instructions/multisigCreateV2.ts | 56 ++ sdk/multisig/src/pda.ts | 12 + sdk/multisig/src/rpc/index.ts | 1 + sdk/multisig/src/rpc/multisigCreate.ts | 6 +- sdk/multisig/src/rpc/multisigCreateV2.ts | 66 +++ sdk/multisig/src/transactions/index.ts | 1 + .../src/transactions/multisigCreate.ts | 6 +- .../src/transactions/multisigCreateV2.ts | 60 ++ test-program-config-initializer-keypair.json | 1 + tests/index.ts | 1 + tests/suites/instructions/multisigCreate.ts | 365 +++++++++++++ tests/suites/instructions/multisigCreateV2.ts | 514 ++++++++++++++++++ tests/suites/multisig-sdk.ts | 362 +----------- tests/suites/program-config-init.ts | 222 ++++++++ tests/utils.ts | 174 ++++++ 43 files changed, 2955 insertions(+), 360 deletions(-) create mode 100644 programs/squads_multisig_program/src/instructions/program_config.rs create mode 100644 programs/squads_multisig_program/src/instructions/program_config_init.rs create mode 100644 programs/squads_multisig_program/src/state/program_config.rs create mode 100644 sdk/multisig/src/generated/accounts/ProgramConfig.ts create mode 100644 sdk/multisig/src/generated/instructions/multisigCreateV2.ts create mode 100644 sdk/multisig/src/generated/instructions/programConfigInit.ts create mode 100644 sdk/multisig/src/generated/instructions/programConfigSetAuthority.ts create mode 100644 sdk/multisig/src/generated/instructions/programConfigSetMultisigCreationFee.ts create mode 100644 sdk/multisig/src/generated/instructions/programConfigSetTreasury.ts create mode 100644 sdk/multisig/src/generated/types/ProgramConfigInitArgs.ts create mode 100644 sdk/multisig/src/generated/types/ProgramConfigSetAuthorityArgs.ts create mode 100644 sdk/multisig/src/generated/types/ProgramConfigSetMultisigCreationFeeArgs.ts create mode 100644 sdk/multisig/src/generated/types/ProgramConfigSetTreasuryArgs.ts create mode 100644 sdk/multisig/src/instructions/multisigCreateV2.ts create mode 100644 sdk/multisig/src/rpc/multisigCreateV2.ts create mode 100644 sdk/multisig/src/transactions/multisigCreateV2.ts create mode 100644 test-program-config-initializer-keypair.json create mode 100644 tests/suites/instructions/multisigCreate.ts create mode 100644 tests/suites/instructions/multisigCreateV2.ts create mode 100644 tests/suites/program-config-init.ts diff --git a/Cargo.lock b/Cargo.lock index 5343ed57..dd445295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4190,6 +4190,7 @@ version = "0.3.0" dependencies = [ "anchor-lang", "anchor-spl", + "solana-program", "solana-security-txt", ] diff --git a/programs/squads_multisig_program/Cargo.toml b/programs/squads_multisig_program/Cargo.toml index 62e8b71d..f7c4e51e 100644 --- a/programs/squads_multisig_program/Cargo.toml +++ b/programs/squads_multisig_program/Cargo.toml @@ -20,5 +20,6 @@ default = [] [dependencies] anchor-lang = { version = "=0.29.0", features = ["allow-missing-optionals"] } anchor-spl = { version="=0.29.0", features=["token"] } +solana-program = "1.17.4" solana-security-txt = "1.1.1" # \ No newline at end of file diff --git a/programs/squads_multisig_program/src/instructions/mod.rs b/programs/squads_multisig_program/src/instructions/mod.rs index 9b380443..d64dd9c1 100644 --- a/programs/squads_multisig_program/src/instructions/mod.rs +++ b/programs/squads_multisig_program/src/instructions/mod.rs @@ -7,6 +7,8 @@ pub use multisig_add_spending_limit::*; pub use multisig_config::*; pub use multisig_create::*; pub use multisig_remove_spending_limit::*; +pub use program_config_init::*; +pub use program_config::*; pub use proposal_activate::*; pub use proposal_create::*; pub use proposal_vote::*; @@ -24,6 +26,8 @@ mod multisig_add_spending_limit; mod multisig_config; mod multisig_create; mod multisig_remove_spending_limit; +mod program_config_init; +mod program_config; mod proposal_activate; mod proposal_create; mod proposal_vote; diff --git a/programs/squads_multisig_program/src/instructions/multisig_create.rs b/programs/squads_multisig_program/src/instructions/multisig_create.rs index 05803539..89fc4212 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_create.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_create.rs @@ -1,5 +1,9 @@ +#![allow(deprecated)] use anchor_lang::prelude::*; +use anchor_lang::system_program; +use solana_program::native_token::LAMPORTS_PER_SOL; +use crate::errors::MultisigError; use crate::state::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -20,6 +24,10 @@ pub struct MultisigCreateArgs { pub memo: Option, } +#[deprecated( + since = "0.4.0", + note = "This instruction is deprecated and will be removed soon. Please use `multisig_create_v2` to ensure future compatibility." +)] #[derive(Accounts)] #[instruction(args: MultisigCreateArgs)] pub struct MultisigCreate<'info> { @@ -43,11 +51,84 @@ pub struct MultisigCreate<'info> { pub system_program: Program<'info, System>, } +#[allow(deprecated)] impl MultisigCreate<'_> { fn validate(&self) -> Result<()> { Ok(()) } + /// Creates a multisig. + #[allow(deprecated)] + #[access_control(ctx.accounts.validate())] + pub fn multisig_create(ctx: Context, args: MultisigCreateArgs) -> Result<()> { + msg!("WARNING: This instruction is deprecated and will be removed soon. Please use `multisig_create_v2` to ensure future compatibility."); + + // Sort the members by pubkey. + let mut members = args.members; + members.sort_by_key(|m| m.key); + + // Initialize the multisig. + let multisig = &mut ctx.accounts.multisig; + multisig.config_authority = args.config_authority.unwrap_or_default(); + multisig.threshold = args.threshold; + multisig.time_lock = args.time_lock; + multisig.transaction_index = 0; + multisig.stale_transaction_index = 0; + multisig.create_key = ctx.accounts.create_key.key(); + multisig.bump = ctx.bumps.multisig; + multisig.members = members; + multisig.rent_collector = args.rent_collector; + + multisig.invariant()?; + + Ok(()) + } +} + +#[derive(Accounts)] +#[instruction(args: MultisigCreateArgs)] +pub struct MultisigCreateV2<'info> { + /// Global program config account. + pub program_config: Account<'info, ProgramConfig>, + + /// The treasury where the creation fee is transferred to. + /// CHECK: validation is performed in the `MultisigCreate::validate()` method. + #[account(mut)] + pub treasury: AccountInfo<'info>, + + #[account( + init, + payer = creator, + space = Multisig::size(args.members.len(), args.rent_collector.is_some()), + seeds = [SEED_PREFIX, SEED_MULTISIG, create_key.key().as_ref()], + bump + )] + pub multisig: Account<'info, Multisig>, + + /// An ephemeral signer that is used as a seed for the Multisig PDA. + /// Must be a signer to prevent front-running attack by someone else but the original creator. + pub create_key: Signer<'info>, + + /// The creator of the multisig. + #[account(mut)] + pub creator: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +impl MultisigCreateV2<'_> { + fn validate(&self) -> Result<()> { + //region treasury + require_keys_eq!( + self.treasury.key(), + self.program_config.treasury, + MultisigError::InvalidAccount + ); + //endregion + + Ok(()) + } + /// Creates a multisig. #[access_control(ctx.accounts.validate())] pub fn multisig_create(ctx: Context, args: MultisigCreateArgs) -> Result<()> { @@ -69,6 +150,22 @@ impl MultisigCreate<'_> { multisig.invariant()?; + let creation_fee = ctx.accounts.program_config.multisig_creation_fee; + + if creation_fee > 0 { + system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.creator.to_account_info(), + to: ctx.accounts.treasury.to_account_info(), + }, + ), + creation_fee, + )?; + msg!("Creation fee: {}", creation_fee / LAMPORTS_PER_SOL); + } + Ok(()) } } diff --git a/programs/squads_multisig_program/src/instructions/program_config.rs b/programs/squads_multisig_program/src/instructions/program_config.rs new file mode 100644 index 00000000..c63a6374 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/program_config.rs @@ -0,0 +1,80 @@ +use anchor_lang::prelude::*; + +use crate::errors::MultisigError; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct ProgramConfigSetAuthorityArgs { + pub new_authority: Pubkey, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct ProgramConfigSetMultisigCreationFeeArgs { + pub new_multisig_creation_fee: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct ProgramConfigSetTreasuryArgs { + pub new_treasury: Pubkey, +} + +#[derive(Accounts)] +pub struct ProgramConfig<'info> { + #[account(mut)] + pub program_config: Account<'info, crate::state::ProgramConfig>, + + pub authority: Signer<'info>, +} + +impl ProgramConfig<'_> { + fn validate(&self) -> Result<()> { + let Self { + program_config, + authority, + } = self; + + // authority + require_keys_eq!( + program_config.authority, + authority.key(), + MultisigError::Unauthorized + ); + + Ok(()) + } + + #[access_control(ctx.accounts.validate())] + pub fn program_config_set_authority( + ctx: Context, + args: ProgramConfigSetAuthorityArgs, + ) -> Result<()> { + let program_config = &mut ctx.accounts.program_config; + + program_config.authority = args.new_authority; + + Ok(()) + } + + #[access_control(ctx.accounts.validate())] + pub fn program_config_set_multisig_creation_fee( + ctx: Context, + args: ProgramConfigSetMultisigCreationFeeArgs, + ) -> Result<()> { + let program_config = &mut ctx.accounts.program_config; + + program_config.multisig_creation_fee = args.new_multisig_creation_fee; + + Ok(()) + } + + #[access_control(ctx.accounts.validate())] + pub fn program_config_set_treasury( + ctx: Context, + args: ProgramConfigSetTreasuryArgs, + ) -> Result<()> { + let program_config = &mut ctx.accounts.program_config; + + program_config.treasury = args.new_treasury; + + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/instructions/program_config_init.rs b/programs/squads_multisig_program/src/instructions/program_config_init.rs new file mode 100644 index 00000000..184199a7 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/program_config_init.rs @@ -0,0 +1,59 @@ +use crate::errors::MultisigError; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey; + +use crate::state::*; + +/// This is a key controlled by the Squads team and is intended to use for the single +/// transaction that initializes the global program config. It is not used for anything else. +#[cfg(not(feature = "testing"))] +const INITIALIZER: Pubkey = pubkey!("HM5y4mz3Bt9JY9mr1hkyhnvqxSH4H2u2451j7Hc2dtvK"); + +#[cfg(feature = "testing")] +const INITIALIZER: Pubkey = pubkey!("BrQAbGdWQ9YUHmWWgKFdFe4miTURH71jkYFPXfaosqDv"); + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct ProgramConfigInitArgs { + /// The authority that can configure the program config: change the treasury, etc. + pub authority: Pubkey, + /// The fee that is charged for creating a new multisig. + pub multisig_creation_fee: u64, + /// The treasury where the creation fee is transferred to. + pub treasury: Pubkey, +} + +#[derive(Accounts)] +pub struct ProgramConfigInit<'info> { + #[account( + init, + payer = initializer, + space = 8 + ProgramConfig::INIT_SPACE, + seeds = [SEED_PREFIX, SEED_PROGRAM_CONFIG], + bump + )] + pub program_config: Account<'info, ProgramConfig>, + + /// The hard-coded account that is used to initialize the program config once. + #[account( + mut, + address = INITIALIZER @ MultisigError::Unauthorized + )] + pub initializer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +impl ProgramConfigInit<'_> { + /// A one-time instruction that initializes the global program config. + pub fn program_config_init(ctx: Context, args: ProgramConfigInitArgs) -> Result<()> { + let program_config = &mut ctx.accounts.program_config; + + program_config.authority = args.authority; + program_config.multisig_creation_fee = args.multisig_creation_fee; + program_config.treasury = args.treasury; + + program_config.invariant()?; + + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs b/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs index e2a5fa7a..fa43b1f6 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs @@ -75,6 +75,7 @@ impl ConfigTransactionAccountsClose<'_> { // Has to be either stale or in a terminal state. let is_stale = proposal.transaction_index <= multisig.stale_transaction_index; + #[allow(deprecated)] let can_close = match proposal.status { // Draft proposals can only be closed if stale, // so they can't be activated anymore. @@ -188,6 +189,7 @@ impl VaultTransactionAccountsClose<'_> { let is_stale = proposal.transaction_index <= multisig.stale_transaction_index; + #[allow(deprecated)] let can_close = match proposal.status { // Draft proposals can only be closed if stale, // so they can't be activated anymore. @@ -351,6 +353,7 @@ impl VaultBatchTransactionAccountClose<'_> { // Batch transactions that are marked as executed within the batch can be closed, // otherwise we need to check the proposal status. + #[allow(deprecated)] let can_close = is_batch_transaction_executed || match proposal.status { // Transactions of Draft proposals can only be closed if stale, @@ -467,6 +470,7 @@ impl BatchAccountsClose<'_> { let is_stale = proposal.transaction_index <= multisig.stale_transaction_index; + #[allow(deprecated)] let can_close = match proposal.status { // Draft proposals can only be closed if stale, // so they can't be activated anymore. diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 12149b22..5da108c1 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -4,13 +4,13 @@ // #![deny(clippy::arithmetic_side_effects)] // #![deny(clippy::integer_arithmetic)] -extern crate core; - -use anchor_lang::prelude::*; - // Re-export anchor_lang for convenience. pub use anchor_lang; +use anchor_lang::prelude::*; +#[cfg(not(feature = "no-entrypoint"))] +use solana_security_txt::security_txt; +pub use instructions::ProgramConfig; pub use instructions::*; pub use state::*; pub use utils::SmallVec; @@ -20,9 +20,6 @@ pub mod instructions; pub mod state; mod utils; -#[cfg(not(feature = "no-entrypoint"))] -use solana_security_txt::security_txt; - #[cfg(not(feature = "no-entrypoint"))] security_txt! { name: "Squads Multisig Program", @@ -44,11 +41,52 @@ declare_id!("GyhGAqjokLwF9UXdQ2dR5Zwiup242j4mX4J1tSMKyAmD"); pub mod squads_multisig_program { use super::*; + /// Initialize the program config. + pub fn program_config_init( + ctx: Context, + args: ProgramConfigInitArgs, + ) -> Result<()> { + ProgramConfigInit::program_config_init(ctx, args) + } + + /// Set the `authority` parameter of the program config. + pub fn program_config_set_authority( + ctx: Context, + args: ProgramConfigSetAuthorityArgs, + ) -> Result<()> { + ProgramConfig::program_config_set_authority(ctx, args) + } + + /// Set the `multisig_creation_fee` parameter of the program config. + pub fn program_config_set_multisig_creation_fee( + ctx: Context, + args: ProgramConfigSetMultisigCreationFeeArgs, + ) -> Result<()> { + ProgramConfig::program_config_set_multisig_creation_fee(ctx, args) + } + + /// Set the `treasury` parameter of the program config. + pub fn program_config_set_treasury( + ctx: Context, + args: ProgramConfigSetTreasuryArgs, + ) -> Result<()> { + ProgramConfig::program_config_set_treasury(ctx, args) + } + /// Create a multisig. + #[allow(deprecated)] pub fn multisig_create(ctx: Context, args: MultisigCreateArgs) -> Result<()> { MultisigCreate::multisig_create(ctx, args) } + /// Create a multisig. + pub fn multisig_create_v2( + ctx: Context, + args: MultisigCreateArgs, + ) -> Result<()> { + MultisigCreateV2::multisig_create(ctx, args) + } + /// Add a new member to the controlled multisig. pub fn multisig_add_member( ctx: Context, diff --git a/programs/squads_multisig_program/src/state/mod.rs b/programs/squads_multisig_program/src/state/mod.rs index 73b24253..85e19941 100644 --- a/programs/squads_multisig_program/src/state/mod.rs +++ b/programs/squads_multisig_program/src/state/mod.rs @@ -1,6 +1,7 @@ pub use self::multisig::*; pub use batch::*; pub use config_transaction::*; +pub use program_config::*; pub use proposal::*; pub use seeds::*; pub use spending_limit::*; @@ -9,6 +10,7 @@ pub use vault_transaction::*; mod batch; mod config_transaction; mod multisig; +mod program_config; mod proposal; mod seeds; mod spending_limit; diff --git a/programs/squads_multisig_program/src/state/program_config.rs b/programs/squads_multisig_program/src/state/program_config.rs new file mode 100644 index 00000000..2f9190a0 --- /dev/null +++ b/programs/squads_multisig_program/src/state/program_config.rs @@ -0,0 +1,38 @@ +use anchor_lang::prelude::*; + +use crate::errors::MultisigError; + +/// Global program configuration account. +#[account] +#[derive(InitSpace)] +pub struct ProgramConfig { + /// The authority which can update the config. + pub authority: Pubkey, + /// The lamports amount charged for creating a new multisig account. + /// This fee is sent to the `treasury` account. + pub multisig_creation_fee: u64, + /// The treasury account to send charged fees to. + pub treasury: Pubkey, + /// Reserved for future use. + pub _reserved: [u8; 64], +} + +impl ProgramConfig { + pub fn invariant(&self) -> Result<()> { + // authority must be non-default. + require_keys_neq!( + self.authority, + Pubkey::default(), + MultisigError::InvalidAccount + ); + + // treasury must be non-default. + require_keys_neq!( + self.treasury, + Pubkey::default(), + MultisigError::InvalidAccount + ); + + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/state/proposal.rs b/programs/squads_multisig_program/src/state/proposal.rs index 646eb099..18a0fcc0 100644 --- a/programs/squads_multisig_program/src/state/proposal.rs +++ b/programs/squads_multisig_program/src/state/proposal.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] use anchor_lang::prelude::*; use crate::errors::*; diff --git a/programs/squads_multisig_program/src/state/seeds.rs b/programs/squads_multisig_program/src/state/seeds.rs index 6efa1055..e44e4ad0 100644 --- a/programs/squads_multisig_program/src/state/seeds.rs +++ b/programs/squads_multisig_program/src/state/seeds.rs @@ -1,4 +1,5 @@ pub const SEED_PREFIX: &[u8] = b"multisig"; +pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config"; pub const SEED_MULTISIG: &[u8] = b"multisig"; pub const SEED_PROPOSAL: &[u8] = b"proposal"; pub const SEED_TRANSACTION: &[u8] = b"transaction"; diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 9f79ccd6..9f79d932 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -2,6 +2,118 @@ "version": "0.3.0", "name": "squads_multisig_program", "instructions": [ + { + "name": "programConfigInit", + "docs": [ + "Initialize the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "initializer", + "isMut": true, + "isSigner": true, + "docs": [ + "The hard-coded account that is used to initialize the program config once." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigInitArgs" + } + } + ] + }, + { + "name": "programConfigSetAuthority", + "docs": [ + "Set the `authority` parameter of the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigSetAuthorityArgs" + } + } + ] + }, + { + "name": "programConfigSetMultisigCreationFee", + "docs": [ + "Set the `multisig_creation_fee` parameter of the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigSetMultisigCreationFeeArgs" + } + } + ] + }, + { + "name": "programConfigSetTreasury", + "docs": [ + "Set the `treasury` parameter of the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigSetTreasuryArgs" + } + } + ] + }, { "name": "multisigCreate", "docs": [ @@ -45,6 +157,65 @@ } ] }, + { + "name": "multisigCreateV2", + "docs": [ + "Create a multisig." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": false, + "isSigner": false, + "docs": [ + "Global program config account." + ] + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false, + "docs": [ + "The treasury where the creation fee is transferred to." + ] + }, + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "createKey", + "isMut": false, + "isSigner": true, + "docs": [ + "An ephemeral signer that is used as a seed for the Multisig PDA.", + "Must be a signer to prevent front-running attack by someone else but the original creator." + ] + }, + { + "name": "creator", + "isMut": true, + "isSigner": true, + "docs": [ + "The creator of the multisig." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigCreateArgs" + } + } + ] + }, { "name": "multisigAddMember", "docs": [ @@ -1497,6 +1668,51 @@ ] } }, + { + "name": "ProgramConfig", + "docs": [ + "Global program configuration account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The authority which can update the config." + ], + "type": "publicKey" + }, + { + "name": "multisigCreationFee", + "docs": [ + "The lamports amount charged for creating a new multisig account.", + "This fee is sent to the `treasury` account." + ], + "type": "u64" + }, + { + "name": "treasury", + "docs": [ + "The treasury account to send charged fees to." + ], + "type": "publicKey" + }, + { + "name": "reserved", + "docs": [ + "Reserved for future use." + ], + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, { "name": "Proposal", "docs": [ @@ -2095,6 +2311,71 @@ ] } }, + { + "name": "ProgramConfigInitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The authority that can configure the program config: change the treasury, etc." + ], + "type": "publicKey" + }, + { + "name": "multisigCreationFee", + "docs": [ + "The fee that is charged for creating a new multisig." + ], + "type": "u64" + }, + { + "name": "treasury", + "docs": [ + "The treasury where the creation fee is transferred to." + ], + "type": "publicKey" + } + ] + } + }, + { + "name": "ProgramConfigSetAuthorityArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newAuthority", + "type": "publicKey" + } + ] + } + }, + { + "name": "ProgramConfigSetMultisigCreationFeeArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newMultisigCreationFee", + "type": "u64" + } + ] + } + }, + { + "name": "ProgramConfigSetTreasuryArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newTreasury", + "type": "publicKey" + } + ] + } + }, { "name": "ProposalCreateArgs", "type": { diff --git a/sdk/multisig/src/generated/accounts/ProgramConfig.ts b/sdk/multisig/src/generated/accounts/ProgramConfig.ts new file mode 100644 index 00000000..061bfc47 --- /dev/null +++ b/sdk/multisig/src/generated/accounts/ProgramConfig.ts @@ -0,0 +1,192 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beet from '@metaplex-foundation/beet' +import * as beetSolana from '@metaplex-foundation/beet-solana' + +/** + * Arguments used to create {@link ProgramConfig} + * @category Accounts + * @category generated + */ +export type ProgramConfigArgs = { + authority: web3.PublicKey + multisigCreationFee: beet.bignum + treasury: web3.PublicKey + reserved: number[] /* size: 64 */ +} + +export const programConfigDiscriminator = [196, 210, 90, 231, 144, 149, 140, 63] +/** + * Holds the data for the {@link ProgramConfig} Account and provides de/serialization + * functionality for that data + * + * @category Accounts + * @category generated + */ +export class ProgramConfig implements ProgramConfigArgs { + private constructor( + readonly authority: web3.PublicKey, + readonly multisigCreationFee: beet.bignum, + readonly treasury: web3.PublicKey, + readonly reserved: number[] /* size: 64 */ + ) {} + + /** + * Creates a {@link ProgramConfig} instance from the provided args. + */ + static fromArgs(args: ProgramConfigArgs) { + return new ProgramConfig( + args.authority, + args.multisigCreationFee, + args.treasury, + args.reserved + ) + } + + /** + * Deserializes the {@link ProgramConfig} from the data of the provided {@link web3.AccountInfo}. + * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. + */ + static fromAccountInfo( + accountInfo: web3.AccountInfo, + offset = 0 + ): [ProgramConfig, number] { + return ProgramConfig.deserialize(accountInfo.data, offset) + } + + /** + * Retrieves the account info from the provided address and deserializes + * the {@link ProgramConfig} from its data. + * + * @throws Error if no account info is found at the address or if deserialization fails + */ + static async fromAccountAddress( + connection: web3.Connection, + address: web3.PublicKey, + commitmentOrConfig?: web3.Commitment | web3.GetAccountInfoConfig + ): Promise { + const accountInfo = await connection.getAccountInfo( + address, + commitmentOrConfig + ) + if (accountInfo == null) { + throw new Error(`Unable to find ProgramConfig account at ${address}`) + } + return ProgramConfig.fromAccountInfo(accountInfo, 0)[0] + } + + /** + * Provides a {@link web3.Connection.getProgramAccounts} config builder, + * to fetch accounts matching filters that can be specified via that builder. + * + * @param programId - the program that owns the accounts we are filtering + */ + static gpaBuilder( + programId: web3.PublicKey = new web3.PublicKey( + 'SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf' + ) + ) { + return beetSolana.GpaBuilder.fromStruct(programId, programConfigBeet) + } + + /** + * Deserializes the {@link ProgramConfig} from the provided data Buffer. + * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. + */ + static deserialize(buf: Buffer, offset = 0): [ProgramConfig, number] { + return programConfigBeet.deserialize(buf, offset) + } + + /** + * Serializes the {@link ProgramConfig} into a Buffer. + * @returns a tuple of the created Buffer and the offset up to which the buffer was written to store it. + */ + serialize(): [Buffer, number] { + return programConfigBeet.serialize({ + accountDiscriminator: programConfigDiscriminator, + ...this, + }) + } + + /** + * Returns the byteSize of a {@link Buffer} holding the serialized data of + * {@link ProgramConfig} + */ + static get byteSize() { + return programConfigBeet.byteSize + } + + /** + * Fetches the minimum balance needed to exempt an account holding + * {@link ProgramConfig} data from rent + * + * @param connection used to retrieve the rent exemption information + */ + static async getMinimumBalanceForRentExemption( + connection: web3.Connection, + commitment?: web3.Commitment + ): Promise { + return connection.getMinimumBalanceForRentExemption( + ProgramConfig.byteSize, + commitment + ) + } + + /** + * Determines if the provided {@link Buffer} has the correct byte size to + * hold {@link ProgramConfig} data. + */ + static hasCorrectByteSize(buf: Buffer, offset = 0) { + return buf.byteLength - offset === ProgramConfig.byteSize + } + + /** + * Returns a readable version of {@link ProgramConfig} properties + * and can be used to convert to JSON and/or logging + */ + pretty() { + return { + authority: this.authority.toBase58(), + multisigCreationFee: (() => { + const x = <{ toNumber: () => number }>this.multisigCreationFee + if (typeof x.toNumber === 'function') { + try { + return x.toNumber() + } catch (_) { + return x + } + } + return x + })(), + treasury: this.treasury.toBase58(), + reserved: this.reserved, + } + } +} + +/** + * @category Accounts + * @category generated + */ +export const programConfigBeet = new beet.BeetStruct< + ProgramConfig, + ProgramConfigArgs & { + accountDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['accountDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['authority', beetSolana.publicKey], + ['multisigCreationFee', beet.u64], + ['treasury', beetSolana.publicKey], + ['reserved', beet.uniformFixedSizeArray(beet.u8, 64)], + ], + ProgramConfig.fromArgs, + 'ProgramConfig' +) diff --git a/sdk/multisig/src/generated/accounts/index.ts b/sdk/multisig/src/generated/accounts/index.ts index c0e3888a..10449a64 100644 --- a/sdk/multisig/src/generated/accounts/index.ts +++ b/sdk/multisig/src/generated/accounts/index.ts @@ -1,6 +1,7 @@ export * from './Batch' export * from './ConfigTransaction' export * from './Multisig' +export * from './ProgramConfig' export * from './Proposal' export * from './SpendingLimit' export * from './VaultBatchTransaction' @@ -10,6 +11,7 @@ import { Batch } from './Batch' import { VaultBatchTransaction } from './VaultBatchTransaction' import { ConfigTransaction } from './ConfigTransaction' import { Multisig } from './Multisig' +import { ProgramConfig } from './ProgramConfig' import { Proposal } from './Proposal' import { SpendingLimit } from './SpendingLimit' import { VaultTransaction } from './VaultTransaction' @@ -19,6 +21,7 @@ export const accountProviders = { VaultBatchTransaction, ConfigTransaction, Multisig, + ProgramConfig, Proposal, SpendingLimit, VaultTransaction, diff --git a/sdk/multisig/src/generated/instructions/index.ts b/sdk/multisig/src/generated/instructions/index.ts index 8b50f45a..23c7f196 100644 --- a/sdk/multisig/src/generated/instructions/index.ts +++ b/sdk/multisig/src/generated/instructions/index.ts @@ -9,11 +9,16 @@ export * from './multisigAddMember' export * from './multisigAddSpendingLimit' export * from './multisigChangeThreshold' export * from './multisigCreate' +export * from './multisigCreateV2' export * from './multisigRemoveMember' export * from './multisigRemoveSpendingLimit' export * from './multisigSetConfigAuthority' export * from './multisigSetRentCollector' export * from './multisigSetTimeLock' +export * from './programConfigInit' +export * from './programConfigSetAuthority' +export * from './programConfigSetMultisigCreationFee' +export * from './programConfigSetTreasury' export * from './proposalActivate' export * from './proposalApprove' export * from './proposalCancel' diff --git a/sdk/multisig/src/generated/instructions/multisigCreateV2.ts b/sdk/multisig/src/generated/instructions/multisigCreateV2.ts new file mode 100644 index 00000000..3d5b75d7 --- /dev/null +++ b/sdk/multisig/src/generated/instructions/multisigCreateV2.ts @@ -0,0 +1,129 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + MultisigCreateArgs, + multisigCreateArgsBeet, +} from '../types/MultisigCreateArgs' + +/** + * @category Instructions + * @category MultisigCreateV2 + * @category generated + */ +export type MultisigCreateV2InstructionArgs = { + args: MultisigCreateArgs +} +/** + * @category Instructions + * @category MultisigCreateV2 + * @category generated + */ +export const multisigCreateV2Struct = new beet.FixableBeetArgsStruct< + MultisigCreateV2InstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', multisigCreateArgsBeet], + ], + 'MultisigCreateV2InstructionArgs' +) +/** + * Accounts required by the _multisigCreateV2_ instruction + * + * @property [] programConfig + * @property [_writable_] treasury + * @property [_writable_] multisig + * @property [**signer**] createKey + * @property [_writable_, **signer**] creator + * @category Instructions + * @category MultisigCreateV2 + * @category generated + */ +export type MultisigCreateV2InstructionAccounts = { + programConfig: web3.PublicKey + treasury: web3.PublicKey + multisig: web3.PublicKey + createKey: web3.PublicKey + creator: web3.PublicKey + systemProgram?: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const multisigCreateV2InstructionDiscriminator = [ + 50, 221, 199, 93, 40, 245, 139, 233, +] + +/** + * Creates a _MultisigCreateV2_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category MultisigCreateV2 + * @category generated + */ +export function createMultisigCreateV2Instruction( + accounts: MultisigCreateV2InstructionAccounts, + args: MultisigCreateV2InstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = multisigCreateV2Struct.serialize({ + instructionDiscriminator: multisigCreateV2InstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.programConfig, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.treasury, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.multisig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.createKey, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.creator, + isWritable: true, + isSigner: true, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/programConfigInit.ts b/sdk/multisig/src/generated/instructions/programConfigInit.ts new file mode 100644 index 00000000..b2f765cf --- /dev/null +++ b/sdk/multisig/src/generated/instructions/programConfigInit.ts @@ -0,0 +1,108 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + ProgramConfigInitArgs, + programConfigInitArgsBeet, +} from '../types/ProgramConfigInitArgs' + +/** + * @category Instructions + * @category ProgramConfigInit + * @category generated + */ +export type ProgramConfigInitInstructionArgs = { + args: ProgramConfigInitArgs +} +/** + * @category Instructions + * @category ProgramConfigInit + * @category generated + */ +export const programConfigInitStruct = new beet.BeetArgsStruct< + ProgramConfigInitInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', programConfigInitArgsBeet], + ], + 'ProgramConfigInitInstructionArgs' +) +/** + * Accounts required by the _programConfigInit_ instruction + * + * @property [_writable_] programConfig + * @property [_writable_, **signer**] initializer + * @category Instructions + * @category ProgramConfigInit + * @category generated + */ +export type ProgramConfigInitInstructionAccounts = { + programConfig: web3.PublicKey + initializer: web3.PublicKey + systemProgram?: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const programConfigInitInstructionDiscriminator = [ + 184, 188, 198, 195, 205, 124, 117, 216, +] + +/** + * Creates a _ProgramConfigInit_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category ProgramConfigInit + * @category generated + */ +export function createProgramConfigInitInstruction( + accounts: ProgramConfigInitInstructionAccounts, + args: ProgramConfigInitInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = programConfigInitStruct.serialize({ + instructionDiscriminator: programConfigInitInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.programConfig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.initializer, + isWritable: true, + isSigner: true, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/programConfigSetAuthority.ts b/sdk/multisig/src/generated/instructions/programConfigSetAuthority.ts new file mode 100644 index 00000000..e3229dd4 --- /dev/null +++ b/sdk/multisig/src/generated/instructions/programConfigSetAuthority.ts @@ -0,0 +1,102 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + ProgramConfigSetAuthorityArgs, + programConfigSetAuthorityArgsBeet, +} from '../types/ProgramConfigSetAuthorityArgs' + +/** + * @category Instructions + * @category ProgramConfigSetAuthority + * @category generated + */ +export type ProgramConfigSetAuthorityInstructionArgs = { + args: ProgramConfigSetAuthorityArgs +} +/** + * @category Instructions + * @category ProgramConfigSetAuthority + * @category generated + */ +export const programConfigSetAuthorityStruct = new beet.BeetArgsStruct< + ProgramConfigSetAuthorityInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', programConfigSetAuthorityArgsBeet], + ], + 'ProgramConfigSetAuthorityInstructionArgs' +) +/** + * Accounts required by the _programConfigSetAuthority_ instruction + * + * @property [_writable_] programConfig + * @property [**signer**] authority + * @category Instructions + * @category ProgramConfigSetAuthority + * @category generated + */ +export type ProgramConfigSetAuthorityInstructionAccounts = { + programConfig: web3.PublicKey + authority: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const programConfigSetAuthorityInstructionDiscriminator = [ + 238, 242, 36, 181, 32, 143, 216, 75, +] + +/** + * Creates a _ProgramConfigSetAuthority_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category ProgramConfigSetAuthority + * @category generated + */ +export function createProgramConfigSetAuthorityInstruction( + accounts: ProgramConfigSetAuthorityInstructionAccounts, + args: ProgramConfigSetAuthorityInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = programConfigSetAuthorityStruct.serialize({ + instructionDiscriminator: programConfigSetAuthorityInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.programConfig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.authority, + isWritable: false, + isSigner: true, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/programConfigSetMultisigCreationFee.ts b/sdk/multisig/src/generated/instructions/programConfigSetMultisigCreationFee.ts new file mode 100644 index 00000000..963e0fbf --- /dev/null +++ b/sdk/multisig/src/generated/instructions/programConfigSetMultisigCreationFee.ts @@ -0,0 +1,104 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + ProgramConfigSetMultisigCreationFeeArgs, + programConfigSetMultisigCreationFeeArgsBeet, +} from '../types/ProgramConfigSetMultisigCreationFeeArgs' + +/** + * @category Instructions + * @category ProgramConfigSetMultisigCreationFee + * @category generated + */ +export type ProgramConfigSetMultisigCreationFeeInstructionArgs = { + args: ProgramConfigSetMultisigCreationFeeArgs +} +/** + * @category Instructions + * @category ProgramConfigSetMultisigCreationFee + * @category generated + */ +export const programConfigSetMultisigCreationFeeStruct = + new beet.BeetArgsStruct< + ProgramConfigSetMultisigCreationFeeInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } + >( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', programConfigSetMultisigCreationFeeArgsBeet], + ], + 'ProgramConfigSetMultisigCreationFeeInstructionArgs' + ) +/** + * Accounts required by the _programConfigSetMultisigCreationFee_ instruction + * + * @property [_writable_] programConfig + * @property [**signer**] authority + * @category Instructions + * @category ProgramConfigSetMultisigCreationFee + * @category generated + */ +export type ProgramConfigSetMultisigCreationFeeInstructionAccounts = { + programConfig: web3.PublicKey + authority: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const programConfigSetMultisigCreationFeeInstructionDiscriminator = [ + 101, 160, 249, 63, 154, 215, 153, 13, +] + +/** + * Creates a _ProgramConfigSetMultisigCreationFee_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category ProgramConfigSetMultisigCreationFee + * @category generated + */ +export function createProgramConfigSetMultisigCreationFeeInstruction( + accounts: ProgramConfigSetMultisigCreationFeeInstructionAccounts, + args: ProgramConfigSetMultisigCreationFeeInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = programConfigSetMultisigCreationFeeStruct.serialize({ + instructionDiscriminator: + programConfigSetMultisigCreationFeeInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.programConfig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.authority, + isWritable: false, + isSigner: true, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/programConfigSetTreasury.ts b/sdk/multisig/src/generated/instructions/programConfigSetTreasury.ts new file mode 100644 index 00000000..5c7a774b --- /dev/null +++ b/sdk/multisig/src/generated/instructions/programConfigSetTreasury.ts @@ -0,0 +1,102 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + ProgramConfigSetTreasuryArgs, + programConfigSetTreasuryArgsBeet, +} from '../types/ProgramConfigSetTreasuryArgs' + +/** + * @category Instructions + * @category ProgramConfigSetTreasury + * @category generated + */ +export type ProgramConfigSetTreasuryInstructionArgs = { + args: ProgramConfigSetTreasuryArgs +} +/** + * @category Instructions + * @category ProgramConfigSetTreasury + * @category generated + */ +export const programConfigSetTreasuryStruct = new beet.BeetArgsStruct< + ProgramConfigSetTreasuryInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', programConfigSetTreasuryArgsBeet], + ], + 'ProgramConfigSetTreasuryInstructionArgs' +) +/** + * Accounts required by the _programConfigSetTreasury_ instruction + * + * @property [_writable_] programConfig + * @property [**signer**] authority + * @category Instructions + * @category ProgramConfigSetTreasury + * @category generated + */ +export type ProgramConfigSetTreasuryInstructionAccounts = { + programConfig: web3.PublicKey + authority: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const programConfigSetTreasuryInstructionDiscriminator = [ + 111, 46, 243, 117, 144, 188, 162, 107, +] + +/** + * Creates a _ProgramConfigSetTreasury_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category ProgramConfigSetTreasury + * @category generated + */ +export function createProgramConfigSetTreasuryInstruction( + accounts: ProgramConfigSetTreasuryInstructionAccounts, + args: ProgramConfigSetTreasuryInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = programConfigSetTreasuryStruct.serialize({ + instructionDiscriminator: programConfigSetTreasuryInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.programConfig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.authority, + isWritable: false, + isSigner: true, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/types/ProgramConfigInitArgs.ts b/sdk/multisig/src/generated/types/ProgramConfigInitArgs.ts new file mode 100644 index 00000000..e3bb8af0 --- /dev/null +++ b/sdk/multisig/src/generated/types/ProgramConfigInitArgs.ts @@ -0,0 +1,29 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beet from '@metaplex-foundation/beet' +import * as beetSolana from '@metaplex-foundation/beet-solana' +export type ProgramConfigInitArgs = { + authority: web3.PublicKey + multisigCreationFee: beet.bignum + treasury: web3.PublicKey +} + +/** + * @category userTypes + * @category generated + */ +export const programConfigInitArgsBeet = + new beet.BeetArgsStruct( + [ + ['authority', beetSolana.publicKey], + ['multisigCreationFee', beet.u64], + ['treasury', beetSolana.publicKey], + ], + 'ProgramConfigInitArgs' + ) diff --git a/sdk/multisig/src/generated/types/ProgramConfigSetAuthorityArgs.ts b/sdk/multisig/src/generated/types/ProgramConfigSetAuthorityArgs.ts new file mode 100644 index 00000000..ba40aeb1 --- /dev/null +++ b/sdk/multisig/src/generated/types/ProgramConfigSetAuthorityArgs.ts @@ -0,0 +1,23 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beetSolana from '@metaplex-foundation/beet-solana' +import * as beet from '@metaplex-foundation/beet' +export type ProgramConfigSetAuthorityArgs = { + newAuthority: web3.PublicKey +} + +/** + * @category userTypes + * @category generated + */ +export const programConfigSetAuthorityArgsBeet = + new beet.BeetArgsStruct( + [['newAuthority', beetSolana.publicKey]], + 'ProgramConfigSetAuthorityArgs' + ) diff --git a/sdk/multisig/src/generated/types/ProgramConfigSetMultisigCreationFeeArgs.ts b/sdk/multisig/src/generated/types/ProgramConfigSetMultisigCreationFeeArgs.ts new file mode 100644 index 00000000..f7f9e07e --- /dev/null +++ b/sdk/multisig/src/generated/types/ProgramConfigSetMultisigCreationFeeArgs.ts @@ -0,0 +1,21 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +export type ProgramConfigSetMultisigCreationFeeArgs = { + newMultisigCreationFee: beet.bignum +} + +/** + * @category userTypes + * @category generated + */ +export const programConfigSetMultisigCreationFeeArgsBeet = + new beet.BeetArgsStruct( + [['newMultisigCreationFee', beet.u64]], + 'ProgramConfigSetMultisigCreationFeeArgs' + ) diff --git a/sdk/multisig/src/generated/types/ProgramConfigSetTreasuryArgs.ts b/sdk/multisig/src/generated/types/ProgramConfigSetTreasuryArgs.ts new file mode 100644 index 00000000..805e8cbf --- /dev/null +++ b/sdk/multisig/src/generated/types/ProgramConfigSetTreasuryArgs.ts @@ -0,0 +1,23 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beetSolana from '@metaplex-foundation/beet-solana' +import * as beet from '@metaplex-foundation/beet' +export type ProgramConfigSetTreasuryArgs = { + newTreasury: web3.PublicKey +} + +/** + * @category userTypes + * @category generated + */ +export const programConfigSetTreasuryArgsBeet = + new beet.BeetArgsStruct( + [['newTreasury', beetSolana.publicKey]], + 'ProgramConfigSetTreasuryArgs' + ) diff --git a/sdk/multisig/src/generated/types/index.ts b/sdk/multisig/src/generated/types/index.ts index e53cad58..7edda603 100644 --- a/sdk/multisig/src/generated/types/index.ts +++ b/sdk/multisig/src/generated/types/index.ts @@ -16,6 +16,10 @@ export * from './MultisigSetRentCollectorArgs' export * from './MultisigSetTimeLockArgs' export * from './Period' export * from './Permissions' +export * from './ProgramConfigInitArgs' +export * from './ProgramConfigSetAuthorityArgs' +export * from './ProgramConfigSetMultisigCreationFeeArgs' +export * from './ProgramConfigSetTreasuryArgs' export * from './ProposalCreateArgs' export * from './ProposalStatus' export * from './ProposalVoteArgs' diff --git a/sdk/multisig/src/instructions/index.ts b/sdk/multisig/src/instructions/index.ts index ab51b43b..1b2b791f 100644 --- a/sdk/multisig/src/instructions/index.ts +++ b/sdk/multisig/src/instructions/index.ts @@ -6,6 +6,7 @@ export * from "./configTransactionAccountsClose.js"; export * from "./configTransactionCreate.js"; export * from "./configTransactionExecute.js"; export * from "./multisigCreate.js"; +export * from "./multisigCreateV2.js"; export * from "./multisigAddMember.js"; export * from "./multisigAddSpendingLimit.js"; export * from "./multisigRemoveSpendingLimit.js"; diff --git a/sdk/multisig/src/instructions/multisigCreate.ts b/sdk/multisig/src/instructions/multisigCreate.ts index ec67b74f..7191c4bb 100644 --- a/sdk/multisig/src/instructions/multisigCreate.ts +++ b/sdk/multisig/src/instructions/multisigCreate.ts @@ -5,6 +5,7 @@ import { PROGRAM_ID, } from "../generated"; +/** @deprecated This instruction is deprecated and will be removed soon. Please use `multisigCreateV2` to ensure future compatibility. */ export function multisigCreate({ creator, multisigPda, diff --git a/sdk/multisig/src/instructions/multisigCreateV2.ts b/sdk/multisig/src/instructions/multisigCreateV2.ts new file mode 100644 index 00000000..e9df7346 --- /dev/null +++ b/sdk/multisig/src/instructions/multisigCreateV2.ts @@ -0,0 +1,56 @@ +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { + createMultisigCreateV2Instruction, + Member, + PROGRAM_ID, +} from "../generated"; +import { getProgramConfigPda } from "../pda"; + +export function multisigCreateV2({ + treasury, + creator, + multisigPda, + configAuthority, + threshold, + members, + timeLock, + createKey, + rentCollector, + memo, + programId = PROGRAM_ID, +}: { + treasury: PublicKey; + creator: PublicKey; + multisigPda: PublicKey; + configAuthority: PublicKey | null; + threshold: number; + members: Member[]; + timeLock: number; + createKey: PublicKey; + rentCollector: PublicKey | null; + memo?: string; + programId?: PublicKey; +}): TransactionInstruction { + const programConfigPda = getProgramConfigPda({ programId })[0]; + + return createMultisigCreateV2Instruction( + { + programConfig: programConfigPda, + treasury, + creator, + createKey, + multisig: multisigPda, + }, + { + args: { + configAuthority, + threshold, + members, + timeLock, + rentCollector, + memo: memo ?? null, + }, + }, + programId + ); +} diff --git a/sdk/multisig/src/pda.ts b/sdk/multisig/src/pda.ts index 735a0c22..043db534 100644 --- a/sdk/multisig/src/pda.ts +++ b/sdk/multisig/src/pda.ts @@ -4,6 +4,7 @@ import { toU32Bytes, toU64Bytes, toU8Bytes, toUtfBytes } from "./utils"; import invariant from "invariant"; const SEED_PREFIX = toUtfBytes("multisig"); +const SEED_PROGRAM_CONFIG = toUtfBytes("program_config"); const SEED_MULTISIG = toUtfBytes("multisig"); const SEED_VAULT = toUtfBytes("vault"); const SEED_TRANSACTION = toUtfBytes("transaction"); @@ -12,6 +13,17 @@ const SEED_BATCH_TRANSACTION = toUtfBytes("batch_transaction"); const SEED_EPHEMERAL_SIGNER = toUtfBytes("ephemeral_signer"); const SEED_SPENDING_LIMIT = toUtfBytes("spending_limit"); +export function getProgramConfigPda({ + programId = PROGRAM_ID, +}: { + programId?: PublicKey; +}): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [SEED_PREFIX, SEED_PROGRAM_CONFIG], + programId + ); +} + export function getMultisigPda({ createKey, programId = PROGRAM_ID, diff --git a/sdk/multisig/src/rpc/index.ts b/sdk/multisig/src/rpc/index.ts index 984572a2..ccad3c34 100644 --- a/sdk/multisig/src/rpc/index.ts +++ b/sdk/multisig/src/rpc/index.ts @@ -9,6 +9,7 @@ export * from "./multisigAddMember.js"; export * from "./multisigAddSpendingLimit.js"; export * from "./multisigRemoveSpendingLimit.js"; export * from "./multisigCreate.js"; +export * from "./multisigCreateV2.js"; export * from "./multisigSetConfigAuthority.js"; export * from "./multisigSetRentCollector.js"; export * from "./multisigSetTimeLock.js"; diff --git a/sdk/multisig/src/rpc/multisigCreate.ts b/sdk/multisig/src/rpc/multisigCreate.ts index 00b0e9fa..1623d4a1 100644 --- a/sdk/multisig/src/rpc/multisigCreate.ts +++ b/sdk/multisig/src/rpc/multisigCreate.ts @@ -9,7 +9,11 @@ import { Member } from "../generated"; import * as transactions from "../transactions"; import { translateAndThrowAnchorError } from "../errors"; -/** Creates a new multisig. */ +/** + * @deprecated This instruction is deprecated and will be removed soon. Please use `multisigCreateV2` to ensure future compatibility. + * + * Creates a new multisig. + */ export async function multisigCreate({ connection, createKey, diff --git a/sdk/multisig/src/rpc/multisigCreateV2.ts b/sdk/multisig/src/rpc/multisigCreateV2.ts new file mode 100644 index 00000000..3f593e4a --- /dev/null +++ b/sdk/multisig/src/rpc/multisigCreateV2.ts @@ -0,0 +1,66 @@ +import { + Connection, + PublicKey, + SendOptions, + Signer, + TransactionSignature, +} from "@solana/web3.js"; +import { Member } from "../generated"; +import * as transactions from "../transactions"; +import { translateAndThrowAnchorError } from "../errors"; + +/** Creates a new multisig. */ +export async function multisigCreateV2({ + connection, + treasury, + createKey, + creator, + multisigPda, + configAuthority, + threshold, + members, + timeLock, + rentCollector, + memo, + sendOptions, + programId, +}: { + connection: Connection; + treasury: PublicKey; + createKey: Signer; + creator: Signer; + multisigPda: PublicKey; + configAuthority: PublicKey | null; + threshold: number; + members: Member[]; + timeLock: number; + rentCollector: PublicKey | null; + memo?: string; + sendOptions?: SendOptions; + programId?: PublicKey; +}): Promise { + const blockhash = (await connection.getLatestBlockhash()).blockhash; + + const tx = transactions.multisigCreateV2({ + blockhash, + treasury, + createKey: createKey.publicKey, + creator: creator.publicKey, + multisigPda, + configAuthority, + threshold, + members, + timeLock, + rentCollector, + memo, + programId, + }); + + tx.sign([creator, createKey]); + + try { + return await connection.sendTransaction(tx, sendOptions); + } catch (err) { + translateAndThrowAnchorError(err); + } +} diff --git a/sdk/multisig/src/transactions/index.ts b/sdk/multisig/src/transactions/index.ts index 984572a2..ccad3c34 100644 --- a/sdk/multisig/src/transactions/index.ts +++ b/sdk/multisig/src/transactions/index.ts @@ -9,6 +9,7 @@ export * from "./multisigAddMember.js"; export * from "./multisigAddSpendingLimit.js"; export * from "./multisigRemoveSpendingLimit.js"; export * from "./multisigCreate.js"; +export * from "./multisigCreateV2.js"; export * from "./multisigSetConfigAuthority.js"; export * from "./multisigSetRentCollector.js"; export * from "./multisigSetTimeLock.js"; diff --git a/sdk/multisig/src/transactions/multisigCreate.ts b/sdk/multisig/src/transactions/multisigCreate.ts index b3a6b8fa..6f896171 100644 --- a/sdk/multisig/src/transactions/multisigCreate.ts +++ b/sdk/multisig/src/transactions/multisigCreate.ts @@ -6,7 +6,11 @@ import { import { Member } from "../generated"; import * as instructions from "../instructions"; -/** Returns unsigned `VersionedTransaction` that needs to be signed by `creator` and `createKey` before sending it. */ +/** + * @deprecated This instruction is deprecated and will be removed soon. Please use `multisigCreateV2` to ensure future compatibility. + * + * Returns unsigned `VersionedTransaction` that needs to be signed by `creator` and `createKey` before sending it. + */ export function multisigCreate({ blockhash, configAuthority, diff --git a/sdk/multisig/src/transactions/multisigCreateV2.ts b/sdk/multisig/src/transactions/multisigCreateV2.ts new file mode 100644 index 00000000..407a7da5 --- /dev/null +++ b/sdk/multisig/src/transactions/multisigCreateV2.ts @@ -0,0 +1,60 @@ +import { + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { Member } from "../generated"; +import * as instructions from "../instructions"; + +/** + * Returns unsigned `VersionedTransaction` that needs to be signed by `creator` and `createKey` before sending it. + */ +export function multisigCreateV2({ + blockhash, + treasury, + configAuthority, + createKey, + creator, + multisigPda, + threshold, + members, + timeLock, + rentCollector, + memo, + programId, +}: { + blockhash: string; + treasury: PublicKey; + createKey: PublicKey; + creator: PublicKey; + multisigPda: PublicKey; + configAuthority: PublicKey | null; + threshold: number; + members: Member[]; + timeLock: number; + rentCollector: PublicKey | null; + memo?: string; + programId?: PublicKey; +}): VersionedTransaction { + const ix = instructions.multisigCreateV2({ + treasury, + creator, + multisigPda, + configAuthority, + threshold, + members, + timeLock, + createKey, + rentCollector, + memo, + programId, + }); + + const message = new TransactionMessage({ + payerKey: creator, + recentBlockhash: blockhash, + instructions: [ix], + }).compileToV0Message(); + + return new VersionedTransaction(message); +} diff --git a/test-program-config-initializer-keypair.json b/test-program-config-initializer-keypair.json new file mode 100644 index 00000000..4a984be8 --- /dev/null +++ b/test-program-config-initializer-keypair.json @@ -0,0 +1 @@ +[168,71,115,45,51,142,144,54,204,224,170,91,5,155,233,68,44,228,177,80,49,178,122,118,161,100,208,237,160,190,226,101,161,60,133,165,247,251,224,202,171,60,28,85,164,124,163,142,17,113,134,163,131,198,241,184,11,245,167,15,20,231,54,125] \ No newline at end of file diff --git a/tests/index.ts b/tests/index.ts index 5cc5c4b4..cc3947f7 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,4 +1,5 @@ // The order of imports is the order the test suite will run in. +import "./suites/program-config-init"; import "./suites/multisig-sdk"; import "./suites/account-migrations"; import "./suites/examples/batch-sol-transfer"; diff --git a/tests/suites/instructions/multisigCreate.ts b/tests/suites/instructions/multisigCreate.ts new file mode 100644 index 00000000..09da8fa3 --- /dev/null +++ b/tests/suites/instructions/multisigCreate.ts @@ -0,0 +1,365 @@ +import * as multisig from "@sqds/multisig"; +import { + comparePubkeys, + createAutonomousMultisig, + createControlledMultisig, + createLocalhostConnection, + generateFundedKeypair, + generateMultisigMembers, + getTestProgramId, + TestMembers, +} from "../../utils"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import assert from "assert"; + +const { Multisig } = multisig.accounts; +const { Permission, Permissions } = multisig.types; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Instructions / multisig_create", () => { + let members: TestMembers; + + before(async () => { + members = await generateMultisigMembers(connection); + }); + + it("error: duplicate member", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreate({ + connection, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + ], + createKey, + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Found multiple members with the same pubkey/ + ); + }); + + it("error: missing signature from `createKey`", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + const tx = multisig.transactions.multisigCreate({ + blockhash: (await connection.getLatestBlockhash()).blockhash, + createKey: createKey.publicKey, + creator: creator.publicKey, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + rentCollector: null, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + ], + programId, + }); + + // Missing signature from `createKey`. + tx.sign([creator]); + + await assert.rejects( + () => connection.sendTransaction(tx, { skipPreflight: true }), + /Transaction signature verification failure/ + ); + }); + + it("error: empty members", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreate({ + connection, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + members: [], + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Members don't include any proposers/ + ); + }); + + it("error: member has unknown permission", async () => { + const creator = await generateFundedKeypair(connection); + const member = Keypair.generate(); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreate({ + connection, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + members: [ + { + key: member.publicKey, + permissions: { + mask: 1 | 2 | 4 | 8, + }, + }, + ], + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Member has unknown permission/ + ); + }); + + // We cannot really test it because we can't pass u16::MAX members to the instruction. + it("error: too many members"); + + it("error: invalid threshold (< 1)", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreate({ + connection, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 0, + members: Object.values(members).map((m) => ({ + key: m.publicKey, + permissions: Permissions.all(), + })), + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Invalid threshold, must be between 1 and number of members/ + ); + }); + + it("error: invalid threshold (> members with permission to Vote)", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreate({ + connection, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + // Can only initiate transactions. + { + key: members.proposer.publicKey, + permissions: Permissions.fromPermissions([Permission.Initiate]), + }, + // Can only vote on transactions. + { + key: members.voter.publicKey, + permissions: Permissions.fromPermissions([Permission.Vote]), + }, + // Can only execute transactions. + { + key: members.executor.publicKey, + permissions: Permissions.fromPermissions([Permission.Execute]), + }, + ], + // Threshold is 3, but there are only 2 voters. + threshold: 3, + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Invalid threshold, must be between 1 and number of members with Vote permission/ + ); + }); + + it("create a new autonomous multisig", async () => { + const createKey = Keypair.generate(); + + const [multisigPda, multisigBump] = await createAutonomousMultisig({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + rentCollector: null, + programId, + }); + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + assert.strictEqual( + multisigAccount.configAuthority.toBase58(), + PublicKey.default.toBase58() + ); + assert.strictEqual(multisigAccount.threshold, 2); + assert.deepEqual( + multisigAccount.members, + [ + { + key: members.almighty.publicKey, + permissions: { + mask: Permission.Initiate | Permission.Vote | Permission.Execute, + }, + }, + { + key: members.proposer.publicKey, + permissions: { + mask: Permission.Initiate, + }, + }, + { + key: members.voter.publicKey, + permissions: { + mask: Permission.Vote, + }, + }, + { + key: members.executor.publicKey, + permissions: { + mask: Permission.Execute, + }, + }, + ].sort((a, b) => comparePubkeys(a.key, b.key)) + ); + assert.strictEqual(multisigAccount.rentCollector, null); + assert.strictEqual(multisigAccount.transactionIndex.toString(), "0"); + assert.strictEqual(multisigAccount.staleTransactionIndex.toString(), "0"); + assert.strictEqual( + multisigAccount.createKey.toBase58(), + createKey.publicKey.toBase58() + ); + assert.strictEqual(multisigAccount.bump, multisigBump); + }); + + it("create a new autonomous multisig with rent reclamation enabled", async () => { + const createKey = Keypair.generate(); + const rentCollector = Keypair.generate().publicKey; + + const [multisigPda, multisigBump] = await createAutonomousMultisig({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + rentCollector, + programId, + }); + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + + assert.strictEqual( + multisigAccount.rentCollector?.toBase58(), + rentCollector.toBase58() + ); + }); + + it("create a new controlled multisig", async () => { + const createKey = Keypair.generate(); + const configAuthority = await generateFundedKeypair(connection); + + const [multisigPda] = await createControlledMultisig({ + connection, + createKey, + configAuthority: configAuthority.publicKey, + members, + threshold: 2, + timeLock: 0, + rentCollector: null, + programId, + }); + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + + assert.strictEqual( + multisigAccount.configAuthority.toBase58(), + configAuthority.publicKey.toBase58() + ); + // We can skip the rest of the assertions because they are already tested + // in the previous case and will be the same here. + }); +}); diff --git a/tests/suites/instructions/multisigCreateV2.ts b/tests/suites/instructions/multisigCreateV2.ts new file mode 100644 index 00000000..71e1e12b --- /dev/null +++ b/tests/suites/instructions/multisigCreateV2.ts @@ -0,0 +1,514 @@ +import * as multisig from "@sqds/multisig"; +import { + comparePubkeys, + createAutonomousMultisigV2, + createControlledMultisigV2, + createLocalhostConnection, + generateFundedKeypair, + generateMultisigMembers, + getTestProgramConfigAuthority, + getTestProgramId, + getTestProgramTreasury, + TestMembers, +} from "../../utils"; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import assert from "assert"; + +const { Multisig } = multisig.accounts; +const { Permission, Permissions } = multisig.types; + +const connection = createLocalhostConnection(); + +const programId = getTestProgramId(); +const programConfigAuthority = getTestProgramConfigAuthority(); +const programTreasury = getTestProgramTreasury(); +const programConfigPda = multisig.getProgramConfigPda({ programId })[0]; + +describe("Instructions / multisig_create_v2", () => { + let members: TestMembers; + let programTreasury: PublicKey; + + before(async () => { + members = await generateMultisigMembers(connection); + + const programConfigPda = multisig.getProgramConfigPda({ programId })[0]; + const programConfig = + await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + programTreasury = programConfig.treasury; + }); + + it("error: duplicate member", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + ], + createKey, + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Found multiple members with the same pubkey/ + ); + }); + + it("error: missing signature from `createKey`", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + const tx = multisig.transactions.multisigCreateV2({ + blockhash: (await connection.getLatestBlockhash()).blockhash, + treasury: programTreasury, + createKey: createKey.publicKey, + creator: creator.publicKey, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + rentCollector: null, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + ], + programId, + }); + + // Missing signature from `createKey`. + tx.sign([creator]); + + await assert.rejects( + () => connection.sendTransaction(tx, { skipPreflight: true }), + /Transaction signature verification failure/ + ); + }); + + it("error: empty members", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + members: [], + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Members don't include any proposers/ + ); + }); + + it("error: member has unknown permission", async () => { + const creator = await generateFundedKeypair(connection); + const member = Keypair.generate(); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + members: [ + { + key: member.publicKey, + permissions: { + mask: 1 | 2 | 4 | 8, + }, + }, + ], + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Member has unknown permission/ + ); + }); + + // We cannot really test it because we can't pass u16::MAX members to the instruction. + it("error: too many members"); + + it("error: invalid threshold (< 1)", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 0, + members: Object.values(members).map((m) => ({ + key: m.publicKey, + permissions: Permissions.all(), + })), + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Invalid threshold, must be between 1 and number of members/ + ); + }); + + it("error: invalid threshold (> members with permission to Vote)", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + await assert.rejects( + () => + multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + // Can only initiate transactions. + { + key: members.proposer.publicKey, + permissions: Permissions.fromPermissions([Permission.Initiate]), + }, + // Can only vote on transactions. + { + key: members.voter.publicKey, + permissions: Permissions.fromPermissions([Permission.Vote]), + }, + // Can only execute transactions. + { + key: members.executor.publicKey, + permissions: Permissions.fromPermissions([Permission.Execute]), + }, + ], + // Threshold is 3, but there are only 2 voters. + threshold: 3, + rentCollector: null, + sendOptions: { skipPreflight: true }, + programId, + }), + /Invalid threshold, must be between 1 and number of members with Vote permission/ + ); + }); + + it("create a new autonomous multisig", async () => { + const createKey = Keypair.generate(); + + const [multisigPda, multisigBump] = await createAutonomousMultisigV2({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + rentCollector: null, + programId, + }); + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + assert.strictEqual( + multisigAccount.configAuthority.toBase58(), + PublicKey.default.toBase58() + ); + assert.strictEqual(multisigAccount.threshold, 2); + assert.deepEqual( + multisigAccount.members, + [ + { + key: members.almighty.publicKey, + permissions: { + mask: Permission.Initiate | Permission.Vote | Permission.Execute, + }, + }, + { + key: members.proposer.publicKey, + permissions: { + mask: Permission.Initiate, + }, + }, + { + key: members.voter.publicKey, + permissions: { + mask: Permission.Vote, + }, + }, + { + key: members.executor.publicKey, + permissions: { + mask: Permission.Execute, + }, + }, + ].sort((a, b) => comparePubkeys(a.key, b.key)) + ); + assert.strictEqual(multisigAccount.rentCollector, null); + assert.strictEqual(multisigAccount.transactionIndex.toString(), "0"); + assert.strictEqual(multisigAccount.staleTransactionIndex.toString(), "0"); + assert.strictEqual( + multisigAccount.createKey.toBase58(), + createKey.publicKey.toBase58() + ); + assert.strictEqual(multisigAccount.bump, multisigBump); + }); + + it("create a new autonomous multisig with rent reclamation enabled", async () => { + const createKey = Keypair.generate(); + const rentCollector = Keypair.generate().publicKey; + + const [multisigPda, multisigBump] = await createAutonomousMultisigV2({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + rentCollector, + programId, + }); + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + + assert.strictEqual( + multisigAccount.rentCollector?.toBase58(), + rentCollector.toBase58() + ); + }); + + it("create a new controlled multisig", async () => { + const createKey = Keypair.generate(); + const configAuthority = await generateFundedKeypair(connection); + + const [multisigPda] = await createControlledMultisigV2({ + connection, + createKey, + configAuthority: configAuthority.publicKey, + members, + threshold: 2, + timeLock: 0, + rentCollector: null, + programId, + }); + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + + assert.strictEqual( + multisigAccount.configAuthority.toBase58(), + configAuthority.publicKey.toBase58() + ); + // We can skip the rest of the assertions because they are already tested + // in the previous case and will be the same here. + }); + + it("create a new multisig and pay creation fee", async () => { + //region Airdrop to the program config authority + let signature = await connection.requestAirdrop( + programConfigAuthority.publicKey, + LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + //endregion + + const multisigCreationFee = 0.1 * LAMPORTS_PER_SOL; + + //region Configure the global multisig creation fee + const setCreationFeeIx = + multisig.generated.createProgramConfigSetMultisigCreationFeeInstruction( + { + programConfig: programConfigPda, + authority: programConfigAuthority.publicKey, + }, + { + args: { newMultisigCreationFee: multisigCreationFee }, + }, + programId + ); + const message = new TransactionMessage({ + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + payerKey: programConfigAuthority.publicKey, + instructions: [setCreationFeeIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([programConfigAuthority]); + signature = await connection.sendTransaction(tx); + await connection.confirmTransaction(signature); + let programConfig = + await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + assert.strictEqual( + programConfig.multisigCreationFee.toString(), + multisigCreationFee.toString() + ); + //endregion + + //region Create a new multisig + const creator = await generateFundedKeypair(connection); + const createKey = Keypair.generate(); + + const creatorBalancePre = await connection.getBalance(creator.publicKey); + + const multisigPda = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + })[0]; + + signature = await multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 2, + members: [ + { key: members.almighty.publicKey, permissions: Permissions.all() }, + { + key: members.proposer.publicKey, + permissions: Permissions.fromPermissions([Permission.Initiate]), + }, + { + key: members.voter.publicKey, + permissions: Permissions.fromPermissions([Permission.Vote]), + }, + { + key: members.executor.publicKey, + permissions: Permissions.fromPermissions([Permission.Execute]), + }, + ], + rentCollector: null, + programId, + sendOptions: { skipPreflight: true }, + }); + await connection.confirmTransaction(signature); + + const creatorBalancePost = await connection.getBalance(creator.publicKey); + const rentAndNetworkFee = 2515600; + + assert.strictEqual( + creatorBalancePost, + creatorBalancePre - rentAndNetworkFee - multisigCreationFee + ); + //endregion + + //region Reset the global multisig creation fee + const resetCreationFeeIx = + multisig.generated.createProgramConfigSetMultisigCreationFeeInstruction( + { + programConfig: programConfigPda, + authority: programConfigAuthority.publicKey, + }, + { + args: { newMultisigCreationFee: 0 }, + }, + programId + ); + const message2 = new TransactionMessage({ + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + payerKey: programConfigAuthority.publicKey, + instructions: [resetCreationFeeIx], + }).compileToV0Message(); + const tx2 = new VersionedTransaction(message2); + tx2.sign([programConfigAuthority]); + signature = await connection.sendTransaction(tx2); + await connection.confirmTransaction(signature); + programConfig = await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + assert.strictEqual(programConfig.multisigCreationFee.toString(), "0"); + //endregion + }); +}); diff --git a/tests/suites/multisig-sdk.ts b/tests/suites/multisig-sdk.ts index 61425fab..dad811b5 100644 --- a/tests/suites/multisig-sdk.ts +++ b/tests/suites/multisig-sdk.ts @@ -7,6 +7,7 @@ import { import * as multisig from "@sqds/multisig"; import * as assert from "assert"; import { + comparePubkeys, createAutonomousMultisig, createControlledMultisig, createLocalhostConnection, @@ -18,13 +19,9 @@ import { TestMembers, } from "../utils"; -const { toBigInt } = multisig.utils; -const { Multisig, VaultTransaction, ConfigTransaction, Proposal, Batch } = - multisig.accounts; -const { Permission, Permissions } = multisig.types; - -const programId = getTestProgramId(); - +// Import test suites. +import "./instructions/multisigCreate"; +import "./instructions/multisigCreateV2"; import "./instructions/multisigSetRentCollector"; import "./instructions/configTransactionExecute"; import "./instructions/configTransactionAccountsClose"; @@ -32,6 +29,13 @@ import "./instructions/vaultBatchTransactionAccountClose"; import "./instructions/batchAccountsClose"; import "./instructions/vaultTransactionAccountsClose"; +const { toBigInt } = multisig.utils; +const { Multisig, VaultTransaction, ConfigTransaction, Proposal } = + multisig.accounts; +const { Permission, Permissions } = multisig.types; + +const programId = getTestProgramId(); + describe("Multisig SDK", () => { const connection = createLocalhostConnection(); @@ -41,346 +45,6 @@ describe("Multisig SDK", () => { members = await generateMultisigMembers(connection); }); - describe("multisig_create", () => { - it("error: duplicate member", async () => { - const creator = await generateFundedKeypair(connection); - - const createKey = Keypair.generate(); - const [multisigPda] = multisig.getMultisigPda({ - createKey: createKey.publicKey, - programId, - }); - - await assert.rejects( - () => - multisig.rpc.multisigCreate({ - connection, - creator, - multisigPda, - configAuthority: null, - timeLock: 0, - threshold: 1, - members: [ - { - key: members.almighty.publicKey, - permissions: Permissions.all(), - }, - { - key: members.almighty.publicKey, - permissions: Permissions.all(), - }, - ], - createKey, - rentCollector: null, - sendOptions: { skipPreflight: true }, - programId, - }), - /Found multiple members with the same pubkey/ - ); - }); - - it("error: missing signature from `createKey`", async () => { - const creator = await generateFundedKeypair(connection); - - const createKey = Keypair.generate(); - const [multisigPda] = multisig.getMultisigPda({ - createKey: createKey.publicKey, - programId, - }); - - const tx = multisig.transactions.multisigCreate({ - blockhash: (await connection.getLatestBlockhash()).blockhash, - createKey: createKey.publicKey, - creator: creator.publicKey, - multisigPda, - configAuthority: null, - timeLock: 0, - threshold: 1, - rentCollector: null, - members: [ - { - key: members.almighty.publicKey, - permissions: Permissions.all(), - }, - { - key: members.almighty.publicKey, - permissions: Permissions.all(), - }, - ], - programId, - }); - - // Missing signature from `createKey`. - tx.sign([creator]); - - await assert.rejects( - () => connection.sendTransaction(tx, { skipPreflight: true }), - /Transaction signature verification failure/ - ); - }); - - it("error: empty members", async () => { - const creator = await generateFundedKeypair(connection); - - const createKey = Keypair.generate(); - const [multisigPda] = multisig.getMultisigPda({ - createKey: createKey.publicKey, - programId, - }); - - await assert.rejects( - () => - multisig.rpc.multisigCreate({ - connection, - createKey, - creator, - multisigPda, - configAuthority: null, - timeLock: 0, - threshold: 1, - members: [], - rentCollector: null, - sendOptions: { skipPreflight: true }, - programId, - }), - /Members don't include any proposers/ - ); - }); - - it("error: member has unknown permission", async () => { - const creator = await generateFundedKeypair(connection); - const member = Keypair.generate(); - - const createKey = Keypair.generate(); - const [multisigPda] = multisig.getMultisigPda({ - createKey: createKey.publicKey, - programId, - }); - - await assert.rejects( - () => - multisig.rpc.multisigCreate({ - connection, - createKey, - creator, - multisigPda, - configAuthority: null, - timeLock: 0, - threshold: 1, - members: [ - { - key: member.publicKey, - permissions: { - mask: 1 | 2 | 4 | 8, - }, - }, - ], - rentCollector: null, - sendOptions: { skipPreflight: true }, - programId, - }), - /Member has unknown permission/ - ); - }); - - // We cannot really test it because we can't pass u16::MAX members to the instruction. - it("error: too many members"); - - it("error: invalid threshold (< 1)", async () => { - const creator = await generateFundedKeypair(connection); - - const createKey = Keypair.generate(); - const [multisigPda] = multisig.getMultisigPda({ - createKey: createKey.publicKey, - programId, - }); - - await assert.rejects( - () => - multisig.rpc.multisigCreate({ - connection, - createKey, - creator, - multisigPda, - configAuthority: null, - timeLock: 0, - threshold: 0, - members: Object.values(members).map((m) => ({ - key: m.publicKey, - permissions: Permissions.all(), - })), - rentCollector: null, - sendOptions: { skipPreflight: true }, - programId, - }), - /Invalid threshold, must be between 1 and number of members/ - ); - }); - - it("error: invalid threshold (> members with permission to Vote)", async () => { - const creator = await generateFundedKeypair(connection); - - const createKey = Keypair.generate(); - const [multisigPda] = multisig.getMultisigPda({ - createKey: createKey.publicKey, - programId, - }); - - await assert.rejects( - () => - multisig.rpc.multisigCreate({ - connection, - createKey, - creator, - multisigPda, - configAuthority: null, - timeLock: 0, - members: [ - { - key: members.almighty.publicKey, - permissions: Permissions.all(), - }, - // Can only initiate transactions. - { - key: members.proposer.publicKey, - permissions: Permissions.fromPermissions([Permission.Initiate]), - }, - // Can only vote on transactions. - { - key: members.voter.publicKey, - permissions: Permissions.fromPermissions([Permission.Vote]), - }, - // Can only execute transactions. - { - key: members.executor.publicKey, - permissions: Permissions.fromPermissions([Permission.Execute]), - }, - ], - // Threshold is 3, but there are only 2 voters. - threshold: 3, - rentCollector: null, - sendOptions: { skipPreflight: true }, - programId, - }), - /Invalid threshold, must be between 1 and number of members with Vote permission/ - ); - }); - - it("create a new autonomous multisig", async () => { - const createKey = Keypair.generate(); - - const [multisigPda, multisigBump] = await createAutonomousMultisig({ - connection, - createKey, - members, - threshold: 2, - timeLock: 0, - rentCollector: null, - programId, - }); - - const multisigAccount = await Multisig.fromAccountAddress( - connection, - multisigPda - ); - assert.strictEqual( - multisigAccount.configAuthority.toBase58(), - PublicKey.default.toBase58() - ); - assert.strictEqual(multisigAccount.threshold, 2); - assert.deepEqual( - multisigAccount.members, - [ - { - key: members.almighty.publicKey, - permissions: { - mask: Permission.Initiate | Permission.Vote | Permission.Execute, - }, - }, - { - key: members.proposer.publicKey, - permissions: { - mask: Permission.Initiate, - }, - }, - { - key: members.voter.publicKey, - permissions: { - mask: Permission.Vote, - }, - }, - { - key: members.executor.publicKey, - permissions: { - mask: Permission.Execute, - }, - }, - ].sort((a, b) => comparePubkeys(a.key, b.key)) - ); - assert.strictEqual(multisigAccount.rentCollector, null); - assert.strictEqual(multisigAccount.transactionIndex.toString(), "0"); - assert.strictEqual(multisigAccount.staleTransactionIndex.toString(), "0"); - assert.strictEqual( - multisigAccount.createKey.toBase58(), - createKey.publicKey.toBase58() - ); - assert.strictEqual(multisigAccount.bump, multisigBump); - }); - - it("create a new autonomous multisig with rent reclamation enabled", async () => { - const createKey = Keypair.generate(); - const rentCollector = Keypair.generate().publicKey; - - const [multisigPda, multisigBump] = await createAutonomousMultisig({ - connection, - createKey, - members, - threshold: 2, - timeLock: 0, - rentCollector, - programId, - }); - - const multisigAccount = await Multisig.fromAccountAddress( - connection, - multisigPda - ); - - assert.strictEqual( - multisigAccount.rentCollector?.toBase58(), - rentCollector.toBase58() - ); - }); - - it("create a new controlled multisig", async () => { - const createKey = Keypair.generate(); - const configAuthority = await generateFundedKeypair(connection); - - const [multisigPda] = await createControlledMultisig({ - connection, - createKey, - configAuthority: configAuthority.publicKey, - members, - threshold: 2, - timeLock: 0, - rentCollector: null, - programId, - }); - - const multisigAccount = await Multisig.fromAccountAddress( - connection, - multisigPda - ); - - assert.strictEqual( - multisigAccount.configAuthority.toBase58(), - configAuthority.publicKey.toBase58() - ); - // We can skip the rest of the assertions because they are already tested - // in the previous case and will be the same here. - }); - }); - describe("multisig_add_member", () => { const newMember = { key: Keypair.generate().publicKey, @@ -2594,7 +2258,3 @@ describe("Multisig SDK", () => { }); }); }); - -function comparePubkeys(a: PublicKey, b: PublicKey) { - return a.toBuffer().compare(b.toBuffer()); -} diff --git a/tests/suites/program-config-init.ts b/tests/suites/program-config-init.ts new file mode 100644 index 00000000..e6ff2e5b --- /dev/null +++ b/tests/suites/program-config-init.ts @@ -0,0 +1,222 @@ +import * as multisig from "@sqds/multisig"; +import { + createLocalhostConnection, + generateFundedKeypair, + getTestProgramConfigAuthority, + getTestProgramConfigInitializer, + getTestProgramId, + getTestProgramTreasury, +} from "../utils"; +import { + LAMPORTS_PER_SOL, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import assert from "assert"; + +const programId = getTestProgramId(); +const programConfigInitializer = getTestProgramConfigInitializer(); +const programConfigAuthority = getTestProgramConfigAuthority(); +const programTreasury = getTestProgramTreasury(); +const programConfigPda = multisig.getProgramConfigPda({ programId })[0]; + +const connection = createLocalhostConnection(); + +describe("Initialize Global ProgramConfig", () => { + before(async () => { + // Airdrop to the program config initializer + const signature = await connection.requestAirdrop( + programConfigInitializer.publicKey, + LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + }); + + it("error: invalid initializer", async () => { + const fakeInitializer = await generateFundedKeypair(connection); + + const initIx = multisig.generated.createProgramConfigInitInstruction( + { + programConfig: programConfigPda, + initializer: fakeInitializer.publicKey, + }, + { + args: { + authority: programConfigAuthority.publicKey, + treasury: programTreasury, + multisigCreationFee: 0, + }, + }, + programId + ); + + const blockhash = (await connection.getLatestBlockhash()).blockhash; + const message = new TransactionMessage({ + recentBlockhash: blockhash, + payerKey: fakeInitializer.publicKey, + instructions: [initIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([fakeInitializer]); + + await assert.rejects( + () => + connection + .sendRawTransaction(tx.serialize()) + .catch(multisig.errors.translateAndThrowAnchorError), + /Unauthorized: Attempted to perform an unauthorized action/ + ); + }); + + it("error: `authority` is PublicKey.default", async () => { + const initIx = multisig.generated.createProgramConfigInitInstruction( + { + programConfig: programConfigPda, + initializer: programConfigInitializer.publicKey, + }, + { + args: { + authority: PublicKey.default, + treasury: programTreasury, + multisigCreationFee: 0, + }, + }, + programId + ); + + const blockhash = (await connection.getLatestBlockhash()).blockhash; + const message = new TransactionMessage({ + recentBlockhash: blockhash, + payerKey: programConfigInitializer.publicKey, + instructions: [initIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([programConfigInitializer]); + + await assert.rejects( + () => + connection + .sendRawTransaction(tx.serialize()) + .catch(multisig.errors.translateAndThrowAnchorError), + /InvalidAccount: Invalid account provided/ + ); + }); + + it("error: `treasury` is PublicKey.default", async () => { + const initIx = multisig.generated.createProgramConfigInitInstruction( + { + programConfig: programConfigPda, + initializer: programConfigInitializer.publicKey, + }, + { + args: { + authority: programConfigAuthority.publicKey, + treasury: PublicKey.default, + multisigCreationFee: 0, + }, + }, + programId + ); + + const blockhash = (await connection.getLatestBlockhash()).blockhash; + const message = new TransactionMessage({ + recentBlockhash: blockhash, + payerKey: programConfigInitializer.publicKey, + instructions: [initIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([programConfigInitializer]); + + await assert.rejects( + () => + connection + .sendRawTransaction(tx.serialize()) + .catch(multisig.errors.translateAndThrowAnchorError), + /InvalidAccount: Invalid account provided/ + ); + }); + + it("initialize program config", async () => { + const initIx = multisig.generated.createProgramConfigInitInstruction( + { + programConfig: programConfigPda, + initializer: programConfigInitializer.publicKey, + }, + { + args: { + authority: programConfigAuthority.publicKey, + treasury: programTreasury, + multisigCreationFee: 0, + }, + }, + programId + ); + + const blockhash = (await connection.getLatestBlockhash()).blockhash; + const message = new TransactionMessage({ + recentBlockhash: blockhash, + payerKey: programConfigInitializer.publicKey, + instructions: [initIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([programConfigInitializer]); + const sig = await connection.sendRawTransaction(tx.serialize()); + await connection.confirmTransaction(sig); + + const programConfigData = + await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + + assert.strictEqual( + programConfigData.authority.toBase58(), + programConfigAuthority.publicKey.toBase58() + ); + assert.strictEqual(programConfigData.multisigCreationFee.toString(), "0"); + assert.strictEqual( + programConfigData.treasury.toBase58(), + programTreasury.toBase58() + ); + }); + + it("error: initialize program config twice", async () => { + const initIx = multisig.generated.createProgramConfigInitInstruction( + { + programConfig: programConfigPda, + initializer: programConfigInitializer.publicKey, + }, + { + args: { + authority: programConfigAuthority.publicKey, + treasury: programTreasury, + multisigCreationFee: 0, + }, + }, + programId + ); + + const blockhash = (await connection.getLatestBlockhash()).blockhash; + const message = new TransactionMessage({ + recentBlockhash: blockhash, + payerKey: programConfigInitializer.publicKey, + instructions: [initIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(message); + tx.sign([programConfigInitializer]); + + const err = await connection + .sendRawTransaction(tx.serialize()) + .catch((err) => { + return err; + }); + + assert.ok(multisig.errors.isErrorWithLogs(err)); + assert.ok( + err.logs.find((line) => { + return line.includes("already in use"); + }) + ); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 95162735..4ce574c9 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -33,6 +33,44 @@ export function getTestProgramId() { return programKeypair.publicKey; } +export function getTestProgramConfigInitializer() { + return Keypair.fromSecretKey( + Buffer.from( + JSON.parse( + readFileSync( + path.join( + __dirname, + "../test-program-config-initializer-keypair.json" + ), + "utf-8" + ) + ) + ) + ); +} + +export function getTestProgramConfigAuthority() { + return Keypair.fromSecretKey( + new Uint8Array([ + 58, 1, 5, 229, 201, 214, 134, 29, 37, 52, 43, 109, 207, 214, 183, 48, 98, + 98, 141, 175, 249, 88, 126, 84, 69, 100, 223, 58, 255, 212, 102, 90, 107, + 20, 85, 127, 19, 55, 155, 38, 5, 66, 116, 148, 35, 139, 23, 147, 13, 179, + 188, 20, 37, 180, 156, 157, 85, 137, 29, 133, 29, 66, 224, 91, + ]) + ); +} + +export function getTestProgramTreasury() { + return Keypair.fromSecretKey( + new Uint8Array([ + 232, 179, 154, 90, 210, 236, 13, 219, 79, 25, 133, 75, 156, 226, 144, 171, + 193, 108, 104, 128, 11, 221, 29, 219, 139, 195, 211, 242, 231, 36, 196, + 31, 76, 110, 20, 42, 135, 60, 143, 79, 151, 67, 78, 132, 247, 97, 157, 8, + 86, 47, 10, 52, 72, 7, 88, 121, 175, 107, 108, 245, 215, 149, 242, 20, + ]) + ).publicKey; +} + export type TestMembers = { almighty: Keypair; proposer: Keypair; @@ -143,6 +181,71 @@ export async function createAutonomousMultisig({ return [multisigPda, multisigBump] as const; } +export async function createAutonomousMultisigV2({ + connection, + createKey = Keypair.generate(), + members, + threshold, + timeLock, + rentCollector, + programId, +}: { + createKey?: Keypair; + members: TestMembers; + threshold: number; + timeLock: number; + rentCollector: PublicKey | null; + connection: Connection; + programId: PublicKey; +}) { + const creator = await generateFundedKeypair(connection); + + const programConfig = + await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + multisig.getProgramConfigPda({ programId })[0] + ); + const programTreasury = programConfig.treasury; + + const [multisigPda, multisigBump] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + const signature = await multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + creator, + multisigPda, + configAuthority: null, + timeLock, + threshold, + members: [ + { key: members.almighty.publicKey, permissions: Permissions.all() }, + { + key: members.proposer.publicKey, + permissions: Permissions.fromPermissions([Permission.Initiate]), + }, + { + key: members.voter.publicKey, + permissions: Permissions.fromPermissions([Permission.Vote]), + }, + { + key: members.executor.publicKey, + permissions: Permissions.fromPermissions([Permission.Execute]), + }, + ], + createKey: createKey, + rentCollector, + sendOptions: { skipPreflight: true }, + programId, + }); + + await connection.confirmTransaction(signature); + + return [multisigPda, multisigBump] as const; +} + export async function createControlledMultisig({ connection, createKey = Keypair.generate(), @@ -202,6 +305,73 @@ export async function createControlledMultisig({ return [multisigPda, multisigBump] as const; } +export async function createControlledMultisigV2({ + connection, + createKey = Keypair.generate(), + configAuthority, + members, + threshold, + timeLock, + rentCollector, + programId, +}: { + createKey?: Keypair; + configAuthority: PublicKey; + members: TestMembers; + threshold: number; + timeLock: number; + rentCollector: PublicKey | null; + connection: Connection; + programId: PublicKey; +}) { + const creator = await generateFundedKeypair(connection); + + const [multisigPda, multisigBump] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + }); + + const programConfig = + await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + multisig.getProgramConfigPda({ programId })[0] + ); + const programTreasury = programConfig.treasury; + + const signature = await multisig.rpc.multisigCreateV2({ + connection, + treasury: programTreasury, + creator, + multisigPda, + configAuthority, + timeLock, + threshold, + members: [ + { key: members.almighty.publicKey, permissions: Permissions.all() }, + { + key: members.proposer.publicKey, + permissions: Permissions.fromPermissions([Permission.Initiate]), + }, + { + key: members.voter.publicKey, + permissions: Permissions.fromPermissions([Permission.Vote]), + }, + { + key: members.executor.publicKey, + permissions: Permissions.fromPermissions([Permission.Execute]), + }, + ], + createKey: createKey, + rentCollector, + sendOptions: { skipPreflight: true }, + programId, + }); + + await connection.confirmTransaction(signature); + + return [multisigPda, multisigBump] as const; +} + export type MultisigWithRentReclamationAndVariousBatches = { multisigPda: PublicKey; /** @@ -1015,3 +1185,7 @@ export function range(min: number, max: number, step: number = 1) { } return result; } + +export function comparePubkeys(a: PublicKey, b: PublicKey) { + return a.toBuffer().compare(b.toBuffer()); +}