use solana_pubkey::Pubkey;
use solana_sdk::signature::{Keypair, Signer};
use crate::{
plan_error::PlanError,
token::{account_state::TokenAccountState, NativeMintWrappingStrategy, WorkspacePlanConfig},
};
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum TokenAccountStrategy {
WithoutBalance(Pubkey),
WithBalance(Pubkey, u64),
}
impl TokenAccountStrategy {
pub fn mint(&self) -> Pubkey {
match self {
TokenAccountStrategy::WithoutBalance(m) => *m,
TokenAccountStrategy::WithBalance(m, _) => *m,
}
}
}
pub type TokenAccountInstructions = solana_token_toolkit::TokenAccountPlan;
pub fn plan_token_account_instructions(
state: &TokenAccountState,
strategies: &[TokenAccountStrategy],
config: &WorkspacePlanConfig,
keypair_for_wsol: Option<Keypair>,
) -> Result<TokenAccountInstructions, PlanError> {
if config.wsol_strategy == NativeMintWrappingStrategy::Keypair && keypair_for_wsol.is_none() {
return Err(PlanError::TokenAccountPlanning(
"NativeMintWrappingStrategy::Keypair requires a keypair_for_wsol arg".into(),
));
}
let mut plan =
crate::token::adapter::prepare_token_accounts_from_strategies(state, strategies, config)?;
if config.wsol_strategy == NativeMintWrappingStrategy::Keypair {
let keypair = keypair_for_wsol.expect("validated above: Keypair strategy requires keypair");
apply_keypair_replacement(&mut plan, keypair)?;
}
Ok(plan)
}
pub(super) fn apply_keypair_replacement(
plan: &mut TokenAccountInstructions,
keypair: Keypair,
) -> Result<(), PlanError> {
match plan.additional_signers.len() {
0 => {
return Err(PlanError::TokenAccountPlanning(
"Keypair wrapping requested but toolkit produced no additional_signers".into(),
));
}
1 => {}
n => {
return Err(PlanError::TokenAccountPlanning(format!(
"Keypair wrapping expected exactly 1 toolkit-generated signer, got {n}; workspace \
replacement logic only handles the single-wSOL case"
)));
}
}
let generated_pubkey = plan.additional_signers[0].pubkey();
let replacement_pubkey = keypair.pubkey();
for ix in &mut plan.create_instructions {
for account in &mut ix.accounts {
if account.pubkey == generated_pubkey {
account.pubkey = replacement_pubkey;
}
}
}
for ix in &mut plan.cleanup_instructions {
for account in &mut ix.accounts {
if account.pubkey == generated_pubkey {
account.pubkey = replacement_pubkey;
}
}
}
for address in plan.token_account_addresses.values_mut() {
if *address == generated_pubkey {
*address = replacement_pubkey;
}
}
plan.additional_signers = vec![keypair];
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use solana_instruction::{AccountMeta, Instruction};
use solana_sdk::signature::{Keypair, Signer};
use spl_token_interface::native_mint;
use super::*;
fn empty_plan() -> TokenAccountInstructions {
TokenAccountInstructions {
create_instructions: vec![],
cleanup_instructions: vec![],
additional_signers: vec![],
token_account_addresses: HashMap::new(),
}
}
fn ix_with_account(pk: Pubkey) -> Instruction {
Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![AccountMeta::new(pk, false)],
data: vec![],
}
}
#[test]
fn keypair_strategy_without_keypair_arg_errors_via_public_api() {
let owner = Pubkey::new_unique();
let state = TokenAccountState { owner, mints: HashMap::new() };
let config = WorkspacePlanConfig {
wsol_strategy: NativeMintWrappingStrategy::Keypair,
..Default::default()
};
let strategies = [TokenAccountStrategy::WithBalance(native_mint::ID, 1_000_000)];
let err = plan_token_account_instructions(&state, &strategies, &config, None)
.expect_err("expected Err when Keypair strategy is set without keypair_for_wsol");
let msg = match err {
PlanError::TokenAccountPlanning(s) => s,
other => panic!("expected TokenAccountPlanning, got {other:?}"),
};
assert!(
msg.contains("Keypair") && msg.contains("keypair_for_wsol"),
"unexpected error message: {msg}"
);
}
#[test]
fn apply_keypair_replacement_errors_on_zero_signers() {
let mut plan = empty_plan();
let user_keypair = Keypair::new();
let err = apply_keypair_replacement(&mut plan, user_keypair)
.expect_err("expected Err when plan has zero additional_signers");
let msg = match err {
PlanError::TokenAccountPlanning(s) => s,
other => panic!("expected TokenAccountPlanning, got {other:?}"),
};
assert!(msg.contains("no additional_signers"), "unexpected error message: {msg}");
}
#[test]
fn apply_keypair_replacement_errors_on_multi_signer() {
let mut plan = empty_plan();
plan.additional_signers = vec![Keypair::new(), Keypair::new()];
let user_keypair = Keypair::new();
let err = apply_keypair_replacement(&mut plan, user_keypair)
.expect_err("expected Err when plan has more than one additional_signer");
let msg = match err {
PlanError::TokenAccountPlanning(s) => s,
other => panic!("expected TokenAccountPlanning, got {other:?}"),
};
assert!(
msg.contains("got 2") && msg.contains("single-wSOL"),
"unexpected error message: {msg}"
);
}
#[test]
fn apply_keypair_replacement_swaps_pubkeys_in_all_locations() {
let toolkit_keypair = Keypair::new();
let toolkit_pubkey = toolkit_keypair.pubkey();
let unrelated_mint = Pubkey::new_unique();
let unrelated_pubkey = Pubkey::new_unique();
let mut plan = empty_plan();
plan.create_instructions =
vec![ix_with_account(toolkit_pubkey), ix_with_account(unrelated_pubkey)];
plan.cleanup_instructions = vec![ix_with_account(toolkit_pubkey)];
plan.additional_signers = vec![toolkit_keypair];
plan.token_account_addresses.insert(native_mint::ID, toolkit_pubkey);
plan.token_account_addresses.insert(unrelated_mint, unrelated_pubkey);
let user_keypair = Keypair::new();
let user_pubkey = user_keypair.pubkey();
apply_keypair_replacement(&mut plan, user_keypair).expect("happy path should succeed");
assert_eq!(plan.create_instructions[0].accounts[0].pubkey, user_pubkey);
assert_eq!(plan.create_instructions[1].accounts[0].pubkey, unrelated_pubkey);
assert_eq!(plan.cleanup_instructions[0].accounts[0].pubkey, user_pubkey);
assert_eq!(plan.token_account_addresses[&native_mint::ID], user_pubkey);
assert_eq!(plan.token_account_addresses[&unrelated_mint], unrelated_pubkey);
assert_eq!(plan.additional_signers.len(), 1);
assert_eq!(plan.additional_signers[0].pubkey(), user_pubkey);
}
}