Skip to content

fix: keep preset amounts when fiat has no rate - #4651

Open
TheSeydiCharyyev wants to merge 5 commits into
ZeusLN:masterfrom
TheSeydiCharyyev:fix/fiat-sentinel-amount-input
Open

TheSeydiCharyyev wants to merge 5 commits into
ZeusLN:masterfrom
TheSeydiCharyyev:fix/fiat-sentinel-amount-input

Conversation

@TheSeydiCharyyev

@TheSeydiCharyyev TheSeydiCharyyev commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Description

Relates to issue: #4635, #3831

When fiat is the active unit and there is no rate for it:

  • a preset amount (from a BIP21 link, a noffer price, or a satAmount param) no longer reaches the amount input as the "Disabled" placeholder. The input shows the amount in sats instead, with the reason under it.
  • large amounts (jumboText) that show "N/A" now have the reason under them, for example the wallet balance and the Verify screen total. There is no auto-switch; tapping the amount still changes units. This is the approach suggested for handle N/A amounts and disable fiat currency rates while app is offline #3831.

Before this PR, getRawAmountFromSats returned getUnformattedAmount(...).amount || sats, and the placeholder is truthy, so the fallback never ran. What the user got depended on when the amount arrived (details and before screenshots in the issue):

  • Send opened with an amount: AmountInput parsed "Disabled" as 0 and passed that up, so the amount was dropped.
  • A new amount while Send was open: AmountInput did not pass anything up, so Send kept the real amount while the field showed "Disabled" and 0 sats. Proceed was on, and the Verify screen showed "≈N/A", so the amount was not visible before Slide to Pay.

Changes:

  • getUnformattedAmount: the no-currency branch now sets error too, so all three fiat failures signal it the same way. The amount placeholder stays, because Amount.tsx and existing tests depend on it.
  • getRawAmountFromSats now returns { amount, forceUnit?, error? }. When the fiat conversion fails, amount is the sats value, forceUnit is 'sats' and error has the reason. Returning the sats value alone would be wrong, because the input would read "12618" as fiat.
  • AmountInput takes a new fiatError prop and shows it under the input, in place of the conversion rate line.
  • Send, ClinkPay and LnurlPay keep the error in state and pin their amount inputs to sats (forceUnit="sats", unit toggle hidden) until the screen is closed. The pin is not removed when a rate arrives later, because AmountInput would then read the sats value as fiat. The global unit does not change.
  • LnurlPay had the same problem but was not listed in the issue: it put getUnformattedAmount(...).amount into its input directly, in stateFromProps and in recalculateDisplayAmount. The second commit also pins the input when the sats fallback is equal to the amount already shown.
  • Amount: in the error branch, a jumboText amount gets the error under it. While rates are still loading, the existing spinner is shown and no warning. Pending amounts (for example pending channel balances under the wallet balance) do not repeat the warning, because the main amount above them already shows it.

What this does not cover

  • Small amounts (not jumboText), for example the rows in the balance list, still show only "N/A".
  • FiatStore.getFiatRates still replaces rates that were already loaded with undefined when a refresh fails (from my comment in handle N/A amounts and disable fiat currency rates while app is offline #3831). With this PR the user sees why amounts show "N/A", but a failed refresh still removes working rates. That is a store change, so I left it for a separate PR.
  • LnurlPay and ClinkPay were not run on a device. On my test wallet I found no way to open LnurlPay with a satAmount while the unit is fiat without a rate, and I have no noffer. They use the same AmountInput props as Send, which was run.
  • With no fiat currency set at all, Amount now shows "N/A" instead of "Disabled", because that branch now has error. This state should not happen in practice: the default is USD, and a migration fixes an empty value. The error text reuses general.fiatRateNotAvailable, so there are no new locale strings.
  • iOS was not tested.

This pull request is categorized as a:

  • New feature
  • Bug fix
  • Code refactor
  • Configuration change
  • Locales update
  • Quality assurance
  • Other

Checklist

  • I've run yarn run tsc and made sure my code compiles correctly
  • I've run yarn run lint and made sure my code didn't contain any problematic patterns
  • I've run yarn run prettier and made sure my code is formatted correctly
  • I've run yarn run test and made sure all of the tests pass

Testing

If you modified or added a utility file, did you add new unit tests?

  • No, I'm a fool
  • Yes
  • N/A

In utils/AmountUtils.test.ts: three new tests for the three fiat failure branches of getRawAmountFromSats (no rates, no rate for the selected currency, no currency), each checking the returned sats amount, forceUnit and error, and that getSatAmount reads the amount back correctly with that unit. The five existing round-trip tests now check the new return shape, and the no-currency test of getUnformattedAmount checks error. Run against master's AmountUtils.ts, exactly these 9 tests fail. The view and component changes have no unit tests; there are no component tests for them in the repo.

I have tested this PR on the following platforms (please specify OS version and phone model/VM):

  • Android
  • iOS

Pixel 9 Pro emulator, x86_64, Android 16. To get "fiat unit, no rate" I selected IDR, which is in the currency list but has no entry in the ZEUS rates response (BGN and KWD are missing too). Send was opened with bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.00012618, then the same link with amount=0.0002 was sent while Send was open. Also checked: the keypad opened from the pinned input is in sats without the unit toggle, the wallet balance and the Verify screen show the warning under "≈N/A", and with USD (which has a rate) Send looks the same as on master. Screenshots are in comments below.

I have tested this PR with the following types of nodes (please specify node version and API version where appropriate):

On-device

  • LDK Node
  • Embedded LND

Remote

  • LND (REST)
  • LND (Lightning Node Connect)
  • Core Lightning (CLNRest)
  • Nostr Wallet Connect
  • LndHub

LDK Node on mainnet. The change is in amount handling in the views and does not depend on the backend.

Locales

  • I've added new locale text that requires translations
  • I'm aware that new translations should be made on the ZEUS Transfix page and not directly to this repo

Third Party Dependencies and Packages

  • Contributors will need to run yarn after this PR is merged in
  • 3rd party dependencies have been modified:
    • verify that package.json and yarn.lock have been properly updated
    • verify that dependencies are installed for both iOS and Android platforms

Other:

Closes #4635
Closes #3831

With fiat as the unit and no rate for it, getRawAmountFromSats returned the 'Disabled' placeholder, so preset amounts in Send, ClinkPay and LnurlPay showed 'Disabled' in the input. Opening Send with an amount dropped it to 0, and a new amount while Send was open stayed hidden behind the placeholder.

getRawAmountFromSats now returns the amount in sats with forceUnit 'sats' and the error when the fiat conversion fails. The views pin their amount inputs to sats until the screen is closed, and AmountInput shows the error under the input. The no-currency branch of getUnformattedAmount sets error too.

Closes ZeusLN#4635

@athena-zeusln athena-zeusln Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Athena Review

Pinned commit: 60a2ded

anthropic:claude-opus-5: completed openai:gpt-5.6-sol: completed

4 validated findings. This is a nonblocking COMMENT review, not an approval. No findings does not establish correctness.

Limitations:

  • Full source of utils/AmountUtils.ts getUnformattedAmount (return type and the exact conditions that set the 'Disabled'/error branches) is not included, so the nullability of amount and the meaning of each error string could not be verified.
  • components/AmountInput.tsx props interface is only partially shown; could not verify that hideUnitChangeButton exists as a prop (used in Send.tsx and LnurlPay.tsx) or that themeColor('warning') is a defined theme key.
  • No list of all callers of getRawAmountFromSats was provided; call sites outside the diff that still expect a string return value could not be checked.
  • The test file's beforeEach/setup (which would reset the mutated fiatStore.fiatRates and settingsStore.settings.fiat between cases) is not included, so cross-test state pollution from the added tests could not be confirmed or ruled out.
  • The full repository call-site search and build/test results were not supplied, so completeness of the getRawAmountFromSats return-type migration could not be verified.

Comment thread utils/AmountUtils.ts
Comment thread views/LnurlPay/LnurlPay.tsx
Comment thread components/AmountInput.tsx
Comment thread views/ClinkPay/ClinkPay.tsx
@TheSeydiCharyyev

Copy link
Copy Markdown
Contributor Author

Before and after on the Android emulator (Pixel 9 Pro, Android 16), with IDR selected so the unit is fiat with no rate. Columns: Send opened with bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.00012618, the same link with amount=0.0002 while Send is open, and the Verify screen. After: the input shows the amount in sats with the reason under it, and the unit toggle is hidden. The Verify screen still shows "≈N/A", as noted in the description.

With USD, which has a rate, the screen is the same as on master: the amount in fiat, the rate line and the unit toggle.
zeus-4635-before-after

zeus-4635-usd-unchanged

@kaloudis

Copy link
Copy Markdown
Contributor

I think when in jumboText mode and rates can't be fetched we should display a warning below those amounts too.

That would resolve #3831 imho. No auto-switch but user is given an explanation for the display and can switch as they wish.

recalculateDisplayAmount only updated state when the amount string changed. If the unit was switched to fiat without a rate while the input already showed the sats value, the sats fallback was equal to it, so the input was not pinned and later read the value as fiat.
When the unit is fiat and there is no rate, jumboText amounts such as the wallet balance and the Verify screen total showed only N/A. They now show the fiat error under the amount. Pending amounts do not repeat it, and nothing is shown while rates are still loading. Tapping the amount still switches units.

Refs ZeusLN#3831
@TheSeydiCharyyev

Copy link
Copy Markdown
Contributor Author

Added in 815d13b: large (jumboText) amounts that show "N/A" now have the error under them. Before and after on the wallet balance and the Verify screen, with IDR selected. Tapping the amount still switches units. Pending amounts under the balance do not repeat the warning.

zeus-4635-jumbo-warning

@kaloudis kaloudis 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.

Nits from another pass over the branch, all non-blocking.

Comment thread views/Send.tsx
Comment thread components/AmountInput.tsx Outdated
Comment thread views/LnurlPay/LnurlPay.tsx
Pin the additional-output amount inputs in Send the same way as the primary input. Hide the fiat conversion row in AmountInput when fiatError is shown, so it no longer shows N/A under the warning. Route the fixed-amount branch in LnurlPay through getRawAmountFromSats instead of relying on resetUnits running first.
@kaloudis
kaloudis requested a review from ajaysehwal September 18, 2026 15:01
@kaloudis

Copy link
Copy Markdown
Contributor

Round 2. All three of my round-1 nits are done, and the bot's two MEDIUMs are correctly dispatched. Code looks right to me. What is left is device coverage on the paths that changed after round 1.

Verified

The || sats.toString() rebuttal is correct. I walked all five branches of getUnformattedAmount (AmountUtils.ts:126-193) and every one returns a non-empty string: FeeUtils.toFixed yields "0" for zero, the sats branch is toString(), the fiat branch is toFixed(decimals), and the two failure branches return localized placeholders. The old fallback was dead, and it was the wrong shape besides, since a bare sats string in a fiat-unit input is exactly the #4635 failure. Dropping it is right.

The fiat-disabled rebuttal is correct. getUnformattedAmount never reads fiatEnabled. The branch that gained error is !currency at AmountUtils.ts:147, which is settings.fiat being unset, not fiat display being off. And Currency.tsx:118 resets the unit to sats when fiat is switched off while UnitsStore.getNextUnit (UnitsStore.ts:40) skips fiat while it is disabled, so effectiveUnits === 'fiat' with fiatEnabled: false is not reachable from the UI.

The ClinkPay rebuttal is correct. fiatError is only set for NofferPriceType.Fixed (ClinkPay.tsx:117), and that is exactly the case where the input is locked (ClinkPay.tsx:319), so !hideUnitChangeButton && !locked at AmountInput.tsx:254 already hides the toggle. Passing hideUnitChangeButton there would be redundant. Leaving it is fine.

Locale claim checks out. general.fiatRateNotAvailable and general.errorFetchingFiatRates both already exist in locales/en.json, so no new strings and nothing owed on Transifex.

One thing worth pulling into the description

6b03c58 fixes a second, unrelated bug on the LnurlPay fixed-amount branch, and I do not think you noticed. On master, stateFromProps destructures units at the top, then calls resetUnits() a few lines later when minSendable === maxSendable. The destructured units is a plain copy taken before the reset, so the ternary at LnurlPay.tsx:189 branches on the stale pre-reset unit while the input renders in sats.

With a working rate and fiat as the active unit, that means a fixed-amount LNURL prefilled the locked input with the fiat string, so a 12,618 sat request rendered as "6.31 sats" and getSatAmount read it back as 6 sats. Routing through getRawAmountFromSats (which reads unitsStore.units live, after the reset) removes the staleness and the ternary at once.

So my round-1 nit about the hidden coupling was not cosmetic. Please call this out in the description, since it changes what a reviewer should test and it is a user-facing prefill bug independent of #4635.

Testing: what I would like before merge

The store and util side is well covered, and the nine-test delta against master's AmountUtils.ts is the right way to show it. The gap is that everything added after round 1 is untested on a device, and two of those paths are cheap to reach:

  1. The LnurlPay pin (8fbb9ed). You do not need a satAmount param for this one. With IDR selected, open any lightning address, enter an amount, open the keypad, switch the unit to fiat there, and back out without confirming. That is precisely the case where the sats fallback equals the amount already shown, which is the case the commit exists for.

  2. The LnurlPay fixed-amount branch (6b03c58). Open a fixed-amount LNURL with fiat as the active unit, once with USD to confirm the prefill is now correct, and once with IDR for the error path.

  3. iOS. Still unchecked. This one is a layout change on the jumbo path (the new centering View wrapper in Amount.tsx), so an iOS look at the wallet balance and the Verify screen with IDR is worth having before merge, more than the logic is.

The additional-output inputs are fine to leave unverified given LDK Node has no onchain batching and the props are identical to the primary input you did run. Say that in the description rather than leaving the box empty.

Optional

getRawAmountFromSats(sats, 'sats') returning no error while the active unit is fiat with no rate is the contract that keeps the pin sticky in both Send.tsx:220 and recalculateDisplayAmount. Nothing breaks if it regresses (both call sites would just re-set the same error), so this is a pin rather than a guard, but it is a two-line test next to the three you added.

tACK on the code. Happy to merge once the two LnurlPay runs and an iOS pass are in the matrix.

@TheSeydiCharyyev

Copy link
Copy Markdown
Contributor Author

Same as on #4450: I cannot test iOS, there is no Mac or iPhone here, so that box stays empty on my side. Can someone with a Mac take that pass? The change on the jumbo path is the centering View in Amount.tsx, so it is a look at the wallet balance and at the Verify screen with a currency that has no rate.

The rest I will do: both LnurlPay runs on Android, the description note about 6b03c58 and about the additional-output inputs, and the small test for getRawAmountFromSats(sats, 'sats').

@TheSeydiCharyyev

Copy link
Copy Markdown
Contributor Author

I checked 6b03c58 before putting it in the description, and I cannot confirm the second bug. Two of your three steps hold. The outcome does not.

Holds: units is a plain copy taken at LnurlPay.tsx:139, resetUnits() runs at :150, and the ternary reads that stale copy. (I think :189 is a typo: on master :189 is the lightningAddress block, the ternary is at :176.)

Does not hold: the other arm of the ternary is unformattedAmount, and it is built by the getUnformattedAmount call at :172, which runs after the reset, not before it.

getUnformattedAmount does not take the copy. It reads the unit live: effectiveUnits = fixedUnits || unitsStore.units at AmountUtils.ts:120. resetUnits is a synchronous assignment, () => (this.units = 'sats') at UnitsStore.ts:55. And it is one object: Stores.ts:63 builds a single unitsStore, AmountUtils.ts:3 imports that one, App.tsx:445 passes that one into the provider as UnitsStore.

So on that branch the live unit is already sats by the time the string is built, and the ternary picks between "12618" and "12618".

I ran it as well, against master's AmountUtils, with a working rate of 50,000 USD/BTC and fiat as the active unit, calling in the same order as stateFromProps:

staleCopy=fiat liveUnit=sats fiatBranchWouldBe=6.31 unformatted=12618 otherArm=12618 picked=12618 readBack=12618

6.31 is real. It is what getUnformattedAmount returns for 12,618 sats while the unit is fiat. But the call has to run before the reset to return it, and on this path it runs after.

The same holds after 6b03c58. getRawAmountFromSats(minSendableSats) cannot return an error there either, because the unit is already sats when it is called: branch6b03c58=12618 branchError=undefined. The commit drops a dead choice, it does not change what the input receives.

Do you see a path where the two arms differ? The only way I can get the fiat string into the prefill is if resetUnits() does not reach the store that AmountUtils reads, and I could not build that from the UI.

If you still read it as a user-facing fix, name the case and I will run it on the device before I write it into the description.

The rest of round 2 is unchanged and in progress: both LnurlPay runs on Android, the additional-output note, and the getRawAmountFromSats(sats, 'sats') test.

@TheSeydiCharyyev

TheSeydiCharyyev commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Both LnurlPay runs are done on Android, and I ran the same cases on master as a control, so the before and after sit side by side. Pixel 9 Pro emulator, API 36, debug build, an LND (REST) stub wallet. I checked which code was on the device each time: the branch bundle has fiatError 40 times, the master bundle 0 times. The screenshots are cropped to the amount area.

1. The fixed-amount branch. I cannot reproduce the second bug.

master 7838154, USD active, the rate row reading 1,242 sats = 1 USD, fixed-amount LNURL for 12,618 sats: the locked input reads 12,618 sats. The ≈$10.16 under it is the conversion row, not the prefill.

This branch, same case: 12,618 sats as well. It is the same screen down to the pixel. I compared the two screenshots pixel by pixel, and the only region that differs is (143, 61) - (1148, 93), which is the status bar clock. Everything below it is identical, so one screenshot stands for both.

crop-run2-usd-master

That is what the trace said. The getUnformattedAmount call at :172 runs after resetUnits(), so both arms of the ternary are the sats string. resetUnits() is visible from the outside too: after leaving that screen the unit on the main screen is sats.

So 6b03c58 drops a dead choice there. I would rather not write a user-facing fix into the description that I cannot reproduce on master. If you know a path that reaches it, name it and I will run that one.

2. The pin. Here the two differ a lot.

IDR selected, unit sats, 12,618 entered and confirmed on a variable-amount LNURL, then the keypad, unit switched to IDR there, back out without confirming.

master: the input reads RpDisabled, the lines under it read N/A 0 BTC/IDR, ₿0 and 0 sats, and CONFIRM is enabled. A 0 sat payment can be confirmed for a 12,618 sat request.

crop-run1-master

This branch: the input stays 12,618 sats, with Rate for selected currency not available under it, and the fiat conversion row is gone.

crop-run1-pin-branch

3. The additional outputs are tested now, not assumed.

I was wrong that I could not reach them. The stand wallet is LND (REST), and LND.ts:1042 has supportsOnchainBatching = () => true, so the button is there. It is LDK Node that returns false.

With IDR and no rate, an on-chain Send with one added output: both Amount inputs are in sats, both carry Rate for selected currency not available, and neither has the unit change button. The added output behaves like the primary one.

crop-addoutput-branch

4. One thing that is not from this PR.

On the fixed-amount screen with a currency that has no rate, the lines under the input read Infinity sats = 1 IDR and ≈N/A, and no warning is shown. The warning cannot appear there, because getRawAmountFromSats cannot return an error on that branch: the unit is already sats when it is called.

master prints the same Infinity sats = 1 IDR. That screen is pixel identical on the two builds as well, again apart from the clock, so this is not a regression from this PR. Separate issue, or fold it in here?

crop-run2-idr-master

5. iOS.

Still not possible here, no Mac and no iPhone.

I will add the getRawAmountFromSats(sats, 'sats') test, and update the description once you answer on 1 and 4.

Send.tsx:220 and the LnurlPay focus handler re-derive the amount with fixedUnits sats while the active unit is still fiat without a rate. Both rely on that call returning no error. The existing fixedUnits test only covers a working rate.
@github-actions

Copy link
Copy Markdown

Test coverage

Metric Coverage Covered/Total vs master
Statements 42.61% 6010/14104 +0.01%
Branches 39.23% 3499/8917 0.00%
Functions 31.5% 793/2517 0.00%
Lines 43.05% 5752/13361 +0.01%

Commit: a9c1321 | Test run | Baseline: master@dd12ab334

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.

@TheSeydiCharyyev, I think the new fiatError = error assignment in the fixed-amount branch is effectively dead code.

resetUnits() runs unconditionally immediately before this branch and forces unitsStore.units to 'sats'. Since getRawAmountFromSats(minSendableSats) is called without a fixedUnits override, it always takes the sats path, so getUnformattedAmount returns without an error and error is always undefined.

This also means the change doesn’t actually achieve the goal described in 6b03c58c1 (“Route the fixed-amount branch in LnurlPay through getRawAmountFromSats instead of relying on resetUnits running first”) — resetUnits is still what determines the result.

It’s harmless today because locked is independently forced for fixed amounts, but worth revisiting since the new error handling won’t protect the input if that separate logic changes later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right. That fiatError line is dead.

resetUnits() runs just above it, on the same condition. getUnformattedAmount reads the unit live at AmountUtils.ts:120. So the call always goes to the sats path and never returns an error.

I said the same in my comment above. You can also see it on a device: with IDR the screen shows Infinity sats = 1 IDR and ≈N/A, and no warning at all. That is the dead line.

You are right about the commit message too, and I did not see this. 6b03c58 says the branch does not rely on resetUnits() any more. It still does. The call has no fixedUnits, so the live unit decides.

One fix to "harmless today". locked helps, but the main reason is another one: the payment does not read the string on the screen. stateFromProps puts minSendableSats into satAmount (LnurlPay.tsx:186). recalculateDisplayAmount writes only amount. CONFIRM sends this.state.satAmount (:695). So a wrong unit here is a display bug, not a wrong payment.

Two lines would do what the commit message says:

  • getRawAmountFromSats(minSendableSats, 'sats') for the string
  • forceUnit set to sats for the fixed amount case, not only when fiatError is set

forceUnit goes into effectiveUnits at AmountInput.tsx:189. It decides what you see and how the string is read back to sats. After that the screen does not need resetUnits(). Nothing changes today, because resetUnits() already sets sats. The dead line goes away too.

This came from a round 1 review nit, so I do not want to grow the PR on my own. If you guys say it belongs here, I push it and run the two fixed amount cases on the device again. If not, I open a new PR.

@@ -307,6 +315,8 @@ export default class ClinkPay extends React.Component<
</Text>
<AmountInput

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.

nit: missing hideUnitChangeButton={!!fiatError}, which the sibling Send.tsx / LnurlPay.tsx call sites both pass.

It’s currently masked because fiatError is only reachable when locked is already true (Fixed-price offers), but it’s an inconsistency that could surface if that coupling changes later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked it. You are right about the facts.

ClinkPay.tsx is the only call site without it. Send.tsx passes it in four places, LnurlPay.tsx in one.

It changes nothing today. fiatError is set only inside priceType === Fixed && price (ClinkPay.tsx:117-123), and locked is loading || priceType === Fixed (:329-333). The guard in the component is !hideUnitChangeButton && !locked (AmountInput.tsx:258), so locked alone already hides the button.

One thing you may have missed: this line was already looked at in round 2. The bot raised it, I said it was not a bug, and Evan checked it and wrote "Passing hideUnitChangeButton there would be redundant. Leaving it is fine". It is here: #4651 (comment)

Your point is not the same as the bot's. The bot said the button shows up. You say the three call sites do not match. That part is true, and it is the same shape as your other note: the code leans on a coupling instead of saying what it wants.

I am fine either way. If you guys want them to match, I add the one line in the same commit as the forceUnit change from the other thread. No behaviour change, only the same props everywhere.

@kaloudis

Copy link
Copy Markdown
Contributor

Round 3. You are right about 6b03c58, and I was wrong. getUnformattedAmount at LnurlPay.tsx:172 runs after resetUnits() and reads unitsStore.units live, so both arms of the ternary are the sats string. The stale copy only chooses between two identical values. Your master control run settles it. There is no user-facing fix to write into the description: 6b03c58 removes a dead choice, nothing more. Thanks for checking instead of taking my word for it.

On the runs:

  1. Fixed-amount branch. USD matches master, as the trace predicts. Good.
  2. The pin. This is the real bug. On master, a 12,618 sat request can be confirmed as 0 sats after backing out of a fiat switch in the keypad. That belongs in the description as the concrete before/after for 8fbb9ed.
  3. Additional outputs. Thanks for actually running them on LND REST. Better than the assumption I suggested.
  4. Infinity sats = 1 IDR. Separate issue, please. It is FiatStore.getRate falling back to a 0 rate and dividing by it, a store change like the refresh one you already scoped out. Happy to see a small follow-up PR for it.

a9c1321 looks right. beforeEach resets the stores, it covers both the missing-rate and no-rates branches, and toEqual catches any error creeping in. Suite passes locally.

Remaining before merge: update the description (the LnurlPay runs, the additional-output run, drop the 6b03c58 claim, link the Infinity issue). I will do the iOS pass on the Amount.tsx jumbo path myself, since you have no Mac.

tACK a9c1321 on Android runs + code. Will upgrade to ACK after iOS.

@kaloudis
kaloudis requested a review from ajaysehwal September 22, 2026 19:23

This branch has not been deployed

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

Projects

None yet

3 participants