wp-solana-core 0.1.2

Cross-protocol shared core for waterpump-solana: token planning compatibility, error types, system constants.
Documentation
//! Async I/O shells for token-account preparation.
//!
//! **Layering note:** unlike the rest of `wp-solana-core::token`, this module
//! makes RPC calls and is NOT pure planning. It lives here to avoid duplicating
//! the same legacy-wSOL logic across every protocol SDK crate (Meteora,
//! Raydium, …). Once `solana-token-toolkit` adds native support for
//! `WrapSolStrategy::Seed`, this module can be deleted entirely and callers can
//! switch to toolkit's own async shell.

use std::time::{Duration, SystemTime, UNIX_EPOCH};

use solana_client::nonblocking::rpc_client::RpcClient;
use solana_program_pack::Pack;
use solana_pubkey::Pubkey;
use solana_sdk::hash::hashv;
use solana_system_interface::instruction::create_account_with_seed;
use spl_token::state::Account as SplTokenAccount;
use spl_token_interface::{
    instruction::{close_account, initialize_account3},
    native_mint, ID as TOKEN_PROGRAM_ID,
};

use crate::token::{
    account_planner::{TokenAccountInstructions, TokenAccountStrategy},
    account_state::TokenAccountState,
    adapter::{prepare_token_accounts_workspace, strategies_to_intent},
    NativeMintWrappingStrategy, WorkspacePlanConfig,
};

/// Build a token-account plan that uses the legacy seeded-wSOL pattern.
///
/// Toolkit v0.2 supports `WrapSolStrategy::{Ata, Keypair, None}` but not
/// `Seed`. This shell handles that one case by:
/// 1. Stripping native SOL from the spec + state
/// 2. Calling toolkit with `WrapSolStrategy::Ata` for the remaining mints
/// 3. Appending `create_account_with_seed` + `initialize_account3` +
///    `close_account` instructions for the seeded wSOL account
/// 4. Recording the seed-derived pubkey in `token_account_addresses`
///
/// The seed is derived from `(owner, current millis, TOKEN_PROGRAM_ID)`. This
/// makes the resulting wSOL pubkey non-deterministic across runs but unique
/// per transaction — acceptable for one-shot CLI flows. Callers needing
/// deterministic seeds should switch to `WrapSolStrategy::Keypair` with a
/// caller-supplied keypair.
///
/// Takes the original `WorkspacePlanConfig` so that `enforce_balance` is
/// honored for the non-SOL portion. The `wsol_strategy` field is forced to
/// `Ata` for the inner toolkit call (we synthesize the seeded wSOL ourselves).
///
/// Caller must have already verified that `spec` contains `native_mint::ID`
/// and that `config.wsol_strategy == Seed`.
pub async fn prepare_seeded_wsol_legacy(
    rpc: &RpcClient,
    owner: Pubkey,
    spec: Vec<TokenAccountStrategy>,
    state: TokenAccountState,
    config: &WorkspacePlanConfig,
) -> Result<TokenAccountInstructions, Box<dyn std::error::Error>> {
    let mints: Vec<Pubkey> = spec.iter().map(TokenAccountStrategy::mint).collect();
    let native_mint_index = mints
        .iter()
        .position(|m| *m == native_mint::ID)
        .ok_or("prepare_seeded_wsol_legacy called without native mint in spec")?;
    let native_strategy = spec[native_mint_index];

    let spec_without_wsol: Vec<TokenAccountStrategy> =
        spec.iter().copied().filter(|s| s.mint() != native_mint::ID).collect();

    let state_without_wsol = TokenAccountState {
        owner: state.owner,
        mints: state
            .mints
            .iter()
            .filter(|(k, _)| **k != native_mint::ID)
            .map(|(k, v)| (*k, v.clone()))
            .collect(),
    };

    // Force Ata for the inner planner; the seeded path appends wSOL ix directly.
    let inner_config = WorkspacePlanConfig {
        wsol_strategy: NativeMintWrappingStrategy::Ata,
        enforce_balance: config.enforce_balance,
    };
    let intent_without_wsol = strategies_to_intent(&spec_without_wsol, &inner_config);
    let rent_exempt = rpc.get_minimum_balance_for_rent_exemption(SplTokenAccount::LEN).await?;
    let mut result = prepare_token_accounts_workspace(
        &state_without_wsol,
        &intent_without_wsol,
        rent_exempt,
        &inner_config,
    )?;

    let mut lamports = rent_exempt;
    if let TokenAccountStrategy::WithBalance(_, balance) = native_strategy {
        lamports += balance;
    }

    let seed = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or(Duration::from_secs(0))
        .as_millis()
        .to_string();
    let hash_result = hashv(&[owner.as_ref(), seed.as_bytes(), TOKEN_PROGRAM_ID.as_ref()]);
    let pubkey = Pubkey::from(hash_result.to_bytes());

    result.create_instructions.push(create_account_with_seed(
        &owner,
        &pubkey,
        &owner,
        &seed,
        lamports,
        SplTokenAccount::LEN as u64,
        &TOKEN_PROGRAM_ID,
    ));

    result.create_instructions.push(initialize_account3(
        &TOKEN_PROGRAM_ID,
        &pubkey,
        &native_mint::ID,
        &owner,
    )?);

    result.cleanup_instructions.push(close_account(
        &TOKEN_PROGRAM_ID,
        &pubkey,
        &owner,
        &owner,
        &[],
    )?);

    result.token_account_addresses.insert(native_mint::ID, pubkey);

    Ok(result)
}