wp-solana-core 0.1.2

Cross-protocol shared core for waterpump-solana: token planning compatibility, error types, system constants.
Documentation
//! Adapter between the workspace's domain types ([`TokenAccountStrategy`],
//! [`WorkspacePlanConfig`]) and `solana_token_toolkit`'s intent / config
//! shape.
//!
//! All workspace planning policies — zero-amount short-circuit,
//! `enforce_balance` bypass, native+`None` short-circuit, forced
//! `AtaCreateMode::Legacy`, rent constant — live here. Protocol-core
//! plan fns and SDK async shells call through this module instead of
//! talking to toolkit directly.
//!
//! All entry points take `&WorkspacePlanConfig` explicitly; no globals.

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,
};

/// Translate a workspace `NativeMintWrappingStrategy` into the toolkit's
/// `WrapSolStrategy`. `Seed` is workspace-only — the caller (an async shell)
/// must intercept it before reaching this conversion.
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(),
            ))
        }
    }
}

/// Convert a workspace `TokenAccountStrategy` into a (mint, intent) pair
/// using the supplied config.
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,
        // Seed isn't representable in toolkit's MintIntent; the SDK shell
        // intercepts it before reaching here. Use Ata mapping as a safe default
        // so this fn stays infallible — the seeded path doesn't consume the
        // result of strategy_to_intent_pair anyway.
        NativeMintWrappingStrategy::Seed => solana_token_toolkit::WrapSolStrategy::Ata,
    };

    match strat {
        TokenAccountStrategy::WithoutBalance(mint) => (*mint, MintIntent::EnsureAtaExists),
        // Native SOL with WrapSolStrategy::None: caller wants to spend SOL
        // directly (no wSOL wrapping), so the toolkit only needs to ensure the
        // ATA exists; balance is satisfied from the system account.
        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 })
        }
        // Zero-amount short-circuit: matches pre-v0.1 workspace behaviour.
        // Asking the toolkit to validate "balance >= 0" is always trivially
        // true and would emit a redundant RequireTokenBalance intent, so
        // collapse to EnsureAtaExists. Avoids spurious failures in callers
        // that pass `WithBalance(mint, 0)` as a "create-the-ATA" signal.
        TokenAccountStrategy::WithBalance(mint, 0) => (*mint, MintIntent::EnsureAtaExists),
        // enforce_balance = false reproduces the v0.1 silent-drop behaviour
        // (non-SOL WithBalance degrades to EnsureAtaExists). Used by callers
        // that have their own pre-flight balance check or accept the runtime
        // failure mode for missing balance.
        TokenAccountStrategy::WithBalance(mint, _) if !config.enforce_balance => {
            (*mint, MintIntent::EnsureAtaExists)
        }
        TokenAccountStrategy::WithBalance(mint, amount) => {
            (*mint, MintIntent::RequireTokenBalance { amount: *amount })
        }
    }
}

/// Convert a slice of `TokenAccountStrategy` into a toolkit intent using the
/// supplied config.
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 }
}

/// Top-level convenience entry for protocol-core plan fns. Takes a workspace
/// strategy slice + config, produces a toolkit `TokenAccountPlan`.
///
/// Equivalent to `strategies_to_intent` + `prepare_token_accounts_workspace`
/// with the workspace's standard rent-exempt constant.
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)
}

/// Prepare token accounts through the toolkit using workspace policies.
///
/// Forces `AtaCreateMode::Legacy` so emitted create-ATA instructions use the
/// non-idempotent encoding the workspace standardized on (matches the
/// pre-toolkit-migration planner output). The wSOL strategy comes from
/// `config`. SDK async shells use this directly when they need to control the
/// rent value or supply a custom intent (e.g. the seeded-wSOL path).
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");
    }
}