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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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