use solana_pubkey::Pubkey;
use solana_token_toolkit::{MintIntent, TokenAccountIntent, TokenAccountPlan, TokenAccountState};
use spl_token_interface::native_mint;
use crate::token::{
account_planner::TokenAccountStrategy, NativeMintWrappingStrategy, WorkspacePlanConfig,
};
pub fn workspace_to_toolkit_wsol(
strategy: NativeMintWrappingStrategy,
) -> Result<solana_token_toolkit::WrapSolStrategy, crate::plan_error::PlanError> {
match strategy {
NativeMintWrappingStrategy::Ata => Ok(solana_token_toolkit::WrapSolStrategy::Ata),
NativeMintWrappingStrategy::Keypair => Ok(solana_token_toolkit::WrapSolStrategy::Keypair),
NativeMintWrappingStrategy::None => Ok(solana_token_toolkit::WrapSolStrategy::None),
NativeMintWrappingStrategy::Seed => {
Err(crate::plan_error::PlanError::TokenAccountPlanning(
"Seed wSOL wrapping is not supported in the pure planner; use the async shell"
.into(),
))
}
}
}
pub fn strategy_to_intent_pair(
strat: &TokenAccountStrategy,
config: &WorkspacePlanConfig,
) -> (Pubkey, MintIntent) {
let toolkit_wsol = match config.wsol_strategy {
NativeMintWrappingStrategy::Ata => solana_token_toolkit::WrapSolStrategy::Ata,
NativeMintWrappingStrategy::Keypair => solana_token_toolkit::WrapSolStrategy::Keypair,
NativeMintWrappingStrategy::None => solana_token_toolkit::WrapSolStrategy::None,
NativeMintWrappingStrategy::Seed => solana_token_toolkit::WrapSolStrategy::Ata,
};
match strat {
TokenAccountStrategy::WithoutBalance(mint) => (*mint, MintIntent::EnsureAtaExists),
TokenAccountStrategy::WithBalance(mint, _)
if *mint == native_mint::ID
&& toolkit_wsol == solana_token_toolkit::WrapSolStrategy::None =>
{
(*mint, MintIntent::EnsureAtaExists)
}
TokenAccountStrategy::WithBalance(mint, lamports) if *mint == native_mint::ID => {
(*mint, MintIntent::WithBalance { lamports: *lamports })
}
TokenAccountStrategy::WithBalance(mint, 0) => (*mint, MintIntent::EnsureAtaExists),
TokenAccountStrategy::WithBalance(mint, _) if !config.enforce_balance => {
(*mint, MintIntent::EnsureAtaExists)
}
TokenAccountStrategy::WithBalance(mint, amount) => {
(*mint, MintIntent::RequireTokenBalance { amount: *amount })
}
}
}
pub fn strategies_to_intent(
strategies: &[TokenAccountStrategy],
config: &WorkspacePlanConfig,
) -> TokenAccountIntent {
use std::collections::HashMap;
let mut mints = HashMap::with_capacity(strategies.len());
for strat in strategies {
let (mint, intent) = strategy_to_intent_pair(strat, config);
mints.insert(mint, intent);
}
TokenAccountIntent { mints }
}
pub fn prepare_token_accounts_from_strategies(
state: &TokenAccountState,
strategies: &[TokenAccountStrategy],
config: &WorkspacePlanConfig,
) -> Result<TokenAccountPlan, crate::plan_error::PlanError> {
const RENT_EXEMPT_TOKEN_ACCOUNT_LAMPORTS: u64 = 2_039_280;
let intent = strategies_to_intent(strategies, config);
prepare_token_accounts_workspace(state, &intent, RENT_EXEMPT_TOKEN_ACCOUNT_LAMPORTS, config)
}
pub fn prepare_token_accounts_workspace(
state: &TokenAccountState,
intent: &TokenAccountIntent,
rent_exempt_lamports: u64,
config: &WorkspacePlanConfig,
) -> Result<TokenAccountPlan, crate::plan_error::PlanError> {
let toolkit_strategy = workspace_to_toolkit_wsol(config.wsol_strategy)?;
solana_token_toolkit::prepare_token_accounts(
state,
intent,
solana_token_toolkit::TokenAccountPlanConfig {
wsol_strategy: toolkit_strategy,
ata_create_mode: solana_token_toolkit::AtaCreateMode::Legacy,
rent_exempt_lamports,
},
)
.map_err(|e| crate::plan_error::PlanError::TokenAccountPlanning(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn ata_default() -> WorkspacePlanConfig {
WorkspacePlanConfig::default()
}
fn no_enforce() -> WorkspacePlanConfig {
WorkspacePlanConfig { enforce_balance: false, ..Default::default() }
}
fn no_wrap() -> WorkspacePlanConfig {
WorkspacePlanConfig {
wsol_strategy: NativeMintWrappingStrategy::None,
..Default::default()
}
}
#[test]
fn without_balance_maps_to_ensure_ata_exists() {
let mint = Pubkey::new_unique();
let strat = TokenAccountStrategy::WithoutBalance(mint);
assert_eq!(
strategy_to_intent_pair(&strat, &ata_default()),
(mint, MintIntent::EnsureAtaExists)
);
}
#[test]
fn with_balance_native_mint_maps_to_with_balance() {
let strat = TokenAccountStrategy::WithBalance(native_mint::ID, 1_500_000_000);
assert_eq!(
strategy_to_intent_pair(&strat, &ata_default()),
(native_mint::ID, MintIntent::WithBalance { lamports: 1_500_000_000 }),
);
}
#[test]
fn with_balance_non_sol_mint_maps_to_require_token_balance() {
let mint = Pubkey::new_unique();
let strat = TokenAccountStrategy::WithBalance(mint, 999_999);
assert_eq!(
strategy_to_intent_pair(&strat, &ata_default()),
(mint, MintIntent::RequireTokenBalance { amount: 999_999 }),
);
}
#[test]
fn with_balance_non_sol_mint_with_enforce_off_drops_to_ensure_ata_exists() {
let mint = Pubkey::new_unique();
let strat = TokenAccountStrategy::WithBalance(mint, 999_999);
assert_eq!(
strategy_to_intent_pair(&strat, &no_enforce()),
(mint, MintIntent::EnsureAtaExists),
);
}
#[test]
fn with_zero_balance_non_sol_mint_maps_to_ensure_ata_exists() {
let mint = Pubkey::new_unique();
let strat = TokenAccountStrategy::WithBalance(mint, 0);
assert_eq!(
strategy_to_intent_pair(&strat, &ata_default()),
(mint, MintIntent::EnsureAtaExists)
);
}
#[test]
fn native_mint_with_no_wrap_maps_to_ensure_ata_exists() {
let strat = TokenAccountStrategy::WithBalance(native_mint::ID, 5_000_000);
assert_eq!(
strategy_to_intent_pair(&strat, &no_wrap()),
(native_mint::ID, MintIntent::EnsureAtaExists),
);
}
#[test]
fn empty_strategies_produces_empty_intent() {
let intent = strategies_to_intent(&[], &ata_default());
assert!(intent.mints.is_empty());
}
#[test]
fn mixed_strategies_round_trip() {
let mint_a = Pubkey::new_unique();
let strategies = vec![
TokenAccountStrategy::WithoutBalance(mint_a),
TokenAccountStrategy::WithBalance(native_mint::ID, 500),
];
let intent = strategies_to_intent(&strategies, &ata_default());
assert_eq!(intent.mints.len(), 2);
assert_eq!(intent.mints[&mint_a], MintIntent::EnsureAtaExists);
assert_eq!(intent.mints[&native_mint::ID], MintIntent::WithBalance { lamports: 500 });
}
#[test]
fn mixed_strategies_with_non_sol_balance_uses_require_token_balance() {
let mint_a = Pubkey::new_unique();
let mint_b = Pubkey::new_unique();
let strategies = vec![
TokenAccountStrategy::WithoutBalance(mint_a),
TokenAccountStrategy::WithBalance(native_mint::ID, 500),
TokenAccountStrategy::WithBalance(mint_b, 1_000_000),
];
let intent = strategies_to_intent(&strategies, &ata_default());
assert_eq!(intent.mints.len(), 3);
assert_eq!(intent.mints[&mint_a], MintIntent::EnsureAtaExists);
assert_eq!(intent.mints[&native_mint::ID], MintIntent::WithBalance { lamports: 500 });
assert_eq!(intent.mints[&mint_b], MintIntent::RequireTokenBalance { amount: 1_000_000 });
}
#[test]
fn workspace_to_toolkit_wsol_seed_errors() {
let result = workspace_to_toolkit_wsol(NativeMintWrappingStrategy::Seed);
assert!(result.is_err(), "Seed should fail in pure planner");
}
}