use borsh::{BorshDeserialize, BorshSerialize};
use sha2::{Digest, Sha256};
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
use std::io::{Read as IoRead, Write};
use std::marker::PhantomData;
pub const SQUADS_PROGRAM_ID: &str = "Sqds111111111111111111111111111111111111111";
pub const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111";
pub const SEED_PREFIX: &[u8] = b"multisig";
pub const SEED_MULTISIG: &[u8] = b"multisig";
pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config";
pub const SEED_TRANSACTION: &[u8] = b"transaction";
pub const SEED_PROPOSAL: &[u8] = b"proposal";
pub const SEED_VAULT: &[u8] = b"vault";
#[derive(Clone, Debug, Default)]
pub struct SmallVec<L, T>(Vec<T>, PhantomData<L>);
impl<L, T> From<SmallVec<L, T>> for Vec<T> {
fn from(val: SmallVec<L, T>) -> Self {
val.0
}
}
impl<L, T> From<Vec<T>> for SmallVec<L, T> {
fn from(val: Vec<T>) -> Self {
Self(val, PhantomData)
}
}
impl<T: BorshSerialize> BorshSerialize for SmallVec<u8, T> {
fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
let len = u8::try_from(self.0.len()).map_err(|_| std::io::ErrorKind::InvalidInput)?;
writer.write_all(&len.to_le_bytes())?;
for item in &self.0 {
item.serialize(writer)?;
}
Ok(())
}
}
impl<T: BorshDeserialize> BorshDeserialize for SmallVec<u8, T> {
fn deserialize_reader<R: IoRead>(reader: &mut R) -> std::io::Result<Self> {
let mut len_bytes = [0u8; 1];
reader.read_exact(&mut len_bytes)?;
let len = u8::from_le_bytes(len_bytes) as usize;
let mut vec = Vec::with_capacity(len);
for _ in 0..len {
vec.push(T::deserialize_reader(reader)?);
}
Ok(Self(vec, PhantomData))
}
}
impl<T: BorshSerialize> BorshSerialize for SmallVec<u16, T> {
fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
let len = u16::try_from(self.0.len()).map_err(|_| std::io::ErrorKind::InvalidInput)?;
writer.write_all(&len.to_le_bytes())?;
for item in &self.0 {
item.serialize(writer)?;
}
Ok(())
}
}
impl<T: BorshDeserialize> BorshDeserialize for SmallVec<u16, T> {
fn deserialize_reader<R: IoRead>(reader: &mut R) -> std::io::Result<Self> {
let mut len_bytes = [0u8; 2];
reader.read_exact(&mut len_bytes)?;
let len = u16::from_le_bytes(len_bytes) as usize;
let mut vec = Vec::with_capacity(len);
for _ in 0..len {
vec.push(T::deserialize_reader(reader)?);
}
Ok(Self(vec, PhantomData))
}
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct Member {
pub key: Pubkey,
pub permissions: Permissions,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct Permissions {
pub mask: u8,
}
impl Permissions {
pub fn all() -> Self {
Self {
mask: Permission::Initiate as u8 | Permission::Vote as u8 | Permission::Execute as u8,
}
}
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum Permission {
Initiate = 1 << 0, Vote = 1 << 1, Execute = 1 << 2, }
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct Multisig {
pub create_key: Pubkey,
pub config_authority: Pubkey,
pub threshold: u16,
pub time_lock: u32,
pub transaction_index: u64,
pub stale_transaction_index: u64,
pub rent_collector: Option<Pubkey>,
pub bump: u8,
pub members: Vec<Member>,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct MultisigCreateArgsV2 {
pub config_authority: Option<Pubkey>,
pub threshold: u16,
pub members: Vec<Member>,
pub time_lock: u32,
pub rent_collector: Option<Pubkey>,
pub memo: Option<String>,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct VaultTransactionCreateArgs {
pub vault_index: u8,
pub ephemeral_signers: u8,
pub transaction_message: Vec<u8>,
pub memo: Option<String>,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct ProposalCreateArgs {
pub transaction_index: u64,
pub draft: bool,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct TransactionMessage {
pub num_signers: u8,
pub num_writable_signers: u8,
pub num_writable_non_signers: u8,
pub account_keys: SmallVec<u8, Pubkey>,
pub instructions: SmallVec<u8, CompiledInstruction>,
pub address_table_lookups: SmallVec<u8, MessageAddressTableLookup>,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct VaultTransaction {
pub multisig: Pubkey,
pub creator: Pubkey,
pub index: u64,
pub bump: u8,
pub vault_index: u8,
pub vault_bump: u8,
pub ephemeral_signer_bumps: Vec<u8>,
pub message: VaultTransactionMessage,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct VaultTransactionMessage {
pub num_signers: u8,
pub num_writable_signers: u8,
pub num_writable_non_signers: u8,
pub account_keys: Vec<Pubkey>,
pub instructions: Vec<MultisigCompiledInstruction>,
pub address_table_lookups: Vec<MultisigMessageAddressTableLookup>,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct MultisigCompiledInstruction {
pub program_id_index: u8,
pub account_indexes: Vec<u8>,
pub data: Vec<u8>,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct MultisigMessageAddressTableLookup {
pub account_key: Pubkey,
pub writable_indexes: Vec<u8>,
pub readonly_indexes: Vec<u8>,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct CompiledInstruction {
pub program_id_index: u8,
pub account_indexes: SmallVec<u8, u8>,
pub data: SmallVec<u16, u8>,
}
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct MessageAddressTableLookup {
pub account_key: Pubkey,
pub writable_indexes: Vec<u8>,
pub readonly_indexes: Vec<u8>,
}
pub fn parse_transaction_index(account_data: &[u8]) -> eyre::Result<u64> {
const OFFSET: usize = 8 + 32 + 32 + 2 + 4; if account_data.len() < OFFSET + 8 {
return Err(eyre::eyre!("Invalid multisig account data (too short)"));
}
let tx_index_bytes: [u8; 8] = account_data[OFFSET..OFFSET + 8]
.try_into()
.map_err(|_| eyre::eyre!("Failed to parse transaction_index"))?;
Ok(u64::from_le_bytes(tx_index_bytes))
}
pub fn anchor_discriminator(namespace: &str, name: &str) -> [u8; 8] {
let preimage = format!("{}:{}", namespace, name);
let hash = Sha256::digest(preimage.as_bytes());
let mut disc = [0u8; 8];
disc.copy_from_slice(&hash[0..8]);
disc
}
pub fn get_program_config_pda(program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[SEED_PREFIX, SEED_PROGRAM_CONFIG], program_id)
}
pub fn get_multisig_pda(create_key: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[SEED_PREFIX, SEED_MULTISIG, create_key.as_ref()],
program_id,
)
}
pub fn get_vault_transaction_pda(
multisig: &Pubkey,
transaction_index: u64,
program_id: &Pubkey,
) -> (Pubkey, u8) {
let next_index = transaction_index
.checked_add(1)
.expect("transaction_index overflow");
Pubkey::find_program_address(
&[
SEED_PREFIX,
multisig.as_ref(),
SEED_TRANSACTION,
&next_index.to_le_bytes(),
],
program_id,
)
}
pub fn get_proposal_pda(
multisig: &Pubkey,
transaction_index: u64,
program_id: &Pubkey,
) -> (Pubkey, u8) {
let next_index = transaction_index
.checked_add(1)
.expect("transaction_index overflow");
Pubkey::find_program_address(
&[
SEED_PREFIX,
multisig.as_ref(),
SEED_TRANSACTION,
&next_index.to_le_bytes(),
SEED_PROPOSAL,
],
program_id,
)
}
pub fn get_vault_pda(multisig: &Pubkey, vault_index: u8, program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
SEED_PREFIX,
multisig.as_ref(),
SEED_VAULT,
&vault_index.to_le_bytes(),
],
program_id,
)
}
pub fn compile_instruction_to_transaction_message(
instruction: &Instruction,
vault_pubkey: &Pubkey,
) -> TransactionMessage {
let mut account_keys = Vec::new();
let mut account_key_indexes = std::collections::HashMap::new();
account_keys.push(*vault_pubkey);
account_key_indexes.insert(*vault_pubkey, 0u8);
for account_meta in &instruction.accounts {
account_key_indexes
.entry(account_meta.pubkey)
.or_insert_with(|| {
let index = account_keys.len() as u8;
account_keys.push(account_meta.pubkey);
index
});
}
account_key_indexes
.entry(instruction.program_id)
.or_insert_with(|| {
let index = account_keys.len() as u8;
account_keys.push(instruction.program_id);
index
});
let mut writable_signers = vec![*vault_pubkey]; let mut readonly_signers = Vec::new();
let mut writable_non_signers = Vec::new();
let mut readonly_non_signers = Vec::new();
for account_meta in &instruction.accounts {
if account_meta.pubkey == *vault_pubkey {
continue; }
if account_meta.is_signer {
if account_meta.is_writable {
writable_signers.push(account_meta.pubkey);
} else {
readonly_signers.push(account_meta.pubkey);
}
} else if account_meta.is_writable {
writable_non_signers.push(account_meta.pubkey);
} else {
readonly_non_signers.push(account_meta.pubkey);
}
}
if instruction.program_id != *vault_pubkey
&& !readonly_non_signers.contains(&instruction.program_id)
{
readonly_non_signers.push(instruction.program_id);
}
let mut ordered_keys = Vec::new();
ordered_keys.extend(&writable_signers);
ordered_keys.extend(&readonly_signers);
ordered_keys.extend(&writable_non_signers);
ordered_keys.extend(&readonly_non_signers);
let mut key_index_map = std::collections::HashMap::new();
for (i, key) in ordered_keys.iter().enumerate() {
key_index_map.insert(*key, i as u8);
}
let program_id_index = *key_index_map.get(&instruction.program_id).unwrap();
let account_indexes: Vec<u8> = instruction
.accounts
.iter()
.map(|meta| *key_index_map.get(&meta.pubkey).unwrap())
.collect();
let compiled_instruction = CompiledInstruction {
program_id_index,
account_indexes: account_indexes.into(),
data: instruction.data.clone().into(),
};
let num_signers = (writable_signers.len() + readonly_signers.len()) as u8;
let num_writable_signers = writable_signers.len() as u8;
let num_writable_non_signers = writable_non_signers.len() as u8;
TransactionMessage {
num_signers,
num_writable_signers,
num_writable_non_signers,
account_keys: ordered_keys.into(),
instructions: vec![compiled_instruction].into(),
address_table_lookups: Vec::new().into(), }
}