From 37361620db1fe28da3ec6645a6f9fa78428a2f17 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Tue, 17 Mar 2026 16:51:15 -0700 Subject: [PATCH] fix: clamp negative amounts to 0 in AmountWrapper deserializer The staking service can send negative remainingStakingAmount (e.g. "-2282880") when the unstake amount exceeds the current balance. The u64 deserializer rejected these with "invalid digit found in string", preventing the intent from being processed at all. The intent builder already guards with > 0 checks before using amount values (e.g. partial unstake in build.rs), so clamping negatives to 0 at deserialization is safe and lets the full unstake path proceed correctly. Ticket: BTC-3184 --- packages/wasm-solana/src/intent/types.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/wasm-solana/src/intent/types.rs b/packages/wasm-solana/src/intent/types.rs index 13c0581..ced31fe 100644 --- a/packages/wasm-solana/src/intent/types.rs +++ b/packages/wasm-solana/src/intent/types.rs @@ -139,7 +139,13 @@ pub struct AmountWrapper { pub symbol: Option, } -/// Deserialize amount from either string or number (for JS BigInt compatibility) +/// Deserialize amount from either string or number (for JS BigInt compatibility). +/// +/// Negative values are clamped to 0 rather than rejected. The staking service can +/// send negative `remainingStakingAmount` when the unstake amount exceeds the current +/// balance. Callers already guard with `> 0` checks (e.g. build.rs partial unstake), +/// so clamping is safe and avoids a deserialization error that would prevent the +/// intent from being processed at all. fn deserialize_amount<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -166,14 +172,19 @@ where where E: de::Error, { - u64::try_from(v).map_err(|_| de::Error::custom("negative amount")) + Ok(u64::try_from(v).unwrap_or(0)) } fn visit_str(self, v: &str) -> Result where E: de::Error, { - v.parse().map_err(de::Error::custom) + // Try u64 first, fall back to i64 parse and clamp negatives to 0 + v.parse::().or_else(|_| { + v.parse::() + .map(|n| u64::try_from(n).unwrap_or(0)) + .map_err(de::Error::custom) + }) } }