Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,18 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletError::PlatformShieldCapacityExceeded { .. } => {
PlatformWalletFFIResultCode::ErrorShieldedInsufficientBalance
}
// The per-input sibling of the account-capacity variant above: a
// live pre-broadcast per-input shortfall (a stale-snapshot race).
// It rides the SAME capacity code — the host's corrective action is
// identical (refresh preflight, retry) — but as its OWN wallet
// variant so the message names the offending address and the typed
// `available`/`required` are understood as that single input's live
// figures, not an account maximum. Minting a distinct FFI code was
// deliberately avoided to not collide with the in-flight code-space
// frontier; the disambiguation lives in the message.
PlatformWalletError::PlatformShieldInputShortfall { .. } => {
PlatformWalletFFIResultCode::ErrorShieldedInsufficientBalance
}
// The core-transaction sibling of the shielded pair above: the
// do-not-retry signal must survive the boundary as a typed code
// so hosts can distinguish it from a definitive rejection.
Expand Down
51 changes: 42 additions & 9 deletions packages/rs-platform-wallet-ffi/src/shielded_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,15 +597,19 @@ fn map_spend_result(
format!("{operation} failed: {e}"),
),
// The cached Platform Payment-account set no longer covers the
// requested claim plus input-0's fee reserve. Keep this distinct from
// generic wallet-operation failures so hosts can refresh preflight and
// re-confirm a smaller amount instead of retrying unchanged.
Err(e @ PlatformWalletError::PlatformShieldCapacityExceeded { .. }) => {
PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorShieldedInsufficientBalance,
format!("{operation} failed: {e}"),
)
}
// requested claim plus input-0's fee reserve (account-wide), or a live
// per-input hard balance check found one input short (a stale-snapshot
// race). Both share this code — the host's corrective action is the same
// (refresh preflight, retry) — and both stay distinct from generic
// wallet-operation failures so a host never retries the stale amount
// unchanged. The per-input variant's message names the short address.
Err(
e @ (PlatformWalletError::PlatformShieldCapacityExceeded { .. }
| PlatformWalletError::PlatformShieldInputShortfall { .. }),
) => PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorShieldedInsufficientBalance,
format!("{operation} failed: {e}"),
),
Err(e) => PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorWalletOperation,
format!("{operation} failed: {e}"),
Expand Down Expand Up @@ -1852,6 +1856,35 @@ mod tests {
);
}

#[test]
fn map_spend_result_maps_per_input_shortfall_to_same_code_with_address() {
// The per-input shortfall (a live stale-snapshot race) must ride the
// same code as the account-capacity variant — not regress to the
// generic ErrorWalletOperation — and keep the offending address in the
// message so a host never misreads the single input's balance as the
// account maximum.
let result = map_spend_result(
Err(PlatformWalletError::PlatformShieldInputShortfall {
address: "yShieldInputAddrExample".to_string(),
available: 3_623_849_220,
required: 3_623_849_221,
}),
"shielded shield",
);

assert_eq!(
result.code,
PlatformWalletFFIResultCode::ErrorShieldedInsufficientBalance
);
let message = message_of(&result);
assert!(
message.contains("yShieldInputAddrExample"),
"message: {message}"
);
assert!(message.contains("3623849220"));
assert!(message.contains("3623849221"));
}

#[test]
fn map_asset_lock_funding_result_preserves_already_consumed_code_only() {
let out_point = dashcore::OutPoint {
Expand Down
20 changes: 20 additions & 0 deletions packages/rs-platform-wallet/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,26 @@ pub enum PlatformWalletError {
#[error("Platform shield capacity exceeded: available {available}, required {required}")]
PlatformShieldCapacityExceeded { available: u64, required: u64 },

/// A shield's pre-broadcast per-input hard balance check found ONE input
/// address short: its live on-chain balance dropped below what the cached
/// planner snapshot assumed (a stale-snapshot race), so the fetched claim
/// cannot be funded. STRICTLY per-input — `available`/`required` are that
/// single address's live figures, NOT an account-capacity total. Distinct
/// from [`PlatformShieldCapacityExceeded`](Self::PlatformShieldCapacityExceeded)
/// (an account-wide deterministic-selection limit) precisely so a host never
/// misreads this per-address `available` as the account maximum (it would
/// understate capacity by up to the versioned max input count). The
/// offending `address` (bech32m) is preserved in the message rather than
/// dropped. Nothing was built or broadcast; refresh preflight and retry.
#[error(
"Shield input address {address} is short: has {available}, requires at least {required}"
)]
PlatformShieldInputShortfall {
address: String,
available: u64,
required: u64,
},

#[error("Shielded build error: {0}")]
ShieldedBuildError(String),

Expand Down
Loading
Loading