Skip to content

fix(send): populate amount field for fixed-amount LNURL - #671

Merged
Kukks merged 1 commit into
masterfrom
fix/lnurl-fixed-amount-blank-field
Jun 14, 2026
Merged

Kukks merged 1 commit into
masterfrom
fix/lnurl-fixed-amount-blank-field

Conversation

@pietro909

@pietro909 pietro909 commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When an LNURL-pay endpoint resolves to a fixed amount (minSendable === maxSendable), the Send form locked the amount input but left it blank. The user saw an empty, read-only field and any attempt to continue produced an unpayable invoice.

Cause

The fixed amount was applied via setSendInfo / setAmount, neither of which syncs amountTextValue — the state the amount <input> is actually bound to. So the value never reached the field.

Fix

Route the auto-filled fixed amount through setState({ ...sendInfo, satoshis: min }), which updates amountTextValue and makes the locked field display the correct value. Removed the dead setAmount(min) call in the read-only branch.

Test

Adds a regression test that mocks a fixed-amount LNURL response (minSendable === maxSendable === 21000 msat → 21 sats) and asserts the amount input is populated with 21 and read-only.

  • pnpm test — 268 passed / 4 skipped
  • pnpm lint, pnpm format:check — clean

Summary by CodeRabbit

  • Bug Fixes

    • Fixed LNURL fixed-amount handling to ensure the send amount input properly syncs with and displays the fixed satoshi amount specified by the payment provider, maintaining consistency between the UI display and internal state, with the field marked as read-only when applicable.
  • Tests

    • Added regression test to verify correct behavior and display of the send amount input when LNURL payments specify a fixed amount.

When an LNURL-pay endpoint resolves to a fixed amount (minSendable ===
maxSendable), route the auto-filled value through setState so the amount
input (bound to amountTextValue) reflects it. Previously the field was
set via setSendInfo/setAmount, which never synced the text value and left
the locked field blank, producing an unpayable invoice.

Adds a regression test covering a fixed-amount LNURL (21000 msat -> 21
sats) that asserts the read-only amount input is populated.
@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR fixes LNURL fixed-amount handling in SendForm to keep the amount input field synchronized with internal state. When LNURL resolves with equal minSendable and maxSendable, the component now updates via setState helper instead of local state. A regression test validates this behavior by mocking the LNURL response and asserting the input displays the fixed amount and is readonly.

Changes

LNURL Fixed-Amount Synchronization

Layer / File(s) Summary
LNURL fixed-amount state synchronization
src/screens/Wallet/Send/Form.tsx
Removed local setAmount(min) call and replaced with setState helper to ensure the UI amount field reflects the fixed satoshi value resolved by LNURL when min equals max.
Regression test for fixed-amount synchronization
src/test/screens/wallet/send.test.tsx
Added vitest and fetch-mock imports, then created an async test case that mocks an LNURL response with fixed amount (minSendable === maxSendable = 21), verifies the amount input displays 21, is readonly, and has the field name send-amount.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • arkade-os/wallet#639: Modifies LNURL fixed-amount and min-max handling in the same SendForm component paths.
  • arkade-os/wallet#643: Updates LNURL conditions effect logic in SendForm, overlapping with the same fixed-amount flow.

Suggested reviewers

  • arkanaai
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing the populated amount field for fixed-amount LNURL endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lnurl-fixed-amount-blank-field

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: 613b960
Status: ✅  Deploy successful!
Preview URL: https://e702eaaa.wallet-bitcoin.pages.dev
Branch Preview URL: https://fix-lnurl-fixed-amount-blank.wallet-bitcoin.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying tmp-boltz-upstream-mainnet-arkade-wallet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 613b960
Status: ✅  Deploy successful!
Preview URL: https://aed950ab.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev
Branch Preview URL: https://fix-lnurl-fixed-amount-blank.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 613b960
Status: ✅  Deploy successful!
Preview URL: https://2341e11b.arkade-wallet.pages.dev
Branch Preview URL: https://fix-lnurl-fixed-amount-blank.arkade-wallet.pages.dev

View logs

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

Review: fix(send): populate amount field for fixed-amount LNURL

Verdict: APPROVE ✅

Analysis

Root cause is correctly identified. Two effects manage fixed-amount LNURL state:

  1. "check lnurl conditions" (Form.tsx ~L410): fetches LNURL pay endpoint, and if min === max, sets sendInfo.satoshis
  2. "check lnurl limits" (Form.tsx ~L393): fires when lnUrlResponse changes, sets amountIsReadOnly

The old code called setSendInfo() directly, which updates context state but never touches amountTextValue — the string bound to the <input>. Only the local setState() helper (L170–183) syncs both. Classic React "two sources of truth" bug.

The fix routes through setState() instead of setSendInfo(). This is correct — setState calls setSendInfo internally AND sets amountTextValue via the info.satoshis branch (L179).

What I checked

  • setAmount(min) removal (L402): Safe. The amount state is only read at L675 for keyboard focus heuristics (handleEnter). For fixed-amount LNURL the field is readonly so this path is irrelevant.
  • setState side effects: setState also calls setScan(false) (L171). Harmless — scan is already false when LNURL conditions resolve.
  • Stale closure on sendInfo: The .then() callback at L419 captures sendInfo from render time. If the user mutates state while the fetch is in-flight, the spread could overwrite changes. This is a pre-existing issue — the old setSendInfo({...sendInfo, satoshis: min}) had the identical closure problem. Not introduced by this PR.
  • Zero-amount edge case: If an LNURL returns minSendable === maxSendable === 0, setState won't update amountTextValue because if (info.satoshis) is falsy for 0. Pre-existing, and 0-sat LNURL is invalid per spec (LUD-06 requires minSendable >= 1).
  • Effect ordering: Conditions effect calls setState then setLnUrlResponse. React batches these. The limits effect fires next render, sees min === max, sets amountIsReadOnly(true). Correct sequence.
  • No protocol-critical paths touched: This is pure UI state management — no VTXO handling, signing, forfeit, or exit paths.

Test

The regression test is well-constructed:

  • Mocks fetch with a fixed-amount LNURL response (minSendable === maxSendable === 21000 msat)
  • Pre-populates sendInfo.lnUrl and sendInfo.recipient to trigger the conditions effect
  • Asserts getByDisplayValue('21') + readonly attribute
  • Properly cleans up with fetchMocker.disableMocks()

Clean fix. No issues found.


🤖 Reviewed by Arkana

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

Review: fix(send): populate amount field for fixed-amount LNURL

Verdict: APPROVE ✅

Analysis

Root cause is correctly identified. Two effects manage fixed-amount LNURL state:

  1. "check lnurl conditions" (Form.tsx ~L410): fetches LNURL pay endpoint, and if min === max, sets sendInfo.satoshis
  2. "check lnurl limits" (Form.tsx ~L393): fires when lnUrlResponse changes, sets amountIsReadOnly

The old code called setSendInfo() directly, which updates context state but never touches amountTextValue — the string bound to the <input>. Only the local setState() helper (L170–183) syncs both. Classic React "two sources of truth" bug.

The fix routes through setState() instead of setSendInfo(). This is correct — setState calls setSendInfo internally AND sets amountTextValue via the info.satoshis branch (L179).

What I checked

  • setAmount(min) removal (L402): Safe. The amount state is only read at L675 for keyboard focus heuristics (handleEnter). For fixed-amount LNURL the field is readonly so this path is irrelevant.
  • setState side effects: setState also calls setScan(false) (L171). Harmless — scan is already false when LNURL conditions resolve.
  • Stale closure on sendInfo: The .then() callback at L419 captures sendInfo from render time. If the user mutates state while the fetch is in-flight, the spread could overwrite changes. This is a pre-existing issue — the old setSendInfo({...sendInfo, satoshis: min}) had the identical closure problem. Not introduced by this PR.
  • Zero-amount edge case: If an LNURL returns minSendable === maxSendable === 0, setState won't update amountTextValue because if (info.satoshis) is falsy for 0. Pre-existing, and 0-sat LNURL is invalid per spec (LUD-06 requires minSendable >= 1).
  • Effect ordering: Conditions effect calls setState then setLnUrlResponse. React batches these. The limits effect fires next render, sees min === max, sets amountIsReadOnly(true). Correct sequence.
  • No protocol-critical paths touched: This is pure UI state management — no VTXO handling, signing, forfeit, or exit paths.

Test

The regression test is well-constructed:

  • Mocks fetch with a fixed-amount LNURL response (minSendable === maxSendable === 21000 msat)
  • Pre-populates sendInfo.lnUrl and sendInfo.recipient to trigger the conditions effect
  • Asserts getByDisplayValue('21') + readonly attribute
  • Properly cleans up with fetchMocker.disableMocks()

Clean fix. No issues found.


🤖 Reviewed by Arkana

@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)
src/screens/Wallet/Send/Form.tsx (1)

366-380: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard async LNURL resolution before applying fixed-amount state.

This effect can apply a stale sendInfo snapshot if an earlier LNURL request resolves late, and Line 373 then overwrites newer form state via setState({ ...sendInfo, satoshis: min }).

Suggested fix
   useEffect(() => {
     if (!sendInfo.lnUrl) return
     if (sendInfo.arkAddress) return
     if (sendInfo.lnUrl && sendInfo.invoice) return
-    checkLnUrlConditions(sendInfo.lnUrl)
+    let cancelled = false
+    const requestedLnUrl = sendInfo.lnUrl
+    checkLnUrlConditions(requestedLnUrl)
       .then((conditions) => {
+        if (cancelled) return
         if (!conditions) return setError('Unable to fetch LNURL conditions')
         const min = Math.floor(conditions.minSendable / 1000) // from millisatoshis to satoshis
         const max = Math.floor(conditions.maxSendable / 1000) // from millisatoshis to satoshis
         // when the LNURL resolves to a fixed amount, set it via setState so the
         // amount input (bound to amountTextValue) reflects it instead of staying blank
         if (min === max) setState({ ...sendInfo, satoshis: min })
         return setLnUrlResponse({ ...conditions, minSendable: min, maxSendable: max })
       })
       .catch((e) => {
+        if (cancelled) return
         consoleError(e, 'Error checking LNURL conditions')
         setError(extractError(e))
       })
-  }, [sendInfo.arkAddress, sendInfo.lnUrl])
+    return () => {
+      cancelled = true
+    }
+  }, [sendInfo.arkAddress, sendInfo.lnUrl, sendInfo.invoice])
🤖 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 `@src/screens/Wallet/Send/Form.tsx` around lines 366 - 380, The effect may
apply a stale sendInfo when an earlier checkLnUrlConditions resolves; capture
the current lnUrl into a local constant before calling checkLnUrlConditions
(e.g., const requestedLnUrl = sendInfo.lnUrl) and in the .then() handler verify
that the component's current lnUrl still equals requestedLnUrl before calling
setState({ ...sendInfo, satoshis: min }) or setLnUrlResponse; additionally,
replace setState({ ...sendInfo, satoshis: min }) with the functional updater
form setState(prev => ({ ...prev, satoshis: min })) to avoid using a stale
sendInfo snapshot if you prefer not to compare strings.
🤖 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 `@src/test/screens/wallet/send.test.tsx`:
- Around line 86-134: The test sets up fetch mocks with createFetchMock and
calls fetchMocker.enableMocks()/mockResponseOnce but calls
fetchMocker.disableMocks() only at the end, which can leak if an assertion
throws; wrap the portion between fetchMocker.enableMocks() and
fetchMocker.disableMocks() in a try/finally so fetchMocker.disableMocks() always
runs (i.e., call fetchMocker.enableMocks(), then try { ...render and assertions
involving SendForm and await waitFor(...) } finally { fetchMocker.disableMocks()
}) to ensure mocks are restored even on failure.

---

Outside diff comments:
In `@src/screens/Wallet/Send/Form.tsx`:
- Around line 366-380: The effect may apply a stale sendInfo when an earlier
checkLnUrlConditions resolves; capture the current lnUrl into a local constant
before calling checkLnUrlConditions (e.g., const requestedLnUrl =
sendInfo.lnUrl) and in the .then() handler verify that the component's current
lnUrl still equals requestedLnUrl before calling setState({ ...sendInfo,
satoshis: min }) or setLnUrlResponse; additionally, replace setState({
...sendInfo, satoshis: min }) with the functional updater form setState(prev =>
({ ...prev, satoshis: min })) to avoid using a stale sendInfo snapshot if you
prefer not to compare strings.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d144435-be5b-4a50-b79d-01981ee517a9

📥 Commits

Reviewing files that changed from the base of the PR and between 13f2652 and 613b960.

📒 Files selected for processing (2)
  • src/screens/Wallet/Send/Form.tsx
  • src/test/screens/wallet/send.test.tsx

Comment on lines +86 to +134
const fetchMocker = createFetchMock(vi)
fetchMocker.enableMocks()
fetchMocker.mockResponseOnce(
JSON.stringify({
callback: 'https://pay.staging.galoy.io/.well-known/lnurlp/testing',
minSendable: 21000, // millisatoshis -> 21 sats
maxSendable: 21000,
metadata: 'mock-metadata',
}),
)
const lnUrl = 'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7ar9wd6xjmn8h9qlv7'
const flowValue = { ...mockFlowContextValue, sendInfo: { ...emptySendInfo, lnUrl, recipient: lnUrl } }
const walletValue = {
...mockWalletContextValue,
balance: 1_000_000,
svcWallet: {
...mockSvcWallet,
getAddress: () => 'tark1mockoffchain',
getBoardingAddress: () => Promise.resolve('bcrt1mockboarding'),
getBalance: () => Promise.resolve({ available: 1_000_000 }),
} as any,
}
render(
<NavigationContext.Provider value={mockNavigationContextValue}>
<AspContext.Provider value={mockAspContextValue}>
<ConfigContext.Provider value={mockConfigContextValue as any}>
<FiatContext.Provider value={mockFiatContextValue as any}>
<SwapsContext.Provider value={mockSwapsContextValue as any}>
<OptionsContext.Provider value={mockOptionsContextValue as any}>
<FlowContext.Provider value={flowValue as any}>
<WalletContext.Provider value={walletValue}>
<LimitsContext.Provider value={mockLimitsContextValue}>
<SendForm />
</LimitsContext.Provider>
</WalletContext.Provider>
</FlowContext.Provider>
</OptionsContext.Provider>
</SwapsContext.Provider>
</FiatContext.Provider>
</ConfigContext.Provider>
</AspContext.Provider>
</NavigationContext.Provider>,
)
// amount input is bound to amountTextValue; before the fix it stayed empty
const amountInput = await waitFor(() => screen.getByDisplayValue('21'))
expect(amountInput).toHaveAttribute('name', 'send-amount')
expect(amountInput).toHaveAttribute('readonly')
fetchMocker.disableMocks()
})

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Always restore fetch mocks with try/finally in this test.

If an assertion throws before Line 133, mocked fetch can leak into later tests and create cascading failures.

Suggested fix
     const fetchMocker = createFetchMock(vi)
     fetchMocker.enableMocks()
-    fetchMocker.mockResponseOnce(
-      JSON.stringify({
-        callback: 'https://pay.staging.galoy.io/.well-known/lnurlp/testing',
-        minSendable: 21000, // millisatoshis -> 21 sats
-        maxSendable: 21000,
-        metadata: 'mock-metadata',
-      }),
-    )
-    const lnUrl = 'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7ar9wd6xjmn8h9qlv7'
-    const flowValue = { ...mockFlowContextValue, sendInfo: { ...emptySendInfo, lnUrl, recipient: lnUrl } }
-    const walletValue = {
-      ...mockWalletContextValue,
-      balance: 1_000_000,
-      svcWallet: {
-        ...mockSvcWallet,
-        getAddress: () => 'tark1mockoffchain',
-        getBoardingAddress: () => Promise.resolve('bcrt1mockboarding'),
-        getBalance: () => Promise.resolve({ available: 1_000_000 }),
-      } as any,
-    }
-    render(
-      <NavigationContext.Provider value={mockNavigationContextValue}>
-        <AspContext.Provider value={mockAspContextValue}>
-          <ConfigContext.Provider value={mockConfigContextValue as any}>
-            <FiatContext.Provider value={mockFiatContextValue as any}>
-              <SwapsContext.Provider value={mockSwapsContextValue as any}>
-                <OptionsContext.Provider value={mockOptionsContextValue as any}>
-                  <FlowContext.Provider value={flowValue as any}>
-                    <WalletContext.Provider value={walletValue}>
-                      <LimitsContext.Provider value={mockLimitsContextValue}>
-                        <SendForm />
-                      </LimitsContext.Provider>
-                    </WalletContext.Provider>
-                  </FlowContext.Provider>
-                </OptionsContext.Provider>
-              </SwapsContext.Provider>
-            </FiatContext.Provider>
-          </ConfigContext.Provider>
-        </AspContext.Provider>
-      </NavigationContext.Provider>,
-    )
-    // amount input is bound to amountTextValue; before the fix it stayed empty
-    const amountInput = await waitFor(() => screen.getByDisplayValue('21'))
-    expect(amountInput).toHaveAttribute('name', 'send-amount')
-    expect(amountInput).toHaveAttribute('readonly')
-    fetchMocker.disableMocks()
+    try {
+      fetchMocker.mockResponseOnce(
+        JSON.stringify({
+          callback: 'https://pay.staging.galoy.io/.well-known/lnurlp/testing',
+          minSendable: 21000, // millisatoshis -> 21 sats
+          maxSendable: 21000,
+          metadata: 'mock-metadata',
+        }),
+      )
+      const lnUrl =
+        'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7ar9wd6xjmn8h9qlv7'
+      const flowValue = { ...mockFlowContextValue, sendInfo: { ...emptySendInfo, lnUrl, recipient: lnUrl } }
+      const walletValue = {
+        ...mockWalletContextValue,
+        balance: 1_000_000,
+        svcWallet: {
+          ...mockSvcWallet,
+          getAddress: () => 'tark1mockoffchain',
+          getBoardingAddress: () => Promise.resolve('bcrt1mockboarding'),
+          getBalance: () => Promise.resolve({ available: 1_000_000 }),
+        } as any,
+      }
+      render(
+        <NavigationContext.Provider value={mockNavigationContextValue}>
+          <AspContext.Provider value={mockAspContextValue}>
+            <ConfigContext.Provider value={mockConfigContextValue as any}>
+              <FiatContext.Provider value={mockFiatContextValue as any}>
+                <SwapsContext.Provider value={mockSwapsContextValue as any}>
+                  <OptionsContext.Provider value={mockOptionsContextValue as any}>
+                    <FlowContext.Provider value={flowValue as any}>
+                      <WalletContext.Provider value={walletValue}>
+                        <LimitsContext.Provider value={mockLimitsContextValue}>
+                          <SendForm />
+                        </LimitsContext.Provider>
+                      </WalletContext.Provider>
+                    </FlowContext.Provider>
+                  </OptionsContext.Provider>
+                </SwapsContext.Provider>
+              </FiatContext.Provider>
+            </ConfigContext.Provider>
+          </AspContext.Provider>
+        </NavigationContext.Provider>,
+      )
+      const amountInput = await waitFor(() => screen.getByDisplayValue('21'))
+      expect(amountInput).toHaveAttribute('name', 'send-amount')
+      expect(amountInput).toHaveAttribute('readonly')
+    } finally {
+      fetchMocker.disableMocks()
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchMocker = createFetchMock(vi)
fetchMocker.enableMocks()
fetchMocker.mockResponseOnce(
JSON.stringify({
callback: 'https://pay.staging.galoy.io/.well-known/lnurlp/testing',
minSendable: 21000, // millisatoshis -> 21 sats
maxSendable: 21000,
metadata: 'mock-metadata',
}),
)
const lnUrl = 'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7ar9wd6xjmn8h9qlv7'
const flowValue = { ...mockFlowContextValue, sendInfo: { ...emptySendInfo, lnUrl, recipient: lnUrl } }
const walletValue = {
...mockWalletContextValue,
balance: 1_000_000,
svcWallet: {
...mockSvcWallet,
getAddress: () => 'tark1mockoffchain',
getBoardingAddress: () => Promise.resolve('bcrt1mockboarding'),
getBalance: () => Promise.resolve({ available: 1_000_000 }),
} as any,
}
render(
<NavigationContext.Provider value={mockNavigationContextValue}>
<AspContext.Provider value={mockAspContextValue}>
<ConfigContext.Provider value={mockConfigContextValue as any}>
<FiatContext.Provider value={mockFiatContextValue as any}>
<SwapsContext.Provider value={mockSwapsContextValue as any}>
<OptionsContext.Provider value={mockOptionsContextValue as any}>
<FlowContext.Provider value={flowValue as any}>
<WalletContext.Provider value={walletValue}>
<LimitsContext.Provider value={mockLimitsContextValue}>
<SendForm />
</LimitsContext.Provider>
</WalletContext.Provider>
</FlowContext.Provider>
</OptionsContext.Provider>
</SwapsContext.Provider>
</FiatContext.Provider>
</ConfigContext.Provider>
</AspContext.Provider>
</NavigationContext.Provider>,
)
// amount input is bound to amountTextValue; before the fix it stayed empty
const amountInput = await waitFor(() => screen.getByDisplayValue('21'))
expect(amountInput).toHaveAttribute('name', 'send-amount')
expect(amountInput).toHaveAttribute('readonly')
fetchMocker.disableMocks()
})
const fetchMocker = createFetchMock(vi)
fetchMocker.enableMocks()
try {
fetchMocker.mockResponseOnce(
JSON.stringify({
callback: 'https://pay.staging.galoy.io/.well-known/lnurlp/testing',
minSendable: 21000, // millisatoshis -> 21 sats
maxSendable: 21000,
metadata: 'mock-metadata',
}),
)
const lnUrl =
'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7ar9wd6xjmn8h9qlv7'
const flowValue = { ...mockFlowContextValue, sendInfo: { ...emptySendInfo, lnUrl, recipient: lnUrl } }
const walletValue = {
...mockWalletContextValue,
balance: 1_000_000,
svcWallet: {
...mockSvcWallet,
getAddress: () => 'tark1mockoffchain',
getBoardingAddress: () => Promise.resolve('bcrt1mockboarding'),
getBalance: () => Promise.resolve({ available: 1_000_000 }),
} as any,
}
render(
<NavigationContext.Provider value={mockNavigationContextValue}>
<AspContext.Provider value={mockAspContextValue}>
<ConfigContext.Provider value={mockConfigContextValue as any}>
<FiatContext.Provider value={mockFiatContextValue as any}>
<SwapsContext.Provider value={mockSwapsContextValue as any}>
<OptionsContext.Provider value={mockOptionsContextValue as any}>
<FlowContext.Provider value={flowValue as any}>
<WalletContext.Provider value={walletValue}>
<LimitsContext.Provider value={mockLimitsContextValue}>
<SendForm />
</LimitsContext.Provider>
</WalletContext.Provider>
</FlowContext.Provider>
</OptionsContext.Provider>
</SwapsContext.Provider>
</FiatContext.Provider>
</ConfigContext.Provider>
</AspContext.Provider>
</NavigationContext.Provider>,
)
// amount input is bound to amountTextValue; before the fix it stayed empty
const amountInput = await waitFor(() => screen.getByDisplayValue('21'))
expect(amountInput).toHaveAttribute('name', 'send-amount')
expect(amountInput).toHaveAttribute('readonly')
} finally {
fetchMocker.disableMocks()
}
🤖 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 `@src/test/screens/wallet/send.test.tsx` around lines 86 - 134, The test sets
up fetch mocks with createFetchMock and calls
fetchMocker.enableMocks()/mockResponseOnce but calls fetchMocker.disableMocks()
only at the end, which can leak if an assertion throws; wrap the portion between
fetchMocker.enableMocks() and fetchMocker.disableMocks() in a try/finally so
fetchMocker.disableMocks() always runs (i.e., call fetchMocker.enableMocks(),
then try { ...render and assertions involving SendForm and await waitFor(...) }
finally { fetchMocker.disableMocks() }) to ensure mocks are restored even on
failure.

@Kukks
Kukks merged commit cd5931e into master Jun 14, 2026
6 checks passed
@Kukks
Kukks deleted the fix/lnurl-fixed-amount-blank-field branch June 14, 2026 15:30
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.

2 participants