use serde_json::{json, Value}; use std::collections::HashSet; fn positive_units(amount: &str, decimals: u8) -> Result { let (whole, fraction) = amount.split_once('.').unwrap_or((amount, "")); if whole.is_empty() || amount.ends_with('.') || !whole.bytes().chain(fraction.bytes()).all(|b| b.is_ascii_digit()) { return Err("Use a positive decimal amount, such as 12.5.".into()); } let width = usize::from(decimals); if fraction.len() > width { return Err(format!("This mint allows {decimals} decimal places.")); } let units = format!("{whole}{fraction}{}", "0".repeat(width - fraction.len())); let parsed = units.trim_start_matches('0').parse::() .map_err(|_| "Amount is zero or exceeds the SPL token limit.".to_string())?; Ok(parsed) } pub fn plan(input: &Value) -> Result { let decimals = input["decimals"].as_u64().and_then(|v| u8::try_from(v).ok()) .ok_or("Invalid mint decimals.")?; let lines = input["list"].as_str().ok_or("Paste a recipient list.")?; if lines.len() > 20000 { return Err("The list is too large. Use up to 100 wallets.".into()); } let mut seen = HashSet::new(); let mut rows = Vec::new(); let mut total = 0u64; for (index, line) in lines.lines().enumerate() { let line = line.trim(); if line.is_empty() { continue; } let number = index + 1; let (wallet, amount) = line.split_once(',').ok_or_else(|| format!("Row {number} needs wallet,amount."))?; let wallet = crate::validate_address(wallet).map_err(|_| format!("Row {number} has an invalid wallet address."))?; if !seen.insert(wallet.clone()) { return Err(format!("Row {number} repeats a wallet. Combine its amounts into one row.")); } let units = positive_units(amount.trim(), decimals).map_err(|e| format!("Row {number}: {e}"))?; total = total.checked_add(units).ok_or("Total exceeds the SPL token limit.")?; rows.push(json!({"wallet":wallet,"units":units.to_string(),"amount":crate::format_amount(&units.to_string(),decimals)?})); if rows.len() > 100 { return Err("Use up to 100 wallets per plan.".into()); } } if rows.is_empty() { return Err("Add at least one wallet and amount.".into()); } let batches: Vec<_> = rows.chunks(3).map(|batch| batch.to_vec()).collect(); Ok(json!({"rows":rows,"batches":batches,"total":crate::format_amount(&total.to_string(),decimals)?,"totalUnits":total.to_string()})) }