Skip to content

fix(web): deduct 2.0 XLM reserve when auto-filling MAX amount (#411) - #543

Open
Mhidesav wants to merge 1 commit into
Fundable-Protocol:mainfrom
Mhidesav:fix/411-max-xlm-reserve-deduction
Open

fix(web): deduct 2.0 XLM reserve when auto-filling MAX amount (#411)#543
Mhidesav wants to merge 1 commit into
Fundable-Protocol:mainfrom
Mhidesav:fix/411-max-xlm-reserve-deduction

Conversation

@Mhidesav

@Mhidesav Mhidesav commented Jul 29, 2026

Copy link
Copy Markdown

Summary

Resolves #411 — Clicking MAX for XLM in the payment stream form left 0 XLM in the wallet, making it impossible to pay transaction fees or maintain the Stellar minimum account reserve.

Problem

When a user clicked "MAX" to auto-fill the total amount for an XLM payment stream, the entire XLM balance was populated into the amount field. This left the wallet with 0 XLM — not enough to cover:

  • Soroban transaction fees (gas + resource costs)
  • Stellar minimum account reserve (currently 1 XLM base reserve + trustline reserves)
  • Fee bumps or retry attempts if the first transaction fails

In practice, this meant users clicking MAX would see an "Insufficient XLM balance" error when trying to submit, with no clear explanation of why.

Solution

Deduct 2.0 XLM from the available balance when populating the MAX amount for XLM streams. This 2.0 XLM cushion covers:

Item Approximate Cost
Stellar minimum account reserve 1.0 XLM
Soroban transaction fees 0.01–0.5 XLM
Safety margin for fee spikes / retries ~0.5+ XLM

For non-XLM tokens (USDC, AQUA, etc.), the full token balance is used as before — since the XLM reserve concerns are handled separately by the wallet's native XLM balance.

Changes

apps/web/src/components/modules/payment-stream/PaymentStreamForm.tsx

  • Added maxBalance?: string | null and onMaxClick?: () => void optional props to StreamFormProps
  • Wrapped the "Total Amount" input in a relative container
  • Added a styled MAX button positioned at the right edge of the input, conditionally rendered when balance data is available

apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx

  • Critical fix: Cleaned up a merge conflict that had left the component body duplicated (two versions of the same component were concatenated)
  • Added XLM_RESERVE = 2.0 constant with documentation
  • Added maxAmount memoized computation:
    • For XLM: Math.max(0, balance - 2.0) formatted to 7 decimal places (Stroop precision) with trailing zeros trimmed
    • For other tokens: returns full balance unchanged
  • Added handleMaxClick callback that sets the stream amount via setStreamData
  • Plumbed maxBalance and onMaxClick props down to PaymentStreamForm

Testing & Validation

Check Result
TypeScript (tsc --noEmit) ✅ No new errors (pre-existing errors in distribution/page.tsx are unrelated)
ESLint ✅ 0 errors on changed files (1 pre-existing react-hooks/exhaustive-deps warning)
Vitest unit tests ✅ 308/310 passed (2 pre-existing failures in stellar.service.*.test.ts, unrelated)

Screenshots / Behavior

Before: Clicking MAX with 10.5 XLM balance → amount set to 10.5, wallet left with 0 XLM → transaction fails.

After: Clicking MAX with 10.5 XLM balance → amount set to 8.5, wallet keeps 2.0 XLM → transaction succeeds.

Related

  • The distribution module (use-distribution-transaction.ts) already reserves 1.0 XLM — this fix uses a more conservative 2.0 XLM for payment streams which have higher Soroban gas costs.
  • This follows the same MAX button pattern used in OfframpForm.tsx.

Summary by CodeRabbit

  • New Features

    • Added a MAX option to automatically fill the payment stream amount with the maximum available balance.
    • Improved payment stream creation with clearer balance and end-time validation.
    • Added unsaved-change tracking and more responsive fee estimates.
  • Bug Fixes

    • Improved submission handling and validation to provide more reliable payment stream creation.

…le-Protocol#411)

When clicking MAX for XLM in the payment stream form, the full XLM balance was populated, leaving 0 XLM for transaction fees and the Stellar minimum account reserve. Now deducts 2.0 XLM so users can still pay fees.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Payment stream creation

Layer / File(s) Summary
Stream setup and validation
apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx
Stream initialization, balance validation, fee estimation dependencies, and end-time submission validation are updated.
Maximum amount entry
apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx, apps/web/src/components/modules/payment-stream/PaymentStreamForm.tsx
A MAX action fills the amount from the available balance, deducting the XLM reserve for native XLM.
Submission state and confirmation
apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx
Submission uses local isSubmitting state with explicit success, error, reset, and modal-close handling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PaymentStreamForm
  participant CreatePaymentStream
  participant StellarService
  participant QueryClient
  PaymentStreamForm->>CreatePaymentStream: submit validated stream data
  CreatePaymentStream->>StellarService: createPaymentStream()
  StellarService-->>CreatePaymentStream: success or error
  CreatePaymentStream->>QueryClient: invalidate stream queries
Loading

Possibly related issues

  • Fundable-Protocol/stellar_client_os issue 311: The PR deducts the XLM reserve when calculating the maximum native XLM stream amount.

Possibly related PRs

Suggested reviewers: idrhas

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: deducting a 2.0 XLM reserve when auto-filling MAX.
Linked Issues check ✅ Passed The MAX amount now subtracts the XLM reserve for XLM streams and the summary reports no new test or type errors.
Out of Scope Changes check ✅ Passed The changes stay within the payment stream form flow and supporting refactors, with no clearly unrelated code introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx (1)

207-212: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent duplicate stream creation requests.

isSubmitting updates asynchronously, so two rapid confirmation events can both pass through and submit separate non-idempotent stream creations. Add a synchronous ref lock before closing the modal and release it in finally.

Proposed fix
-import { useEffect, useMemo, useState, useCallback } from "react";
+import { useEffect, useMemo, useState, useCallback, useRef } from "react";

+const submissionLockRef = useRef(false);

 const handleConfirmStream = async () => {
+    if (submissionLockRef.current) return;
+    submissionLockRef.current = true;
+    setIsSubmitting(true);
     setShowConfirmationModal(false);

     try {
-        setIsSubmitting(true);
         // ...
     } finally {
+        submissionLockRef.current = false;
         setIsSubmitting(false);
     }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx`
around lines 207 - 212, Update handleConfirmStream to use a synchronous
ref-based submission lock checked and set before closing the confirmation modal,
returning immediately when already locked; release the lock in the existing
finally block so failed or completed requests allow another submission.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx`:
- Around line 152-156: Return null instead of the string "0" from the XLM
maximum calculation in CreatePaymentStream when afterReserve is non-positive. In
PaymentStreamForm, render the MAX control only when the parsed maximum is a
positive value, preserving the existing behavior for valid positive limits.

---

Outside diff comments:
In `@apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx`:
- Around line 207-212: Update handleConfirmStream to use a synchronous ref-based
submission lock checked and set before closing the confirmation modal, returning
immediately when already locked; release the lock in the existing finally block
so failed or completed requests allow another submission.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 55375c79-dda6-4ea8-810b-49f7d207ff66

📥 Commits

Reviewing files that changed from the base of the PR and between 375c936 and 93c1f20.

📒 Files selected for processing (2)
  • apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx
  • apps/web/src/components/modules/payment-stream/PaymentStreamForm.tsx

Comment on lines +152 to +156
if (streamData.token === "XLM") {
const afterReserve = balanceNum - XLM_RESERVE;
if (afterReserve <= 0) return "0";
// Format to 7 decimal places (Stroop precision), trim trailing zeros
return afterReserve.toFixed(7).replace(/0+$/, "").replace(/\.$/, "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hide MAX when no XLM remains after the reserve. For 0 < balance <= 2.0, the computed value is "0", which is truthy; the form renders MAX and clicking it fills an invalid zero amount.

  • apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx#L152-L156: return null rather than "0" when afterReserve <= 0.
  • apps/web/src/components/modules/payment-stream/PaymentStreamForm.tsx#L115-L123: render MAX only for a positive parsed maximum as a defensive UI check.
📍 Affects 2 files
  • apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx#L152-L156 (this comment)
  • apps/web/src/components/modules/payment-stream/PaymentStreamForm.tsx#L115-L123
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx`
around lines 152 - 156, Return null instead of the string "0" from the XLM
maximum calculation in CreatePaymentStream when afterReserve is non-positive. In
PaymentStreamForm, render the MAX control only when the parsed maximum is a
positive value, preserving the existing behavior for valid positive limits.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web(PaymentStreamForm): deduct minimum reserve when auto-filling MAX XLM

1 participant