Skip to content

feat(api): Add a pre-flight balance check against live Horizon balances before signing a withdrawal #108

Description

@Emmyt24

Description

crates/api/src/routes/withdrawals.rs::withdraw currently reserves the idempotency key, fetches the sequence number, signs, and submits to Horizon — with no check that the master wallet actually holds enough of the requested asset to cover the withdrawal beforehand. A withdrawal that Horizon will obviously reject for insufficient balance still consumes a signing cycle and (per current behavior) permanently occupies that idempotency key with a "failed" status, meaning the caller cannot simply retry the exact same logical request with a corrected amount using the same key — they'd need a new key even though nothing about their intent changed except that the wallet needed topping up.

Requirements and Context

  • Before signing, call state.horizon().balances(&wallet.stellar_account_g) (already exists) and confirm a balance line matching the requested asset (native or the specific code+issuer) with balance >= amount (converted to the same stroops precision as the rest of the codebase — reuse octo_ingest::amount::to_stroops or a similar conversion helper rather than re-deriving decimal-to-stroops math from scratch; consider whether that helper should move to a shared location both crates/ingest and crates/api can depend on, and note your decision in the PR description).
  • On insufficient balance, return ApiError::BadRequest("insufficient balance for this withdrawal") before calling create_withdrawal (so the idempotency key is never consumed by a request that was never going to succeed) — this changes the current ordering of operations in withdraw, so re-read the whole function carefully before restructuring it.
  • Account for the base reserve (a Stellar account must always retain a minimum XLM reserve) when checking native XLM withdrawals specifically, so a withdrawal that would bring the account below its minimum reserve is also rejected here rather than by Horizon.

Suggested Execution

Branch: feat/api/withdraw-preflight-balance-check

Implement Changes

  • Add the balance check to crates/api/src/routes/withdrawals.rs::withdraw, positioned before the idempotency-key reservation.
  • If the stroops-conversion helper is shared with crates/ingest, extract it to a small shared location (or crates/wallet-core if that fits better) rather than duplicating the digit-by-digit parsing logic.

Test and Commit

  • withdraw_rejects_when_wallet_balance_is_insufficient_without_consuming_the_idempotency_key.
  • withdraw_rejects_native_withdrawal_that_would_breach_the_minimum_reserve.
  • withdraw_succeeds_when_balance_is_sufficient (regression check against the existing happy-path test).
  • Run cargo test -p octo-api locally before committing.

Example Commit Message

feat(api): pre-flight-check wallet balance before signing a withdrawal

Withdrawals with insufficient balance previously reached Horizon before
failing, consuming a signing cycle and permanently occupying the caller's
idempotency key on a request that could never succeed. Adds a pre-flight
balance (and minimum-reserve) check ahead of the idempotency-key reservation,
sharing the existing stroops-conversion logic rather than duplicating it.

Guidelines

  • This changes the order of operations in withdraw — read the whole function carefully (idempotency reservation, sequence fetch, signing, submission, status update, audit, webhook) before restructuring, and make sure the balance check genuinely happens first.
  • This is one of the tracked hard/complex issues in its fuller cross-crate form (see the corresponding hard-issue ticket) — this medium ticket covers the core API-layer check; the hard ticket covers the full design writeup and edge-case matrix.
  • Reference this issue with Closes #<issue-number> in the PR description.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26area/backendBackend crates: api, store, ingest, webhooks, bin/serverdifficulty/mediumMedium difficulty

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions