Skip to content

V8 - #142

Closed
bordalix wants to merge 264 commits into
masterfrom
v8
Closed

V8#142
bordalix wants to merge 264 commits into
masterfrom
v8

Conversation

@bordalix

@bordalix bordalix commented Aug 14, 2025 •

Copy link
Copy Markdown
Collaborator

Several PR merged here for tests:

Summary by CodeRabbit

  • New Features

    • Boltz/LN integration: in-app Boltz app, swap history/details, Lightning send/receive flows, Boltz settings toggle.
    • Password management: new Password screen with biometrics, nudge prompts, and default-password fallback.
    • Scanner and InputAmount updated: scanner exposes onData/onError and mode switch; InputAmount supports “Max”.
  • Improvements

    • Riga-specific alerts/gating, streamlined transaction rows, updated empty/state/status icons, subtle pulse animation, provider nesting adjusted, limits-based UI gating.
  • Tests

    • Extensive unit/UI tests added; Vitest UI/coverage scripts and test setup.
  • Chores

    • Dependency/tooling overhaul, DOMPurify & QR reader added, TypeScript moved to devDeps, Node engine bumped to >=20.

bordalix and others added 7 commits August 28, 2025 17:25
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>

@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: 5

Caution

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

⚠️ Outside diff range comments (2)
src/screens/Wallet/Receive/QrCode.tsx (2)

58-87: Prevent setState after unmount; add missing deps; use functional updates.

Guard async chain and include dependencies to avoid stale closures.

-useEffect(() => {
+useEffect(() => {
+  let cancelled = false
   // if boltz is available and amount is between limits, let's create a swap invoice
-  if (validLnSwap(satoshis) && wallet && svcWallet) {
-    swapProvider
+  if (validLnSwap(satoshis) && wallet && svcWallet && swapProvider) {
+    swapProvider
       ?.createReverseSwap(satoshis)
       .then((pendingSwap) => {
         if (!pendingSwap) throw new Error('Failed to create reverse swap')
         const invoice = pendingSwap.response.invoice
-        setRecvInfo({ ...recvInfo, invoice })
+        if (cancelled) return
+        setRecvInfo((prev) => ({ ...prev, invoice }))
         setInvoice(invoice)
         consoleLog('Reverse swap invoice created:', invoice)
-        swapProvider
+        return swapProvider
           .waitAndClaim(pendingSwap)
-          .then(() => {
-            setRecvInfo({ ...recvInfo, satoshis: pendingSwap.response.onchainAmount })
-            navigate(Pages.ReceiveSuccess)
-          })
+          .then(() => {
+            if (cancelled) return
+            setRecvInfo((prev) => ({ ...prev, satoshis: pendingSwap.response.onchainAmount }))
+            navigate(Pages.ReceiveSuccess)
+          })
           .catch((error) => {
-            setShowQrCode(true)
-            consoleError(error, 'Error claiming reverse swap:')
+            if (cancelled) return
+            setShowQrCode(true)
+            consoleError(error, 'Error claiming reverse swap')
           })
       })
       .catch((error) => {
-        setShowQrCode(true)
-        consoleError(error, 'Error creating reverse swap:')
+        if (cancelled) return
+        setShowQrCode(true)
+        consoleError(error, 'Error creating reverse swap')
       })
   } else {
     setShowQrCode(true)
   }
-}, [satoshis])
+  return () => {
+    cancelled = true
+  }
+}, [satoshis, wallet, svcWallet, swapProvider, validLnSwap, navigate, setRecvInfo])

95-101: Avoid in-place sort; use functional update.

.sort() mutates vtxos.spendable; also avoid stale recvInfo.

-  const lastVtxo = vtxos.spendable.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]
+  const lastVtxo = [...vtxos.spendable].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]
@@
-  setRecvInfo({ ...recvInfo, satoshis: value })
+  setRecvInfo((prev) => ({ ...prev, satoshis: value }))
♻️ Duplicate comments (5)
src/lib/format.ts (1)

38-48: Boundary and shadowing fixes in prettyDelta look good.

Using >= at unit boundaries and renaming the shadowed seconds variable resolves prior feedback.

package.json (3)

88-88: Engines: Node >=20 — good.

Align CI/Docker/.nvmrc to Node 20+ to avoid mismatch.


21-21: DOMPurify: enforce strict sanitize config at call sites.

Default sanitize() is permissive. Harden config wherever used (e.g., AlertBox).

If you want, I can supply a tiny wrapper like sanitizeStrict(html, optsOverride).


27-27: react-qr-barcode-scanner: add secure-context guards and lazy-load.

Mount only when isSecureContext && navigator.mediaDevices?.getUserMedia exist; lazy-load to avoid SSR/import-time issues.

src/screens/Wallet/Receive/QrCode.tsx (1)

35-37: Avoid emitting invalid BIP21 when on-chain address is absent; guard encodeBip21 and add deps.

Currently, address='' yields bitcoin:?... (invalid). Only build BIP21 when address exists; otherwise fall back to invoice for QR.

-  const address = validUtxoTx(satoshis) && utxoTxsAllowed() ? boardingAddr : ''
-  const arkAddress = validVtxoTx(satoshis) && vtxoTxsAllowed() ? offchainAddr : ''
+  const address = validUtxoTx(satoshis) && utxoTxsAllowed() ? boardingAddr : undefined
+  const arkAddress = validVtxoTx(satoshis) && vtxoTxsAllowed() ? offchainAddr : undefined
@@
-  const defaultBip21uri = encodeBip21(address, arkAddress, '', satoshis)
+  const defaultBip21uri = address ? encodeBip21(address, arkAddress ?? '', '', satoshis) : ''
@@
-useEffect(() => {
-  const bip21uri = encodeBip21(address, arkAddress, invoice, satoshis)
-  setBip21uri(bip21uri)
-  setQrValue(bip21uri)
-  if (invoice) setShowQrCode(true)
-}, [invoice])
+useEffect(() => {
+  if (address) {
+    const uri = encodeBip21(address, arkAddress ?? '', invoice, satoshis)
+    setBip21uri(uri)
+    setQrValue(uri)
+  } else if (invoice) {
+    setBip21uri(invoice)
+    setQrValue(invoice)
+  }
+  if (invoice) setShowQrCode(true)
+}, [address, arkAddress, invoice, satoshis])

Also applies to: 43-43, 50-56

🧹 Nitpick comments (14)
src/lib/format.ts (2)

51-52: Optional: floor seconds to avoid fractional output.

If callers ever pass fractional seconds, we’ll print decimals. Floor for consistency with other branches.

-    const secs = delta
+    const secs = Math.floor(delta)

76-83: Coerce before falsy check and avoid param reassignment in prettyLongText.

Move coercion ahead to handle numeric inputs (e.g., 0) correctly and use a local const.

-export const prettyLongText = (str?: string, showChars = 11): string => {
-  if (!str) return ''
-  str = String(str)
-  if (str.length <= showChars * 2 + 4) return str
-  const left = str.substring(0, showChars)
-  const right = str.substring(str.length - showChars, str.length)
-  return `${left}...${right}`
+export const prettyLongText = (str?: string, showChars = 11): string => {
+  const s = String(str ?? '')
+  if (s.length === 0) return ''
+  if (s.length <= showChars * 2 + 4) return s
+  const left = s.substring(0, showChars)
+  const right = s.substring(s.length - showChars, s.length)
+  return `${left}...${right}`
src/test/fixtures.json (4)

10-11: Network mix (bcrt1 vs 1BgGZ…) may confuse future address validation.

You mix regtest Bech32 (bcrt1…) with legacy mainnet vectors (1BgGZ…). That’s fine for parsing tests, but if validation becomes network-aware, this will create false negatives. Consider tagging vectors with network or splitting per network.

Also applies to: 17-19


160-165: Canonical BIP21 string is order- and format-sensitive; keep fixture synced with encoder.

This fixture asserts exact order and decimal formatting. Any change in encodeBip21 (param ordering, prettyNumber behavior) will break tests. Add a note that the canonical order is ark, lightning, amount and decimals must be fixed-point (no scientific notation).


15-113: Upstream vector provenance; add attribution to avoid licensing ambiguity.

These cases resemble upstream bitcoinjs bip21 vectors. Add attribution in a NOTICE/README near tests identifying the source and license.

I can draft the NOTICE snippet if you confirm the source.

Also applies to: 114-158


13-13: Unused arknote fixture—keep only if covered to avoid drift.

If no test uses arknote yet, either add a test or move it to a dedicated arknote fixture to reduce noise.

src/test/lib/bip21.test.ts (5)

13-15: Error path covered.

Covers non-bitcoin: scheme. Consider adding bitcoin:// to assert non-compliance still throws if you choose strict-only behavior.


18-22: String-equality on encoder is acceptable but brittle.

If encoder formatting changes (decimals, param order), this test will fail. Consider asserting decoded equivalence instead of exact string match.

Proposed tweak:

- expect(encodeBip21(address, arkAddress, invoice, satoshis)).toEqual(bip21)
+ const encoded = encodeBip21(address, arkAddress, invoice, satoshis)
+ expect(decodeBip21(encoded)).toEqual({ arkAddress, address, invoice, satoshis, lnurl: undefined })

46-52: Invalid-case coverage skips entries without uri.

If you want full parity with fixtures, generate URIs for those entries (e.g., bitcoin:${address}?amount=${options.amount}) and assert they throw too.

I can provide a helper to build these URIs safely if you want to expand coverage.


6-16: Add lnurl coverage.

Decode supports lightning=lnurl… but tests don’t. Add a case to ensure lnurl is set and invoice is undefined.

Example:

   describe('decodeBip21', () => {
+    it('parses lnurl when present', () => {
+      const uri = 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=1&lightning=lnurl1dp68gurn8ghj7mrfde'
+      const out = decodeBip21(uri)
+      expect(out).toMatchObject({
+        address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
+        satoshis: toSatoshis(1),
+        lnurl: expect.stringMatching(/^lnurl/i),
+        invoice: undefined,
+      })
+    })

25-31: Naming nit: “bip21.js tests”.

Consider renaming to “upstream bip21 vectors” to reflect provenance and TypeScript context.

package.json (1)

64-64: Remove @types/qs from package.json
@types/qs isn’t needed—qs isn’t listed as a dependency and isn’t used in the codebase.

src/screens/Wallet/Receive/QrCode.tsx (2)

111-126: Polling: stabilize UTXO detection and harden error handling.

includes() on objects relies on reference equality; add try/catch for reloadWallet.

-    const interval = setInterval(async () => {
+    const interval = setInterval(async () => {
       const utxos = await svcWallet!.getBoardingUtxos()
       if (utxos.length < currentUtxos.length) {
         currentUtxos = utxos
       }
       if (utxos.length > currentUtxos.length) {
-        const newUtxo = utxos.find((utxo) => !currentUtxos.includes(utxo))
+        // TODO: replace reference check with stable key comparison (e.g., txid:index)
+        const newUtxo = utxos.find((u) => !currentUtxos.includes(u))
         if (newUtxo) {
           currentUtxos = utxos
-          setRecvInfo({ ...recvInfo, satoshis: newUtxo.value })
-          await reloadWallet()
+          setRecvInfo((prev) => ({ ...prev, satoshis: newUtxo.value }))
+          try { await reloadWallet() } catch (e) { consoleError(e, 'Error reloading wallet') }
           notifyPaymentReceived(newUtxo.value)
           navigate(Pages.ReceiveSuccess)
         }
       }
     }, 5_000)

To confirm a stable key you can use (txid, index/vout) from ExtendedCoin; please verify field names in the SDK.


130-138: Minor: define share payload before handler to avoid temporal coupling.

Small readability improvement; no behavior change.

-  const handleShare = () => {
-    setSharing(true)
-    shareData(data)
-      .catch(consoleError)
-      .finally(() => setSharing(false))
-  }
-
-  const data = { title: 'Receive', text: qrValue }
+  const data = { title: 'Receive', text: qrValue }
+
+  const handleShare = () => {
+    setSharing(true)
+    shareData(data).catch(consoleError).finally(() => setSharing(false))
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5acd75d and fdd3084.

📒 Files selected for processing (11)
  • package.json (4 hunks)
  • src/lib/address.ts (2 hunks)
  • src/lib/bip21.ts (2 hunks)
  • src/lib/format.ts (2 hunks)
  • src/screens/Settings/Logs.tsx (2 hunks)
  • src/screens/Wallet/Receive/QrCode.tsx (4 hunks)
  • src/test/components/CheckList.test.tsx (1 hunks)
  • src/test/fixtures.json (1 hunks)
  • src/test/lib/bip21.test.ts (1 hunks)
  • src/test/lib/format.test.ts (1 hunks)
  • src/test/setup.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/screens/Settings/Logs.tsx
  • src/test/setup.ts
  • src/lib/address.ts
  • src/lib/bip21.ts
  • src/test/lib/format.test.ts
  • src/test/components/CheckList.test.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-28T15:58:17.212Z
Learnt from: bordalix
PR: arkade-os/wallet#142
File: src/lib/bip21.ts:7-13
Timestamp: 2025-08-28T15:58:17.212Z
Learning: In src/lib/bip21.ts, the encodeBip21 function is used by the wallet which always has an arkAddress available to pass to the function, while decodeBip21 is used to parse received possible BIP21 URIs which may not contain arkAddress. This explains why arkAddress is required in encodeBip21 but optional in the Bip21Decoded interface.

Applied to files:

  • src/screens/Wallet/Receive/QrCode.tsx
  • src/test/lib/bip21.test.ts
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
PR: arkade-os/wallet#114
File: src/lib/asp.ts:0-0
Timestamp: 2025-06-30T18:33:29.839Z
Learning: In src/lib/asp.ts, only the collaborativeExit function should accept both IWallet and ServiceWorkerWallet types. Other wallet functions (getBalance, getTxHistory, getReceivingAddresses, redeemNotes, sendOnChain, settleVtxos) should maintain their original IWallet-only signatures and not be updated for consistency.

Applied to files:

  • package.json
📚 Learning: 2025-07-07T14:41:35.991Z
Learnt from: bordalix
PR: arkade-os/wallet#106
File: src/lib/bolt11.ts:153-155
Timestamp: 2025-07-07T14:41:35.991Z
Learning: In src/lib/bolt11.ts, the code is copied from the original bitcoinjs/bolt11 library (https://github.com/bitcoinjs/bolt11/blob/master/payreq.js) and should be kept unchanged to maintain compatibility with the upstream source, even if there are potential improvements or syntax issues.

Applied to files:

  • src/test/fixtures.json
🧬 Code graph analysis (2)
src/screens/Wallet/Receive/QrCode.tsx (6)
src/providers/flow.tsx (1)
  • FlowContext (82-95)
src/providers/lightning.tsx (1)
  • LightningContext (21-25)
src/providers/wallet.tsx (1)
  • WalletContext (42-56)
src/providers/limits.tsx (1)
  • LimitsContext (29-40)
src/lib/bip21.ts (1)
  • encodeBip21 (65-72)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/test/lib/bip21.test.ts (2)
src/lib/bip21.ts (2)
  • decodeBip21 (16-63)
  • encodeBip21 (65-72)
src/lib/format.ts (1)
  • toSatoshis (8-10)
🔇 Additional comments (5)
src/test/lib/bip21.test.ts (2)

9-11: Good golden-path assertion.

Directly validates all key fields including absence of lnurl. Solid baseline.


2-2: No action needed: resolveJsonModule is already enabled
The tsconfig.json includes "resolveJsonModule": true on line 14, so importing JSON modules will work as expected.

package.json (2)

35-36: Nice add: UI test/coverage scripts.

Works with Vitest UI/coverage. No changes requested.


7-8: Approve SDK and boltz-swap bumps. Ensure all updated APIs (createReverseSwap, createSubmarineSwap, waitAndClaim, getLimits, getSwapHistory, ArkNote, ServiceWorkerWallet, etc.) still compile and behave correctly—run integration tests and review each package’s changelog for any breaking changes.

src/screens/Wallet/Receive/QrCode.tsx (1)

19-21: Imports: LightningContext + encodeBip21 — OK.

Good modularization.

Comment thread package.json
Comment on lines +60 to +62
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",

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.

💡 Verification agent

🧩 Analysis chain

ESLint 7 + ts-eslint v4 are incompatible with TypeScript 5.8 — upgrade lint stack.

With typescript ^5.8.3, keep ESLint ≥8 and @typescript-eslint ≥7; also bump airbnb config accordingly.

Apply this diff:

   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^4.33.0",
-    "@typescript-eslint/parser": "^4.33.0",
+    "@typescript-eslint/eslint-plugin": "^7.18.0",
+    "@typescript-eslint/parser": "^7.18.0",
-    "eslint": "^7.32.0",
-    "eslint-config-airbnb": "^18.2.1",
+    "eslint": "^8.57.0",
+    "eslint-config-airbnb": "^19.0.4",
     "eslint-config-prettier": "^8.10.0",
     "eslint-plugin-import": "^2.31.0",
     "eslint-plugin-jsx-a11y": "^6.10.2",
     "eslint-plugin-prettier": "^4.2.1",
     "eslint-plugin-react": "^7.37.5",
     "eslint-plugin-react-hooks": "^4.6.2",
     "jsdom": "^26.1.0",
     "typescript": "^5.8.3",
     "vite": "^7.1.3",
     "vitest": "^3.2.4"
   }

Quick check:

Also applies to: 79-79, 81-84


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
jq -r '.devDependencies | {typescript, eslint, "@typescript-eslint/eslint-plugin", "@typescript-eslint/parser", "eslint-config-airbnb"}' package.json

Length of output: 351


Upgrade lint stack for TS 5.8 compatibility
TS 5.8 support was added in typescript-eslint v8.26.0; bump ESLint to ^8.57.0, @typescript-eslint/parser & plugin to ^8.26.0, and eslint-config-airbnb to ^19.0.4. (classic.yarnpkg.com, typescript-eslint.io, unpkg.com)

🤖 Prompt for AI Agents
In package.json around lines 60 to 62, the lint toolchain is out of date for
TypeScript 5.8; update devDependencies: set "eslint" to ^8.57.0,
"@typescript-eslint/parser" and "@typescript-eslint/eslint-plugin" to ^8.26.0,
and "eslint-config-airbnb" to ^19.0.4 (adjust any peer deps if required), then
run your package manager to regenerate the lockfile (yarn install or npm
install) and run the linter to resolve any new rule issues introduced by the
upgraded packages.

Comment thread src/lib/format.ts Resolved
Comment thread src/test/fixtures.json Resolved
Comment thread src/test/lib/bip21.test.ts Resolved

@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: 5

Caution

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

⚠️ Outside diff range comments (10)
.eslintrc.json (1)

56-63: Avoid style-rule overlap with Prettier; remove quotes rule and enable Prettier integration

Having ESLint’s quotes alongside Prettier typically creates redundant/contradictory reports. Prefer enforcing via Prettier.

Apply within this range:

-    "quotes": [
-      "error",
-      "single",
-      {
-        "avoidEscape": true,
-        "allowTemplateLiterals": true
-      }
-    ]

Additionally, enable Prettier via extends (outside this range, top-level of the config):

{
  "extends": ["plugin:prettier/recommended"],
  "plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
  "rules": {
    "prettier/prettier": "error",
    ...
  }
}
src/lib/lnurl.ts (2)

74-76: Bug: UTF-8 conversion direction is reversed; returns bytes instead of string

bech32.decodeToBytes gives URL bytes; you must decode bytes → string. Current code likely returns Uint8Array, breaking callers expecting string.

Apply:

-  const { bytes } = bech32.decodeToBytes(lnurl)
-  return utf8.encode(bytes)
+  const { bytes } = bech32.decodeToBytes(lnurl)
+  return utf8.decode(bytes)

41-46: Build callback URL safely and encode note

Avoid manual string concatenation and ensure proper encoding of comment and coexistence with existing query params.

Apply:

-const fetchLnUrlInvoice = async (amount: number, note: string, data: LnUrlResponse) => {
-  let url = `${data.callback}?amount=${amount}`
-  if (note) url += `&comment=${note}`
-  const res = await fetch(url).then(checkResponse<LnUrlCallbackResponse>)
-  return res.pr
-}
+const fetchLnUrlInvoice = async (amount: number, note: string, data: LnUrlResponse) => {
+  const u = new URL(data.callback)
+  u.searchParams.set('amount', String(amount))
+  if (note) u.searchParams.set('comment', note) // URL API handles encoding
+  const res = await fetch(u.toString()).then(checkResponse<LnUrlCallbackResponse>)
+  return res.pr
+}
src/lib/format.ts (1)

12-19: prettyAgo: fix epoch unit detection (ms/µs) and align with summary

Current logic only handles ms (threshold-based) and misses µs; also inconsistent with the summary claiming truthiness-based parsing. Prefer magnitude-based detection for s/ms/µs.

Apply:

   const unixTimestamp =
     typeof timestamp === 'string'
       ? Math.floor(new Date(timestamp).getTime() / 1000)
-      : timestamp > 200_000_000_000
-        ? Math.floor(timestamp / 1000)
-        : timestamp
+      : (() => {
+          const t = Math.abs(timestamp)
+          if (t >= 1_000_000_000_000_000) return Math.floor(timestamp / 1_000_000) // microseconds → seconds
+          if (t >= 1_000_000_000_000) return Math.floor(timestamp / 1_000) // milliseconds → seconds
+          return Math.floor(timestamp) // seconds
+        })()
src/screens/Wallet/Receive/Amount.tsx (3)

57-58: Fix rules-of-hooks violation: early return precedes later hooks

Move the svcWallet loading early-return below all hooks so hooks are always called in the same order.

-  if (!svcWallet) return <Loading text='Loading...' />
@@
+  // place this after the last hook
+  if (!svcWallet) return <Loading text='Loading...' />

Also applies to: 88-88


59-71: Guard wallet-dependent effect, add deps, avoid string throws, and prevent setState after unmount

Without guarding, this effect runs on mount (svcWallet may be undefined); it also throws strings and lacks unmount protection.

-useEffect(() => {
-  getReceivingAddresses(svcWallet)
-    .then(({ offchainAddr, boardingAddr }) => {
-      if (!offchainAddr) throw 'Unable to get offchain address'
-      if (!boardingAddr) throw 'Unable to get boarding address'
-      setRecvInfo({ boardingAddr, offchainAddr, satoshis: 0 })
-    })
-    .catch((err) => {
-      const error = extractError(err)
-      consoleError(error, 'error getting addresses')
-      setError(error)
-    })
-}, [])
+useEffect(() => {
+  if (!svcWallet) return
+  let cancelled = false
+  getReceivingAddresses(svcWallet)
+    .then(({ offchainAddr, boardingAddr }) => {
+      if (cancelled) return
+      if (!offchainAddr) throw new Error('Unable to get offchain address')
+      if (!boardingAddr) throw new Error('Unable to get boarding address')
+      setRecvInfo({ boardingAddr, offchainAddr, satoshis: 0 })
+    })
+    .catch((err) => {
+      const errorMsg = extractError(err)
+      consoleError(errorMsg, 'error getting addresses')
+      setError(errorMsg)
+    })
+  return () => {
+    cancelled = true
+  }
+}, [svcWallet])

94-107: Harden faucet flow (validate prerequisites, use Error, and prevent double submits)

Guard missing address, validate satoshis, and disable the Faucet button while running.

 const handleFaucet = async () => {
   try {
-    if (!amount) throw 'Invalid amount'
+    if (!satoshis) throw new Error('Invalid amount')
+    if (!recvInfo?.offchainAddr) throw new Error('Missing offchain address')
     setFauceting(true)
     const ok = await callFaucet(recvInfo.offchainAddr, satoshis, aspInfo)
-    if (!ok) throw 'Faucet failed'
+    if (!ok) throw new Error('Faucet failed')
     setFauceting(false)
     setFaucetSuccess(true)
   } catch (err) {
     consoleError(err, 'error fauceting')
     setError(extractError(err))
     setFauceting(false)
   }
 }
@@
-        {showFaucetButton ? <Button disabled={!amount} label='Faucet' onClick={handleFaucet} secondary /> : null}
+        {showFaucetButton ? (
+          <Button disabled={!amount || fauceting} label='Faucet' onClick={handleFaucet} secondary />
+        ) : null}

Also applies to: 168-169

src/screens/Wallet/Send/Details.tsx (1)

106-115: Stop leaving UI in a loading state on early swap errors

You set sending=true at Line 102 but return early on missing response/address without resetting it, leaving the spinner stuck.

Apply:

-      const response = pendingSwap?.response
-      if (!response) return setError('Swap response not available')
-      const swapAddress = pendingSwap?.response.address
-      if (!swapAddress) return setError('Swap address not available')
+      const response = pendingSwap?.response
+      if (!response) {
+        setError('Swap response not available')
+        setSending(false)
+        return
+      }
+      const swapAddress = response.address
+      if (!swapAddress) {
+        setError('Swap address not available')
+        setSending(false)
+        return
+      }
src/screens/Wallet/Send/Form.tsx (2)

71-71: Fix rules-of-hooks violation: early return before hooks

Returning at Line 71 skips subsequent hooks on some renders. Move the loading state into the main render and guard effects that depend on svcWallet.

-  if (!svcWallet) return <Loading text='Loading...' />
+  // Do not early-return before hooks; render guarded later.

@@
-  useEffect(() => {
-    const { recipient, satoshis } = sendInfo
-    setRecipient(recipient ?? '')
-    setAmount(satoshis ? satoshis : undefined)
-    getReceivingAddresses(svcWallet).then(setReceivingAddresses)
-  }, [])
+  useEffect(() => {
+    const { recipient, satoshis } = sendInfo
+    setRecipient(recipient ?? '')
+    setAmount(satoshis ? satoshis : undefined)
+    if (!svcWallet) return
+    getReceivingAddresses(svcWallet).then(setReceivingAddresses)
+  }, [svcWallet])

@@
-  if (scan)
+  if (!svcWallet) return <Loading text='Loading...' />
+  if (scan)
     return (
       <Scanner close={() => setScan(false)} label='Recipient address' onData={setRecipient} onError={smartSetError} />
     )

Also applies to: 73-79


85-101: Do not lowercase recipient data; breaks Base58/BIP21 addresses

Lowercasing corrupts Bitcoin Base58 addresses and BIP21 payloads. Normalize only for detection; pass original-case data to decoders/state.

-      setNudgeBoltz(false)
-      if (!recipient) return
-      const lowerCaseData = recipient.toLowerCase().replace(/^lightning:/, '')
+      setNudgeBoltz(false)
+      if (!recipient) return
+      const raw = recipient.trim()
+      const normalized = raw.replace(/^lightning:/i, '')
+      const lower = normalized.toLowerCase()

@@
-      if (isBip21(lowerCaseData)) {
-        const { address, arkAddress, invoice, satoshis } = decodeBip21(lowerCaseData)
+      if (isBip21(normalized)) {
+        const { address, arkAddress, invoice, satoshis } = decodeBip21(normalized)
         if (!address && !arkAddress && !invoice) return setError('Unable to parse bip21')
         setAmount(useFiat ? toFiat(satoshis) : satoshis ? satoshis : undefined)
-        return setState({ address, arkAddress, invoice, recipient, satoshis })
+        return setState({ address, arkAddress, invoice, recipient: raw, satoshis })
       }
-      if (isArkAddress(lowerCaseData)) {
-        return setState({ ...sendInfo, address: '', arkAddress: lowerCaseData })
+      if (isArkAddress(normalized)) {
+        return setState({ ...sendInfo, address: '', arkAddress: normalized })
       }
-      if (isLightningInvoice(lowerCaseData)) {
+      if (isLightningInvoice(lower)) {
         if (!connected) {
           setError('Lightning swaps not enabled')
           return setNudgeBoltz(true)
         }
-        const satoshis = getInvoiceSatoshis(lowerCaseData)
+        const satoshis = getInvoiceSatoshis(normalized)
         if (!satoshis) return setError('Invoice must have amount defined')
         setAmount(useFiat ? toFiat(satoshis) : satoshis ? satoshis : undefined)
-        return setState({ ...sendInfo, address: '', arkAddress: '', invoice: lowerCaseData })
+        return setState({ ...sendInfo, address: '', arkAddress: '', invoice: normalized })
       }
-      if (isBTCAddress(lowerCaseData)) {
-        return setState({ ...sendInfo, address: lowerCaseData, arkAddress: '' })
+      if (isBTCAddress(normalized)) {
+        return setState({ ...sendInfo, address: normalized, arkAddress: '' })
       }
@@
-      if (isValidLnUrl(lowerCaseData)) {
-        return setState({ ...sendInfo, lnUrl: lowerCaseData })
+      if (isValidLnUrl(lower)) {
+        return setState({ ...sendInfo, lnUrl: normalized })
       }

Also applies to: 92-97, 101-113, 123-127

♻️ Duplicate comments (6)
package.json (3)

7-8: SDK and Boltz bump noted — ensure all API call sites remain compatible

Previous verification covered settle/signature usage; if unchanged since then, no action needed.


21-21: DOMPurify added — enforce hardened sanitize config at call sites

Reiterate: pass strict allowlists to sanitize(); avoid defaults.


27-27: Scanner dep added — mount guard + lazy-load required for secure contexts

Ensure feature-detect + client-only lazy import to avoid SSR/permission pitfalls.

src/lib/storage.ts (1)

5-11: Ensure storage is cleared and config re-saved even if DB cleanup fails (repeat)

On deleteAll/close error, localStorage.clear and config persistence are skipped. Use try/finally and optionally rethrow.

Apply:

 export async function clearStorage(): Promise<void> {
-  const config = readConfigFromStorage()
-  await vtxosRepository.deleteAll()
-  await vtxosRepository.close()
-  localStorage.clear()
-  if (config) saveConfigToStorage(config)
+  const config = readConfigFromStorage()
+  let opError: unknown
+  try {
+    await vtxosRepository.deleteAll()
+    await vtxosRepository.close()
+  } catch (err) {
+    opError = err
+    console.error('Failed to cleanup vtxos repository:', err)
+  } finally {
+    localStorage.clear()
+    if (config) saveConfigToStorage(config)
+  }
+  if (opError) throw opError
 }
src/screens/Wallet/Send/Form.tsx (2)

219-233: Handle null swapProvider; otherwise Proceed stalls

If swapProvider is null, promise is undefined and nothing happens after setting proceed=true. Show error and nudge.

-    if (!sendInfo.arkAddress && sendInfo.invoice && !sendInfo.pendingSwap) {
-      const promise = swapProvider?.createSubmarineSwap(sendInfo.invoice)
-      if (promise) {
-        promise
-          .then((pendingSwap) => {
-            if (!pendingSwap) return setError('Unable to create swap')
-            setState({ ...sendInfo, pendingSwap })
-          })
-          .catch(handleError)
-      }
-    } else navigate(Pages.SendDetails)
+    if (!sendInfo.arkAddress && sendInfo.invoice && !sendInfo.pendingSwap) {
+      const promise = swapProvider?.createSubmarineSwap(sendInfo.invoice)
+      if (!promise) {
+        setError('Lightning swaps not enabled')
+        setNudgeBoltz(true)
+        setProceed(false)
+        return
+      }
+      promise
+        .then((pendingSwap) => {
+          if (!pendingSwap) return setError('Unable to create swap')
+          setState({ ...sendInfo, pendingSwap })
+        })
+        .catch(handleError)
+    } else {
+      navigate(Pages.SendDetails)
+    }

362-368: Ensure isRiga parses env var correctly

This gating depends on isRiga, but isRiga is currently read as a boolean from a string env var. Normalize it to a real boolean in src/lib/constants.ts to avoid inverted gating.

Suggested (constants.ts):

export const isRiga = String(import.meta.env.VITE_IS_RIGA ?? '').toLowerCase() === 'true'
🧹 Nitpick comments (6)
.eslintrc.json (1)

28-28: Tighten no-unused-vars to reduce noise and align with common TS patterns

Empty options add no value. Recommend ignoring underscore-prefixed args/vars and rest siblings.

-    "@typescript-eslint/no-unused-vars": ["warn", {}],
+    "@typescript-eslint/no-unused-vars": [
+      "warn",
+      {
+        "args": "after-used",
+        "argsIgnorePattern": "^_",
+        "varsIgnorePattern": "^_",
+        "ignoreRestSiblings": true
+      }
+    ],
src/lib/format.ts (1)

28-33: Nit: rename local for clarity when suffix is provided

‘sats’ can be misleading when suffix switches units. Rename to ‘value’.

-export const prettyAmount = (amount: string | number, suffix?: string): string => {
-  const sats = typeof amount === 'string' ? Number(amount) : amount
-  if (suffix) return `${prettyNumber(sats, 2)} ${suffix}`
-  if (sats >= 100_000_000_000) return `${prettyNumber(fromSatoshis(sats), 0)} BTC`
-  if (sats >= 100_000_000) return `${prettyNumber(fromSatoshis(sats), 3)} BTC`
-  if (sats >= 1_000_000) return `${prettyNumber(sats / 1_000_000, 3)}M SATS`
-  return `${prettyNumber(sats)} ${sats === 1 ? 'SAT' : 'SATS'}`
+export const prettyAmount = (amount: string | number, suffix?: string): string => {
+  const value = typeof amount === 'string' ? Number(amount) : amount
+  if (suffix) return `${prettyNumber(value, 2)} ${suffix}`
+  if (value >= 100_000_000_000) return `${prettyNumber(fromSatoshis(value), 0)} BTC`
+  if (value >= 100_000_000) return `${prettyNumber(fromSatoshis(value), 3)} BTC`
+  if (value >= 1_000_000) return `${prettyNumber(value / 1_000_000, 3)}M SATS`
+  return `${prettyNumber(value)} ${value === 1 ? 'SAT' : 'SATS'}`
src/screens/Wallet/Receive/Amount.tsx (3)

89-92: Remove duplicate buttonLabel updates; the effect already controls it

Avoid flicker and inconsistent states by letting the effect be the single source of truth.

 const handleChange = (amount: number) => {
   setAmount(amount)
-  setButtonLabel(amount ? 'Continue' : defaultButtonLabel)
 }

137-137: Coalesce amount before toFiat to avoid NaN

Ensure toFiat always receives a number.

-    const displayAmount = useFiat ? prettyAmount(toFiat(amount), config.fiat) : prettyAmount(amount ?? 0)
+    const displayAmount = useFiat ? prettyAmount(toFiat(amount ?? 0), config.fiat) : prettyAmount(amount ?? 0)

51-55: Consider re-pinging faucet when ASP changes

If users switch servers, faucet availability may change. Re-run when aspInfo changes.

-}, [])
+}, [aspInfo])
src/screens/Wallet/Send/Form.tsx (1)

193-196: Keep derived-amount effect stable

The effect reads useFiat/fromFiat but doesn’t depend on them; add to deps to avoid stale conversions.

-  }, [amount])
+  }, [amount, useFiat, fromFiat])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fdd3084 and 0245763.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .eslintrc.json (2 hunks)
  • package.json (3 hunks)
  • src/components/Shadow.tsx (3 hunks)
  • src/components/TransactionsList.tsx (3 hunks)
  • src/index.css (3 hunks)
  • src/ionic.css (2 hunks)
  • src/lib/address.ts (3 hunks)
  • src/lib/format.ts (3 hunks)
  • src/lib/lnurl.ts (1 hunks)
  • src/lib/storage.ts (1 hunks)
  • src/providers/options.tsx (2 hunks)
  • src/screens/Settings/Password.tsx (1 hunks)
  • src/screens/Wallet/Receive/Amount.tsx (3 hunks)
  • src/screens/Wallet/Send/Details.tsx (6 hunks)
  • src/screens/Wallet/Send/Form.tsx (11 hunks)
  • src/test/components/AlertBox.test.tsx (1 hunks)
  • src/test/lib/bip21.test.ts (1 hunks)
  • src/test/lib/format.test.ts (1 hunks)
  • src/test/setup.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/test/components/AlertBox.test.tsx
  • src/components/Shadow.tsx
  • src/test/lib/format.test.ts
  • src/screens/Settings/Password.tsx
  • src/test/lib/bip21.test.ts
  • src/components/TransactionsList.tsx
  • src/ionic.css
  • src/lib/address.ts
  • src/providers/options.tsx
  • src/index.css
  • src/test/setup.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-28T15:58:17.212Z
Learnt from: bordalix
PR: arkade-os/wallet#142
File: src/lib/bip21.ts:7-13
Timestamp: 2025-08-28T15:58:17.212Z
Learning: In src/lib/bip21.ts, the encodeBip21 function is used by the wallet which always has an arkAddress available to pass to the function, while decodeBip21 is used to parse received possible BIP21 URIs which may not contain arkAddress. This explains why arkAddress is required in encodeBip21 but optional in the Bip21Decoded interface.

Applied to files:

  • src/screens/Wallet/Send/Form.tsx
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
PR: arkade-os/wallet#114
File: src/lib/asp.ts:0-0
Timestamp: 2025-06-30T18:33:29.839Z
Learning: In src/lib/asp.ts, only the collaborativeExit function should accept both IWallet and ServiceWorkerWallet types. Other wallet functions (getBalance, getTxHistory, getReceivingAddresses, redeemNotes, sendOnChain, settleVtxos) should maintain their original IWallet-only signatures and not be updated for consistency.

Applied to files:

  • package.json
🧬 Code graph analysis (3)
src/screens/Wallet/Send/Details.tsx (4)
src/providers/lightning.tsx (1)
  • LightningContext (21-25)
src/lib/lightning.tsx (1)
  • calcSwapFee (130-132)
src/lib/constants.ts (1)
  • defaultFee (2-2)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
src/screens/Wallet/Send/Form.tsx (7)
src/providers/lightning.tsx (1)
  • LightningContext (21-25)
src/lib/bip21.ts (2)
  • isBip21 (74-81)
  • decodeBip21 (16-63)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/lib/error.ts (1)
  • extractError (1-10)
src/components/Scanner.tsx (1)
  • Scanner (20-32)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
src/lib/constants.ts (1)
  • isRiga (7-7)
src/screens/Wallet/Receive/Amount.tsx (1)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
🪛 Biome (2.1.2)
src/screens/Wallet/Send/Form.tsx

[error] 193-193: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

src/screens/Wallet/Receive/Amount.tsx

[error] 77-77: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (15)
.eslintrc.json (1)

55-55: Good call on enforcing canonical array syntax

@typescript-eslint/array-type with { default: 'array' } improves consistency and readability.

package.json (2)

35-36: Nice addition of UI/coverage test scripts

This aids local dev and CI visibility.


89-89: Engines: Node ≥20 is correct (Node 18 EOL passed)

This matches modern Vite/Vitest stacks.

src/lib/lnurl.ts (1)

49-53: Catch binding removal is fine

No behavior change; eliminates unused var.

src/lib/storage.ts (1)

17-19: Catch binding removal is fine

Keeps behavior; avoids unused var.

src/lib/format.ts (5)

30-33: BTC/SATS thresholds and pluralization LGTM

= boundaries and singular “SAT” at 1 are correct.


38-41: Days boundary/pluralization fix LGTM


42-45: Hours boundary/pluralization fix LGTM


46-49: Minutes boundary/pluralization fix LGTM


51-52: Seconds branch rename avoids shadowing; LGTM

src/screens/Wallet/Receive/Amount.tsx (2)

7-7: Import rename looks correct

Default export is ErrorMessage from components/Error. Alignment with repo-wide rename.


154-154: Component rename usage LGTM

<ErrorMessage ... /> correctly matches the new import.

src/screens/Wallet/Send/Details.tsx (2)

110-116: Good guard against undefined promise from optional chaining

The added guard avoids calling .then on undefined when swapProvider is null.


148-148: Consistent error UI

Switch to ErrorMessage is consistent with the rest of the app.

src/screens/Wallet/Send/Form.tsx (1)

329-331: Scanner/InputAmount prop updates look good

Prop renames (onData/onError and onMax) are consistent with the new component APIs.

Also applies to: 350-358, 369-375

Comment thread src/lib/format.ts
Comment on lines 76 to 79
export const prettyLongText = (str?: string, showChars = 11): string => {
if (!str) return ''
str = String(str)
if (str.length <= showChars * 2 + 4) return str

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

prettyLongText: coercion happens too late; 0 gets dropped

With the current order, numeric 0 returns '' due to the falsy check before coercion. Move coercion ahead or narrow the falsy check.

-export const prettyLongText = (str?: string, showChars = 11): string => {
-  if (!str) return ''
-  str = String(str)
+export const prettyLongText = (str?: string, showChars = 11): string => {
+  if (str === undefined || str === null) return ''
+  str = String(str)
📝 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
export const prettyLongText = (str?: string, showChars = 11): string => {
if (!str) return ''
str = String(str)
if (str.length <= showChars * 2 + 4) return str
export const prettyLongText = (str?: string, showChars = 11): string => {
if (str === undefined || str === null) return ''
str = String(str)
if (str.length <= showChars * 2 + 4) return str
// …rest of implementation unchanged
}
🤖 Prompt for AI Agents
In src/lib/format.ts around lines 76 to 79, the current falsy check runs before
coercing the input to a string so numeric 0 (and other falsy-but-valid values)
become '' — move the coercion up or change the guard to explicitly check for
null/undefined; specifically, first coerce with str = str === undefined || str
=== null ? undefined : String(str) (or simply String(str) before the emptiness
check), then use a check like if (str === undefined || str === '') return '' (or
if (str == null) return ''), and keep the existing length comparison and
truncation logic unchanged.

Comment on lines 73 to 76
useEffect(() => {
setSatoshis(useFiat ? fromFiat(amount) : amount ?? 0)
setSatoshis(useFiat ? fromFiat(amount) : (amount ?? 0))
}, [amount])

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

Prevent NaN when amount is undefined and include missing deps

fromFiat(amount) can produce NaN when amount is undefined; also missing useFiat/fromFiat in deps.

-useEffect(() => {
-  setSatoshis(useFiat ? fromFiat(amount) : (amount ?? 0))
-}, [amount])
+useEffect(() => {
+  setSatoshis(useFiat ? fromFiat(amount ?? 0) : (amount ?? 0))
+}, [amount, useFiat, fromFiat])
📝 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
useEffect(() => {
setSatoshis(useFiat ? fromFiat(amount) : amount ?? 0)
setSatoshis(useFiat ? fromFiat(amount) : (amount ?? 0))
}, [amount])
useEffect(() => {
setSatoshis(useFiat ? fromFiat(amount ?? 0) : (amount ?? 0))
}, [amount, useFiat, fromFiat])
🧰 Tools
🪛 Biome (2.1.2)

[error] 73-73: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In src/screens/Wallet/Receive/Amount.tsx around lines 73 to 76, the effect calls
fromFiat(amount) which can return NaN when amount is undefined and the
dependency array is missing useFiat and fromFiat; fix by computing a safe
numeric value first (e.g. const safeAmount = amount ?? 0; const satoshis =
useFiat ? fromFiat(safeAmount) : safeAmount) and ensure you coerce NaN to 0
(e.g. Number.isFinite(satoshis) ? satoshis : 0) before calling setSatoshis, and
add useFiat and fromFiat to the effect dependency array.

Comment on lines 77 to 88
useEffect(() => {
setButtonLabel(
!satoshis
? defaultButtonLabel
: amountIsBelowMinLimit(satoshis)
? 'Amount below min limit'
: amountIsAboveMaxLimit(satoshis)
? 'Amount above max limit'
: 'Continue',
? 'Amount below min limit'
: amountIsAboveMaxLimit(satoshis)
? 'Amount above max limit'
: 'Continue',
)
}, [satoshis])

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.

🛠️ Refactor suggestion

Add missing dependencies to label effect

These functions can change; include them to keep label accurate.

-}, [satoshis])
+}, [satoshis, amountIsBelowMinLimit, amountIsAboveMaxLimit])
📝 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
useEffect(() => {
setButtonLabel(
!satoshis
? defaultButtonLabel
: amountIsBelowMinLimit(satoshis)
? 'Amount below min limit'
: amountIsAboveMaxLimit(satoshis)
? 'Amount above max limit'
: 'Continue',
? 'Amount below min limit'
: amountIsAboveMaxLimit(satoshis)
? 'Amount above max limit'
: 'Continue',
)
}, [satoshis])
useEffect(() => {
setButtonLabel(
!satoshis
? defaultButtonLabel
: amountIsBelowMinLimit(satoshis)
? 'Amount below min limit'
: amountIsAboveMaxLimit(satoshis)
? 'Amount above max limit'
: 'Continue',
)
}, [satoshis, amountIsBelowMinLimit, amountIsAboveMaxLimit])
🧰 Tools
🪛 Biome (2.1.2)

[error] 77-77: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In src/screens/Wallet/Receive/Amount.tsx around lines 77 to 88, the useEffect
that sets the button label only depends on satoshis but also reads
defaultButtonLabel, amountIsBelowMinLimit and amountIsAboveMaxLimit (which can
change); update the dependency array to include those values
(defaultButtonLabel, amountIsBelowMinLimit, amountIsAboveMaxLimit) so the effect
re-runs when any of them change and the label stays accurate.

Comment on lines 83 to 86
const handleTxid = (txid: string) => {
if (!txid) return setError('Error sending transaction')
setSendInfo({ ...sendInfo, total: (satoshis ?? 0) + feeInSats, txid })
setSendInfo({ ...sendInfo, total: details?.total, txid })
navigate(Pages.SendSuccess)

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.

🛠️ Refactor suggestion

Don’t trust derived state for totals; compute a safe fallback

details?.total can be undefined/stale. Use a deterministic fallback based on the current inputs.

-    setSendInfo({ ...sendInfo, total: details?.total, txid })
+    const fallbackTotal =
+      details?.total ??
+      (satoshis + (invoice ? calcSwapFee(satoshis) : 0))
+    setSendInfo({ ...sendInfo, total: fallbackTotal, txid })

Optionally make the fee detection explicit instead of coupling to destination equality:

-    const feeInSats = destination === invoice ? calcSwapFee(satoshis) : defaultFee
+    const feeInSats = invoice && lnSwapsAllowed() ? calcSwapFee(satoshis) : defaultFee

Also applies to: 62-64

🤖 Prompt for AI Agents
In src/screens/Wallet/Send/Details.tsx around lines 83-86 (also apply same
change to lines 62-64), do not use details?.total directly as it can be
undefined or stale; instead compute a deterministic fallback total from current
inputs (e.g., derive total from sendInfo amounts and explicit fee) before
setting state. Modify handleTxid to compute total = details?.total ??
computeTotalFrom(sendInfo, explicitFeeDetection) and use that total when calling
setSendInfo and navigate; make fee detection explicit rather than inferring from
destination equality (derive fee from state/inputs or a clear helper).

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
src/components/Scanner.tsx (2)

38-41: Use refs for camera/canvas/cancel; fix early-return close.

Plain lets are lost across renders, breaking cleanup and leaking the camera. Also, handleClose no-ops before init, preventing users from closing quickly.

-  let camera: any
-  let canvas: QRCanvas
-  let cancel: () => void
+  const cameraRef = useRef<any | null>(null)
+  const canvasRef = useRef<QRCanvas | null>(null)
+  const cancelRef = useRef<null | (() => void)>(null)
…
-  const handleClose = () => {
-    if (!cancel && !error) return
-    if (cancel) cancel()
-    camera?.stop()
-    close()
-  }
+  const handleClose = () => {
+    cancelRef.current?.()
+    cancelRef.current = null
+    cameraRef.current?.stop?.()
+    cameraRef.current = null
+    close()
+  }

Also applies to: 69-74


42-67: Effect cleanup, device selection, and handleClose behavior

  • Move camera, canvas, and cancel into useRef hooks to avoid stale closures inside the effect.
  • In handleClose, remove the early if (!cancel && !error) return so that close() always runs—even before camera init.
  • When choosing a device, match /back|environment/i against device.label, falling back to the last entry.
  • In the effect’s cleanup, call cancelRef.current(), stop the camera, then invoke close().
♻️ Duplicate comments (2)
package.json (2)

7-8: SDK/boltz-swap bumps — confirm API compatibility (settle signature, etc.)

Past reports indicated break risk on a 2nd arg to settle; ensure all call sites are aligned.


21-21: DOMPurify added — enforce strict sanitize config at call sites

Use a hardened config (empty ALLOWED_TAGS/ATTR, forbid on* attrs, disallow unknown protocols) instead of defaults in Alert/Info components.

🧹 Nitpick comments (9)
package.json (2)

27-27: react-qr-reader (beta) — guard secure contexts and lazy-load the scanner

Ensure scanner mounts only when isSecureContext && navigator.mediaDevices?.getUserMedia are available; prefer lazy/dynamic import to avoid SSR/worker import issues and shrink initial bundle. Also confirm iOS rear-camera selection fallback via deviceId when facingMode is ignored.


60-60: @eslint/compat added — align on config strategy (flat vs. legacy)

You still have a package.json "eslintConfig". If moving to flat config with @eslint/compat, drop the legacy block and add an eslint.config.(js|mjs); otherwise remove @eslint/compat.

src/test/screens/wallet/index.test.tsx (3)

7-7: Wrap Wallet with Router/providers to avoid context crashes.

If Wallet uses routing/contexts, wrap with MemoryRouter (or project renderWithProviders).

-import { render, screen } from '@testing-library/react'
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router-dom'
@@
-    render(<Wallet />)
+    render(
+      <MemoryRouter>
+        <Wallet />
+      </MemoryRouter>
+    )

6-13: Use role-based, case-insensitive, and async queries for stability (i18n/async UI).

Texts may change casing/locale or render after effects. Prefer roles and /.../i with findBy*.

-describe('Wallet screen', () => {
-  it('renders the wallet screen with the correct elements', () => {
-    render(<Wallet />)
-    expect(screen.getByText('SATS')).toBeInTheDocument()
-    expect(screen.getByText('Send')).toBeInTheDocument()
-    expect(screen.getByText('Receive')).toBeInTheDocument()
-    expect(screen.getByText('My balance')).toBeInTheDocument()
-    expect(screen.getByText('No transactions yet')).toBeInTheDocument()
-  })
-})
+describe('Wallet screen', () => {
+  it('renders primary CTAs and empty state', async () => {
+    render(
+      <MemoryRouter>
+        <Wallet />
+      </MemoryRouter>
+    )
+    expect(await screen.findByText(/sats/i)).toBeInTheDocument()
+    expect(await screen.findByRole('button', { name: /send/i })).toBeInTheDocument()
+    expect(await screen.findByRole('button', { name: /receive/i })).toBeInTheDocument()
+    expect(await screen.findByText(/my balance/i)).toBeInTheDocument()
+    expect(await screen.findByText(/no transactions yet/i)).toBeInTheDocument()
+  })
+})

5-14: Consider isolating states and providers for determinism.

If Wallet conditionally shows Riga alerts/Nudge/default-password banners, mock those providers (or expose feature flags) so this test stays deterministic.

I can add a renderWithProviders test util with default mocked contexts and router. Want a follow-up PR?

src/components/Scanner.tsx (4)

88-88: Ensure camera autoplay on iOS Safari.

Add muted/playsInline/autoplay to avoid fullscreen blocking and permission quirks.

-          <video style={videoStyle} ref={videoRef} />
+          <video style={videoStyle} ref={videoRef} autoPlay playsInline muted />

84-84: Aux label “M/R” may be cryptic.

Consider an icon or “Switch” text for clarity/accessibility.

Also applies to: 101-101


87-87: Message copy nit.

“Camera not available” → “Camera not available. Check permissions in your browser settings.” for clearer guidance.


12-18: Split public/internal props: remove onSwitch from the exported Scanner’s props and extract it into an internal ScannerInnerProps for ScannerMills/ScannerReact. No external callers of onSwitch found.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0245763 and 85caca6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • package.json (3 hunks)
  • src/components/Scanner.tsx (4 hunks)
  • src/test/screens/wallet/index.test.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
PR: arkade-os/wallet#114
File: src/lib/asp.ts:0-0
Timestamp: 2025-06-30T18:33:29.839Z
Learning: In src/lib/asp.ts, only the collaborativeExit function should accept both IWallet and ServiceWorkerWallet types. Other wallet functions (getBalance, getTxHistory, getReceivingAddresses, redeemNotes, sendOnChain, settleVtxos) should maintain their original IWallet-only signatures and not be updated for consistency.

Applied to files:

  • package.json
🧬 Code graph analysis (1)
src/components/Scanner.tsx (2)
src/lib/error.ts (1)
  • extractError (1-10)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages: wallet-bitcoin-v8
🔇 Additional comments (7)
package.json (4)

89-90: Engines Node ≥20 — good call; update CI/tooling pins

LGTM. Ensure GitHub Actions, Dockerfiles, and .nvmrc/asdf match Node 20.


35-36: Vitest UI/coverage scripts — nice additions

Helpful for local dev and CI coverage workflows.


83-85: Peer dependency verification complete
All required peer ranges are satisfied: Vitest 3.2.4 has no Vite peer, and @vitejs/plugin-react 5.0.2 accepts Vite ^7.0.0 (including 7.1.3). No further changes needed.


93-94: Ensure Rollup resolution doesn’t break plugins
pnpm ls rollup shows Rollup 2.79.2 (vite-plugin-eslint) and 4.44.2 (Vite). Since you’ve pinned Rollup to 4.44.2 via resolutions, confirm vite-plugin-eslint is compatible with Rollup 4.x or adjust the resolution.

src/test/screens/wallet/index.test.tsx (1)

3-3: Verified default export and correct path casing

The src/screens/Wallet/Index.tsx file exports a default Wallet component (line 26), and the import path in the test matches the directory and filename casing.

src/components/Scanner.tsx (2)

4-11: Imports look consistent with recent renames and new deps.

ErrorMessage/extractError wiring and QrReader import are correct.


98-118: Remove invalid refactor suggestion
ScannerInnerProps isn’t defined—keep using ScannerProps—and react-qr-reader@3.0.0-beta-1’s onResult delivers a { text: string } result (not a getText() method).

Likely an incorrect or invalid review comment.

Comment thread package.json Resolved
Comment on lines +1 to +3
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Wallet from '../../../screens/Wallet/Index'

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.

💡 Verification agent

🧩 Analysis chain

Ensure jest-dom matchers are loaded in Vitest setup.

toBeInTheDocument() requires @testing-library/jest-dom. Verify setup includes it.

Run:


🏁 Script executed:

#!/bin/bash
# Check Vitest setup file is registered
rg -nP 'setupFiles\s*:\s*\[.*src/test/setup' -g 'vitest.config.*'

# Check jest-dom is imported in setup
rg -n "@testing-library/jest-dom/(vitest)?'" src/test/setup.*

Length of output: 179


Add the setup file to Vitest’s config to load jest-dom matchers

Vitest isn’t currently loading your setup file, so even though src/test/setup.ts imports @testing-library/jest-dom/vitest, those matchers won’t be applied. In vitest.config.ts, under the test section, add:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    setupFiles: ['src/test/setup.ts'],
    // …other options
  },
})

This ensures toBeInTheDocument() and other jest-dom matchers are available in all your tests.

🤖 Prompt for AI Agents
In src/test/screens/wallet/index.test.tsx lines 1-3: Vitest isn't loading the
jest-dom matchers because the test setup file isn't registered; update
vitest.config.ts (the project's root vitest config) and under the test section
add setupFiles: ['src/test/setup.ts'] (or the correct relative path) so that
src/test/setup.ts (which imports @testing-library/jest-dom/vitest) is executed
before tests, ensuring matchers like toBeInTheDocument() are available globally.

@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/Onboard.tsx (1)

86-87: Fix user-facing grammar.
“enable” → “enables”; add article; optionally clarify “Home Screen”.

-          <Title text='Install Arkade on Home' />
-          <Text wrap>Adding Arkade to Home enable push notifications and better user experience.</Text>
+          <Title text='Install Arkade on Home Screen' />
+          <Text wrap>Adding Arkade to the Home Screen enables push notifications and a better user experience.</Text>
🧹 Nitpick comments (9)
src/screens/Settings/About.tsx (4)

22-23: Harden URL/pubkey rendering; consider copyability/overflow handling.

Add a safe fallback to avoid empty cells when values are missing. Optionally make these cells copyable and truncate long strings to prevent layout breakage.

-    ['Server URL', aspInfo.url],
-    ['Server pubkey', aspInfo.signerPubkey],
+    ['Server URL', aspInfo.url || '—'],
+    ['Server pubkey', aspInfo.signerPubkey || '—'],

26-26: Unit casing nit — prefer “sats”.

Consistent lowercase “sats” is more common across UIs.

-    ['Dust', `${aspInfo.dust} SATS`],
+    ['Dust', `${aspInfo.dust} sats`],

27-31: Avoid blank cells when values are 0/NaN; prettyDelta returns '' in those cases.

Add a simple fallback so the table communicates “no delay/unknown” rather than rendering empty.

-    ['Round interval', prettyDelta(Number(aspInfo.roundInterval), true)],
+    ['Round interval', prettyDelta(Number(aspInfo.roundInterval), true) || '—'],
-    ['VTXO tree expiry', prettyDelta(Number(aspInfo.vtxoTreeExpiry), true)],
+    ['VTXO tree expiry', prettyDelta(Number(aspInfo.vtxoTreeExpiry), true) || '—'],
-    ['Boarding exit delay', prettyDelta(Number(aspInfo.boardingExitDelay), true)],
+    ['Boarding exit delay', prettyDelta(Number(aspInfo.boardingExitDelay), true) || '—'],
-    ['Unilateral exit delay', prettyDelta(Number(aspInfo.unilateralExitDelay), true)],
+    ['Unilateral exit delay', prettyDelta(Number(aspInfo.unilateralExitDelay), true) || '—'],

40-40: Drop derived state for error; compute inline to avoid staleness.

No need for useState/useEffect just to mirror aspInfo.unreachable.

// imports
-import { useContext, useEffect, useState } from 'react'
+import { useContext } from 'react'

// inside component
-const [error, setError] = useState(false)
-useEffect(() => {
-  setError(aspInfo.unreachable)
-}, [aspInfo.unreachable])
+const error = Boolean(aspInfo?.unreachable)

Rendering line stays the same:

<ErrorMessage error={error} text='Ark server unreachable' />
src/screens/Wallet/Onboard.tsx (2)

29-30: Avoid stale state; use functional updater and clamp.
Prevents lost increments on rapid double-clicks and guards upper bound.

-  const handleContinue = () => setStep(step + 1)
+  const handleContinue = () => setStep((s) => Math.min(s + 1, steps))

41-46: Type the inline style.
Use a proper React CSS type instead of any.

-    const style: any = {
+    const style: React.CSSProperties = {

And add:

- import { ReactNode, useContext, useState } from 'react'
+ import type { CSSProperties } from 'react'
+ import { ReactNode, useContext, useState } from 'react'
src/test/screens/wallet/onboard.test.tsx (1)

17-25: Prefer userEvent for realistic interactions and add test isolation.
Improves reliability and avoids brittle DOM .click().

-import { render, screen } from '@testing-library/react'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
@@
 describe('Onboard screen', () => {
+  beforeEach(() => {
+    pwaCanInstall = false
+    vi.clearAllMocks()
+  })
@@
-    screen.getByText('Continue').click()
+    const user = userEvent.setup()
+    await user.click(screen.getByText('Continue'))
@@
-    screen.getByText('Continue').click()
+    await user.click(screen.getByText('Continue'))

Repeat similarly for the 4-step test.

src/test/screens/mocks.ts (2)

15-28: Freeze or clone to prevent accidental mutation in tests.
The shared objects are currently mutable and get modified in tests; shallow-freeze them to catch mutations early.

-export const mockFlowContextValue = {
+export const mockFlowContextValue = Object.freeze({
   txInfo: mockTxInfo,
   swapInfo: undefined,
   initInfo: emptyInitInfo,
   noteInfo: emptyNoteInfo,
   recvInfo: emptyRecvInfo,
   sendInfo: emptySendInfo,
   setInitInfo: () => {},
   setNoteInfo: () => {},
   setRecvInfo: () => {},
   setSendInfo: () => {},
   setSwapInfo: () => {},
   setTxInfo: () => {},
-}
+})

Optionally also freeze mockTxInfo.


3-13: Consider typing and const-asserting mocks.
Improves DX and catches shape drift when provider contracts change.

-export const mockTxInfo = {
+export const mockTxInfo = {
   amount: 100000,
@@
-  type: 'received',
+  type: 'received',
-}
+} as const
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 85caca6 and 8b92963.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • package.json (3 hunks)
  • src/screens/Settings/About.tsx (3 hunks)
  • src/screens/Wallet/Onboard.tsx (3 hunks)
  • src/test/screens/mocks.ts (1 hunks)
  • src/test/screens/wallet/onboard.test.tsx (1 hunks)
  • src/test/screens/wallet/transaction.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
🧰 Additional context used
🧬 Code graph analysis (4)
src/test/screens/wallet/onboard.test.tsx (2)
src/lib/pwa.ts (1)
  • pwaCanInstall (5-5)
src/screens/Wallet/Onboard.tsx (1)
  • Onboard (22-134)
src/test/screens/wallet/transaction.test.tsx (4)
src/providers/flow.tsx (1)
  • FlowContext (82-95)
src/test/screens/mocks.ts (2)
  • mockFlowContextValue (15-28)
  • mockLimitsContextValue (30-41)
src/providers/limits.tsx (1)
  • LimitsContext (29-40)
src/screens/Wallet/Transaction.tsx (1)
  • Transaction (26-164)
src/test/screens/mocks.ts (1)
src/providers/flow.tsx (4)
  • emptyInitInfo (57-60)
  • emptyNoteInfo (62-65)
  • emptyRecvInfo (67-71)
  • emptySendInfo (73-80)
src/screens/Settings/About.tsx (2)
src/lib/format.ts (1)
  • prettyDelta (36-55)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
🔇 Additional comments (4)
src/screens/Settings/About.tsx (2)

10-10: LGTM — clearer import alias.

The default import binding to ErrorMessage improves readability and matches the component name.


22-31: AspInfo properties verified All referenced keys (url, signerPubkey, forfeitAddress, network, dust, roundInterval, vtxoTreeExpiry, boardingExitDelay, unilateralExitDelay) are defined on the AspInfo type (see emptyAspInfo in src/lib/asp.ts), so runtime accesses will never be undefined.

src/test/screens/wallet/transaction.test.tsx (1)

55-56: Exact match is correct — the component passes name='Settle transaction' (lowercase “t”), so your test’s case-sensitive assertion matches the rendered text and doesn’t need changing.

src/test/screens/wallet/onboard.test.tsx (1)

5-9: Good dynamic module mock for PWA capability.
This pattern keeps tests deterministic while covering 3- and 4-step paths.

@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)
package.json (1)

91-93: resolutions won’t apply under pnpm; switch to pnpm.overrides

With pnpm 10, use overrides to pin rollup.

-  "resolutions": {
-    "rollup": "4.44.2"
-  }
+  "pnpm": {
+    "overrides": {
+      "rollup": "4.44.2"
+    }
+  }
🧹 Nitpick comments (4)
src/providers/lightning.tsx (2)

48-59: Avoid redundant config writes

Short-circuit when the value is unchanged to prevent unnecessary re-renders across the app.

-  const setConnected = (value: boolean) => {
-    updateConfig({
+  const setConnected = (value: boolean) => {
+    if (config.apps.boltz.connected === value) return
+    updateConfig({
       ...config,
       apps: {
         ...config.apps,
         boltz: {
           ...config.apps.boltz,
           connected: value,
         },
       },
     })
   }

38-39: Treat empty VITE_BOLTZ_URL as unset

An empty string currently disables baseUrl via the nullish coalescing semantics. The revised effect above explicitly trims and ignores empty values to avoid confusion. If you don’t adopt that change, at least normalize here.

-    const baseUrl = BoltzUrl ?? BASE_URLS[aspInfo.network as Network]
+    const baseUrl = (typeof BoltzUrl === 'string' && BoltzUrl.trim()) || BASE_URLS[aspInfo.network as Network]
package.json (2)

26-26: New scanner dep: verify secure-context and feature-detect guards

Ensure components using react-qr-reader only mount in HTTPS/localhost and after checking navigator.mediaDevices/getUserMedia. If you want, I can supply a guarded, lazy-loaded wrapper.


64-64: Remove unused @types/qs

qs was removed; drop its types to avoid stale deps.

-    "@types/qs": "^6.9.18",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8b92963 and 9b12560.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • package.json (3 hunks)
  • src/lib/constants.ts (2 hunks)
  • src/providers/lightning.tsx (1 hunks)
  • src/test/screens/wallet/transaction.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/screens/wallet/transaction.test.tsx
  • src/lib/constants.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
PR: arkade-os/wallet#114
File: src/lib/asp.ts:0-0
Timestamp: 2025-06-30T18:33:29.839Z
Learning: In src/lib/asp.ts, only the collaborativeExit function should accept both IWallet and ServiceWorkerWallet types. Other wallet functions (getBalance, getTxHistory, getReceivingAddresses, redeemNotes, sendOnChain, settleVtxos) should maintain their original IWallet-only signatures and not be updated for consistency.

Applied to files:

  • package.json
🧬 Code graph analysis (1)
src/providers/lightning.tsx (5)
src/lib/lightning.tsx (1)
  • LightningSwapProvider (16-128)
src/providers/asp.tsx (1)
  • AspContext (15-20)
src/providers/wallet.tsx (1)
  • WalletContext (42-56)
src/providers/config.tsx (1)
  • ConfigContext (29-37)
src/lib/constants.ts (1)
  • BoltzUrl (24-24)
🔇 Additional comments (4)
package.json (4)

7-8: Boltz/Sdk bumps look good

Matches provider usage introduced in this PR.


34-35: Nice: added vitest UI/coverage scripts

Good additions for DX.


59-85: Lint config consistency check

You upgraded ESLint/ts-eslint, but package.json still has "eslintConfig": { "extends": ["react-app"] }. If you’re not using eslint-config-react-app, remove that block or add the missing config to devDependencies. Otherwise pnpm lint may fail.


88-88: Engine to Node ≥20: good

Aligns with Vite/Vitest ecosystem and long-term support.

Comment on lines +36 to +45
useEffect(() => {
if (!aspInfo.network || !svcWallet) return
const baseUrl = BoltzUrl ?? BASE_URLS[aspInfo.network as Network]
if (!baseUrl) return // No boltz server for this network
StorageProvider.create().then((storageProvider) => {
setSwapProvider(new LightningSwapProvider(baseUrl, aspInfo, svcWallet, storageProvider))
setConnected(config.apps.boltz.connected)
})
}, [aspInfo, svcWallet, config.apps.boltz.connected])

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

Fix stale provider and race on reconnect; gate by connected + add cleanup/error-handling

If aspInfo/svcWallet/connected changes or becomes unavailable, the old swapProvider remains set. This can lead to using a provider bound to the wrong network/wallet. Also, the async init has no cancellation or error handling.

Apply:

-  useEffect(() => {
-    if (!aspInfo.network || !svcWallet) return
-    const baseUrl = BoltzUrl ?? BASE_URLS[aspInfo.network as Network]
-    if (!baseUrl) return // No boltz server for this network
-    StorageProvider.create().then((storageProvider) => {
-      setSwapProvider(new LightningSwapProvider(baseUrl, aspInfo, svcWallet, storageProvider))
-      setConnected(config.apps.boltz.connected)
-    })
-  }, [aspInfo, svcWallet, config.apps.boltz.connected])
+  useEffect(() => {
+    let cancelled = false
+
+    // Respect user toggle; clear provider when disconnected
+    if (!config.apps.boltz.connected) {
+      setSwapProvider(null)
+      return
+    }
+    // Clear when prerequisites are missing
+    if (!aspInfo?.network || !svcWallet) {
+      setSwapProvider(null)
+      return
+    }
+    // Prefer non-empty override; otherwise derive by network
+    const override = typeof BoltzUrl === 'string' && BoltzUrl.trim().length > 0 ? BoltzUrl : undefined
+    const baseUrl = override ?? BASE_URLS[aspInfo.network as Network]
+    if (!baseUrl) {
+      setSwapProvider(null)
+      return
+    }
+
+    StorageProvider.create()
+      .then((storageProvider) => {
+        if (cancelled) return
+        setSwapProvider(new LightningSwapProvider(baseUrl, aspInfo, svcWallet, storageProvider))
+      })
+      .catch((err) => {
+        console.error('Lightning: failed to init swap provider', err)
+        if (!cancelled) setSwapProvider(null)
+      })
+
+    return () => {
+      cancelled = true
+      setSwapProvider(null)
+    }
+  }, [aspInfo.network, aspInfo.url, svcWallet, config.apps.boltz.connected])
📝 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
useEffect(() => {
if (!aspInfo.network || !svcWallet) return
const baseUrl = BoltzUrl ?? BASE_URLS[aspInfo.network as Network]
if (!baseUrl) return // No boltz server for this network
StorageProvider.create().then((storageProvider) => {
setSwapProvider(new LightningSwapProvider(baseUrl, aspInfo, svcWallet, storageProvider))
setConnected(config.apps.boltz.connected)
})
}, [aspInfo, svcWallet, config.apps.boltz.connected])
useEffect(() => {
let cancelled = false
// Respect user toggle; clear provider when disconnected
if (!config.apps.boltz.connected) {
setSwapProvider(null)
return
}
// Clear when prerequisites are missing
if (!aspInfo?.network || !svcWallet) {
setSwapProvider(null)
return
}
// Prefer non-empty override; otherwise derive by network
const override =
typeof BoltzUrl === 'string' && BoltzUrl.trim().length > 0
? BoltzUrl
: undefined
const baseUrl = override ?? BASE_URLS[aspInfo.network as Network]
if (!baseUrl) {
setSwapProvider(null)
return
}
StorageProvider.create()
.then((storageProvider) => {
if (cancelled) return
setSwapProvider(
new LightningSwapProvider(
baseUrl,
aspInfo,
svcWallet,
storageProvider,
),
)
})
.catch((err) => {
console.error('Lightning: failed to init swap provider', err)
if (!cancelled) setSwapProvider(null)
})
return () => {
cancelled = true
setSwapProvider(null)
}
}, [aspInfo.network, aspInfo.url, svcWallet, config.apps.boltz.connected])
🤖 Prompt for AI Agents
In src/providers/lightning.tsx around lines 36 to 45, the effect currently
initializes a LightningSwapProvider asynchronously but does not gate by the
boltz connected flag, lacks cancellation/cleanup, and leaves stale providers
when aspInfo/svcWallet/connected changes; update the effect to: 1) include
config.apps.boltz.connected in the early-return guard (only proceed when
connected is true and aspInfo and svcWallet are present), 2) create a
cancellable init (use a mounted boolean or AbortController) and ignore/set aside
results if cancelled, 3) add try/catch around async StorageProvider.create and
provider construction to log errors and avoid leaving state inconsistent, 4) on
cleanup clear setSwapProvider(null) and dispose the created provider if it
exposes a close/dispose method, and 5) ensure setConnected is only set based on
actual success or left unchanged on failure.

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
package.json (2)

41-45: Remove CRA “react-app” ESLint preset — not installed and conflicts with Vite

Vite projects typically don’t use eslint-config-react-app. Keep a .eslintrc/flat config instead; this block will cause resolution errors.

Apply:

-  "eslintConfig": {
-    "extends": [
-      "react-app"
-    ]
-  },

92-94: Using “resolutions” with pnpm won’t take effect — use pnpm.overrides

pnpm ignores Yarn’s “resolutions”. Move the Rollup pin to pnpm.overrides (or top-level overrides).

Apply:

-  "resolutions": {
-    "rollup": "4.44.2"
-  }
+  "pnpm": {
+    "overrides": {
+      "rollup": "4.44.2"
+    }
+  }
♻️ Duplicate comments (1)
src/test/fixtures.json (1)

15-113: Avoid reintroducing “compliant” flag coupling; confirm tests treat absence as true.

Past reviews noted valid vectors lacking compliant were skipped when tests used compliant === true. If tests were updated to interpret absence as compliant (i.e., compliant !== false), we’re good. Otherwise, add "compliant": true to the intended-valid cases.

🧹 Nitpick comments (11)
src/test/lib/fiat.test.ts (4)

5-8: Reset mocks between tests to prevent cross-test leakage.

Add a beforeEach to reset the mocked fetch and clear spies.

 describe('fiat utilities', () => {
+  beforeEach(() => {
+    fetchMocker.resetMocks()
+    vi.clearAllMocks()
+  })
   it('should fetch fiat values', async () => {

19-22: Also assert the endpoint and call count.

This ensures the implementation hits the expected URL exactly once.

-    const result = await getPriceFeed()
-    expect(result).toEqual(expected)
+    const result = await getPriceFeed()
+    expect(globalThis.fetch).toHaveBeenCalledTimes(1)
+    expect(globalThis.fetch).toHaveBeenCalledWith('https://blockchain.info/ticker')
+    expect(result).toEqual(expected)

9-23: Add a failure-path test.

Guard against regressions: fetch rejection should yield undefined.

 describe('fiat utilities', () => {
   it('should fetch fiat values', async () => {
@@
     expect(result).toEqual(expected)
   })
+
+  it('returns undefined when fetch fails', async () => {
+    fetchMocker.mockRejectOnce(new Error('network'))
+    await expect(getPriceFeed()).resolves.toBeUndefined()
+  })
 })

5-8: Consider moving enableMocks to a global test setup.

Centralizing fetchMocker.enableMocks() in a Vitest setup file avoids accidental partial mocking across suites.

If you want, I can draft a minimal vitest.setup.ts and config snippet to wire this globally.

package.json (3)

26-26: react-qr-reader (beta) — guard for secure context + lazy-load

This lib accesses camera APIs. Only mount in HTTPS/localhost, feature-detect mediaDevices/getUserMedia, and consider lazy-loading to avoid SSR/import-time issues.


59-86: ESLint/TS stack versions look compatible; remove stale @types/qs

Tooling versions are coherent with TS 5.8. If runtime "qs" was removed, drop @types/qs to avoid dead types.

Apply:

-    "@types/qs": "^6.9.18",

89-89: Engines: Node >=20 is correct; align pnpm to >=10

You pin packageManager to pnpm 10.13.1 but engines.pnpm is >=8. Tighten to avoid toolchain drift.

Apply:

   "engines": {
     "node": ">=20",
-    "pnpm": ">=8"
+    "pnpm": ">=10"
   },
src/test/lib/lnurl.test.ts (4)

1-4: Import beforeEach to reset fetch mocks per test.

-import { describe, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'

6-9: Reset fetch mocks between tests to avoid cross-test leakage.

 fetchMocker.enableMocks()
 
+beforeEach(() => {
+  fetchMocker.resetMocks()
+  vi.clearAllMocks()
+})

34-41: Assert the invoice request includes the correct msat amount and note.

Strengthen this test by verifying the second fetch call hits callback with amount = 21000 (for 21 sats) and the expected note/comment, preventing regressions in query construction.

I can follow up with an assertion snippet tailored to your fetchLnUrlInvoice implementation if you expose the URL build logic.


10-15: Use spec-shaped LNURL metadata to increase fixture fidelity.

LNURL metadata is a JSON-encoded string (e.g., [[\"text/plain\",\"…\"]]). Using that shape better matches real servers.

-  metadata: 'mock-metadata',
+  metadata: '[[\"text/plain\",\"mock-metadata\"]]',
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9b12560 and 84fd37d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • package.json (3 hunks)
  • src/lib/fiat.ts (0 hunks)
  • src/test/fixtures.json (1 hunks)
  • src/test/lib/fiat.test.ts (1 hunks)
  • src/test/lib/lnurl.test.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/lib/fiat.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-07T14:41:35.991Z
Learnt from: bordalix
PR: arkade-os/wallet#106
File: src/lib/bolt11.ts:153-155
Timestamp: 2025-07-07T14:41:35.991Z
Learning: In src/lib/bolt11.ts, the code is copied from the original bitcoinjs/bolt11 library (https://github.com/bitcoinjs/bolt11/blob/master/payreq.js) and should be kept unchanged to maintain compatibility with the upstream source, even if there are potential improvements or syntax issues.

Applied to files:

  • src/test/fixtures.json
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
PR: arkade-os/wallet#114
File: src/lib/asp.ts:0-0
Timestamp: 2025-06-30T18:33:29.839Z
Learning: In src/lib/asp.ts, only the collaborativeExit function should accept both IWallet and ServiceWorkerWallet types. Other wallet functions (getBalance, getTxHistory, getReceivingAddresses, redeemNotes, sendOnChain, settleVtxos) should maintain their original IWallet-only signatures and not be updated for consistency.

Applied to files:

  • package.json
🧬 Code graph analysis (2)
src/test/lib/fiat.test.ts (1)
src/lib/fiat.ts (1)
  • getPriceFeed (8-19)
src/test/lib/lnurl.test.ts (1)
src/lib/lnurl.ts (4)
  • isValidLnUrl (65-65)
  • getCallbackUrl (67-76)
  • checkLnUrlConditions (78-86)
  • fetchInvoice (88-99)
🔇 Additional comments (4)
src/test/lib/fiat.test.ts (1)

11-18: Good: Correctly maps API payload to expected shape.

The test verifies EUR/USD last → lowercase keys, matching src/lib/fiat.ts.

package.json (2)

7-8: SDK/boltz-swap bumps: confirm API compatibility across call sites

These minor bumps look fine. Please re-check any settle/swap flows for signature/behavior changes before release.


34-35: Nice: added vitest UI and coverage scripts

Good quality-of-life additions.

src/test/fixtures.json (1)

127-133: Mixed schema in invalid vectors; ensure the test harness accepts address+options without uri.

This case omits uri but provides address and options. Verify the test runner builds the URI from these fields for negative tests; if not, add the explicit uri to match the options.

Apply if needed:

         {
           "exception": "Invalid amount",
           "address": "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH",
+          "uri": "bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=-1",
           "options": {
             "amount": "-1"
           }
         },

Comment thread package.json
"decimal.js": "^10.5.0",
"dexie": "^4.0.11",
"dexie-react-hooks": "^1.1.7",
"dompurify": "^3.2.6",

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.

🛠️ Refactor suggestion

DOMPurify added — enforce strict sanitization config at call sites

Ensure sanitize() is called with a restrictive config (empty ALLOWED_TAGS/ATTR, forbid on* attrs, disallow unknown protocols).

🤖 Prompt for AI Agents
In package.json around line 20, DOMPurify was added but call sites must enforce
a strict sanitize config; update every DOMPurify.sanitize(...) invocation to
pass a restrictive options object such as ALLOWED_TAGS: [], ALLOWED_ATTR: [],
FORBID_ATTR: ['on*'] and set ALLOW_UNKNOWN_PROTOCOLS: false (or use
ALLOWED_URI_REGEXP to whitelist protocols) so event handler attributes and
unknown protocols are disallowed; ensure all sanitize calls use this config (or
a shared wrapper utility) rather than the default permissive behavior.

Comment thread src/test/fixtures.json
Comment on lines +182 to +191
"privatekey": {
"secret": {
"nsec": "nsec13374vlgh58p4sw9nrdc72rmf40x62qgvk6jac8rekwm872p0esesxqx6t5",
"hex": "8c7d567d17a1c35838b31b71e50f69abcda5010cb6a5dc1c79b3b67f282fcc33"
},
"public": {
"npub": "npub1lvnwe70kgqpta7jctmugtrfy8gk9ntc787449833a09356w4f0us7w9wej",
"hex": "fb26ecf9f64002befa585ef8858d243a2c59af1e3fab529e31ebcb1a69d54bf9"
}
}

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

Real-looking Nostr keys in repo; replace with generated test keys or allowlist with justification.

Committed nsec…/npub… pairs can trip secret scanners and cause policy friction, even if test-only. Prefer generating deterministic throwaway keys at test runtime and removing them from fixtures; alternatively, add a scoped secret-scan allowlist with clear rationale.

Option A (recommended): generate per-test keys and reference them instead of fixtures.

// src/test/utils/nostrKeys.ts
import { sha256 } from '@noble/hashes/sha256'
import { bytesToHex } from '@noble/hashes/utils'
import { getPublicKey } from 'nostr-tools' // or noble-secp256k1 if preferred

export const testNostrKeys = (seed = 'arkade-wallet-test'): { seckeyHex: string; pubkeyHex: string } => {
  const seckeyHex = bytesToHex(sha256(seed))
  const pubkeyHex = getPublicKey(seckeyHex)
  return { seckeyHex, pubkeyHex }
}

Option B (interim): add a scanner allowlist limited to this file/keys with a comment explaining they are inert test vectors. I can provide a minimal gitleaks/secretlint rule if you want.

🤖 Prompt for AI Agents
In src/test/fixtures.json around lines 182-191, the file contains real-looking
Nostr nsec/npub key pairs that can trigger secret scanners; replace them by
either (A) removing these hardcoded keys and updating tests to generate
deterministic throwaway keys at runtime (e.g., derive a seckey via a fixed test
seed and compute its pubkey) and reference those generated values in tests, or
(B) if immediate change is infeasible, add a repository scanner allowlist entry
scoped to this file/these exact values with a brief comment stating they are
inert test vectors and why, then commit that allowlist rule; update tests/docs
accordingly.

@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: 3

🧹 Nitpick comments (5)
src/components/NeedsPassword.tsx (3)

40-46: Nit: improve copy.

“Insert password” reads odd in English; “Enter password” is more idiomatic.

Apply this diff:

-                label='Insert password'
+                label='Enter password'

47-47: Show errors for both flows (biometrics and password).

ErrorMessage is only rendered in the password branch; biometric failures won’t surface to users.

Apply this diff to render the error below the conditional:

-              <ErrorMessage text={error} error={Boolean(error)} />
-            </FlexCol>
-          )}
+            </FlexCol>
+          )}
+          <ErrorMessage text={error} error={Boolean(error)} />

25-25: Biometric UX: avoid double-trigger and surface failure inline.

Consider guarding against rapid re-clicks and showing a user-visible message on catch (e.g., local error state rendered via ErrorMessage). Right now failures only log to console.

src/test/screens/wallet/unlock.test.tsx (2)

22-22: Prefer accessible queries for the button

Use role+name to reduce brittleness against copy changes and ensure a11y alignment.

Apply this diff:

-    const button = screen.getByText('Unlock wallet')
+    const button = screen.getByRole('button', { name: /unlock wallet/i })

7-10: Remove unused PWA mock in unlock.test.tsx
src/test/screens/wallet/unlock.test.tsx (lines 7–10) defines and mocks pwaCanInstall, but Unlock.tsx never imports lib/pwa—delete these lines or relocate the mock to a shared setup if other wallet tests need it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 84fd37d and 817832e.

📒 Files selected for processing (3)
  • src/components/NeedsPassword.tsx (3 hunks)
  • src/test/screens/wallet/unlock.test.tsx (1 hunks)
  • src/test/setup.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/setup.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-06T09:53:28.428Z
Learnt from: bordalix
PR: arkade-os/wallet#134
File: src/screens/Settings/Password.tsx:34-39
Timestamp: 2025-08-06T09:53:28.428Z
Learning: In src/screens/Settings/Password.tsx, the saveNewPassword function doesn't need error handling for getPrivateKey because it's only called after authentication is verified. The useEffect validates the oldPassword using isValidPassword() which internally calls getPrivateKey(), and only sets authenticated=true if successful. The saveNewPassword function has a guard clause that returns early if not authenticated, so getPrivateKey(oldPassword) is guaranteed to work when called.

Applied to files:

  • src/components/NeedsPassword.tsx
📚 Learning: 2025-07-21T18:25:36.391Z
Learnt from: bordalix
PR: arkade-os/wallet#121
File: src/screens/Init/Restore.tsx:58-58
Timestamp: 2025-07-21T18:25:36.391Z
Learning: In src/screens/Init/Restore.tsx, the Input component for private key restoration intentionally omits the `value` prop to make it uncontrolled. This is a security feature to prevent the private key from being displayed if users navigate away and return to the screen, avoiding potential exposure of sensitive data in the UI.

Applied to files:

  • src/components/NeedsPassword.tsx
🧬 Code graph analysis (2)
src/test/screens/wallet/unlock.test.tsx (2)
src/lib/pwa.ts (1)
  • pwaCanInstall (5-5)
src/screens/Wallet/Unlock.tsx (1)
  • Unlock (10-36)
src/components/NeedsPassword.tsx (8)
src/lib/biometrics.ts (1)
  • authenticateUser (71-98)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/icons/Lock.tsx (1)
  • LockIcon (1-11)
src/components/Text.tsx (1)
  • Text (25-79)
src/components/CenterScreen.tsx (1)
  • CenterScreen (9-28)
src/components/FlexCol.tsx (1)
  • FlexCol (15-40)
src/components/InputPassword.tsx (1)
  • InputPassword (15-41)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
🔇 Additional comments (4)
src/components/NeedsPassword.tsx (3)

35-36: LGTM: icon/text tweaks for biometrics.

Lock icon + centered text looks consistent with the new pattern.


3-3: LGTM: ErrorMessage import.

Matches the component’s default export from ./Error.


14-14: LGTM: LockIcon import.

Icon component and usage align with src/icons/Lock.tsx.

src/test/screens/wallet/unlock.test.tsx (1)

21-27: Baseline render assertions look good

The test validates the screen title, primary action, and password field presence — solid smoke coverage for initial render.

}

const handleBiometrics = () => authenticateUser(wallet.passkeyId).then(onPassword).catch(consoleError)
const handleChange = (ev: any) => setPassword(ev.target.value)

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.

💡 Verification agent

🧩 Analysis chain

Fix Ionic event usage: use ev.detail.value, not ev.target.value.

With IonInput, onIonInput emits a CustomEvent; target.value will be undefined, breaking password capture.

Apply this diff:

-  const handleChange = (ev: any) => setPassword(ev.target.value)
+  const handleChange = (ev: any) => setPassword(ev.detail?.value ?? '')

Run to find similar issues across the repo:


🏁 Script executed:

#!/bin/bash
# Look for likely misuses of Ionic input values
rg -nP --type=tsx -C2 '\bonIonInput\b|\bev\.(target|currentTarget)\.value\b' src

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Search for direct ev.target.value or ev.currentTarget.value usage in TSX files
rg -nP 'ev\.(target|currentTarget)\.value' -g '*.tsx' src
# Search for onIonInput usage in TSX files
rg -nP 'onIonInput' -g '*.tsx' src

Length of output: 465


Fix Ionic input event handling in NeedsPassword.tsx
Change to use ev.detail?.value instead of ev.target.value to correctly capture the input value.

--- a/src/components/NeedsPassword.tsx
+++ b/src/components/NeedsPassword.tsx
@@ 26
-  const handleChange = (ev: any) => setPassword(ev.target.value)
+  const handleChange = (ev: any) => setPassword(ev.detail?.value ?? '')
📝 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 handleChange = (ev: any) => setPassword(ev.target.value)
const handleChange = (ev: any) => setPassword(ev.detail?.value ?? '')
🤖 Prompt for AI Agents
In src/components/NeedsPassword.tsx around line 26, the input handler currently
reads the value from ev.target.value which doesn't work with Ionic's IonInput;
update the handler to read the value from ev.detail?.value (and adjust the event
parameter type to the appropriate Ionic input event type if desired) and pass
that value into setPassword so the component captures user input correctly.

Comment on lines +12 to +14
describe('Unlock screen', () => {
const user = userEvent.setup()
it('renders the unlock screen with the correct elements', async () => {

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

Don’t mutate DOM directly; simulate real typing with user-event

Assigning input.value bypasses React’s onChange pipeline and can yield false positives. Use user.type you already set up.

Apply this diff:

-  const user = userEvent.setup()
+  const user = userEvent.setup()
@@
-    input.value = 'testpassword'
-    expect(input).toHaveValue('testpassword')
+    await user.type(input, 'testpassword')
+    expect(input).toHaveValue('testpassword')

Also applies to: 28-29

🤖 Prompt for AI Agents
In src/test/screens/wallet/unlock.test.tsx around lines 12-14 (and similarly
lines 28-29), the test mutates the input by assigning to input.value which
bypasses React's onChange handling; replace direct DOM mutation with the
user-event API by calling await user.type(targetInputElement, 'yourPassword')
(and remove the input.value assignment), ensuring you await the typing and
trigger any submit or blur needed so React handlers run as in real user
interaction.

Comment on lines +15 to +19
render(
<IonApp>
<Unlock />
</IonApp>,
)

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.

💡 Verification agent

🧩 Analysis chain

Stabilize Unlock side effects (initWallet/navigate/getPrivateKey) to avoid flakiness

Unlock triggers async work on mount via useEffect (default password → getPrivateKey → initWallet → navigate). Without explicit mocks/providers, the test can navigate away or throw depending on defaults. Recommend mocking these dependencies or wrapping with providers that no-op.

Run this script to locate the exact import paths to mock and check whether the contexts provide safe defaults:

Once you have the paths, add top-level mocks like:

  • mock getPrivateKey to vi.fn().mockRejectedValueOnce(new Error("test")) (so the screen stays put), or
  • provide NavigationContext.Provider with { navigate: vi.fn() } and WalletContext.Provider with { initWallet: vi.fn() }.

I can wire a minimal provider wrapper once we confirm the context module paths.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Imports in Unlock.tsx for side-effectful deps:"
rg -nP 'import.+(getPrivateKey|defaultPassword|NavigationContext|WalletContext)' src/screens/Wallet/Unlock.tsx -C2 || true

echo
echo "Context defaults (look for createContext initial values):"
rg -nP 'createContext\\s*\\(' -n src -C2 | rg -nP 'Wallet|Navigation' -C2 || true

Length of output: 1023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "WalletContext createContext call:"
rg -nP 'createContext' -n src/providers/wallet

echo
echo "NavigationContext createContext call:"
rg -nP 'createContext' -n src/providers/navigation

Length of output: 204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "List src/providers directory structure:"
fd -t d -d 1 src/providers || true

echo
echo "Find files under src/providers containing WalletContext or NavigationContext:"
rg -lP 'WalletContext|NavigationContext' -n src/providers || true

echo
echo "Show createContext lines in those files:"
for f in $(rg -lP 'WalletContext|NavigationContext' -n src/providers); do
  echo "$f:"
  rg -nP 'createContext' -n "$f" || true
done

Length of output: 3061


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "WalletContext default value:"
sed -n '42,60p' src/providers/wallet.tsx

echo
echo "NavigationContext default value:"
sed -n '154,180p' src/providers/navigation.tsx

Length of output: 1422


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' src/screens/Wallet/Unlock.tsx || true

Length of output: 1202


Mock getPrivateKey in unlock.test.tsx
Stub out the real getPrivateKey call to avoid async side effects—e.g.:

vi.mock('../../lib/privateKey', () => ({
  getPrivateKey: vi.fn().mockResolvedValue('stub-key'),
}));

Both WalletContext and NavigationContext use no-op defaults, so no provider wrappers are required.

🤖 Prompt for AI Agents
In src/test/screens/wallet/unlock.test.tsx around lines 15 to 19, the test
renders Unlock without stubbing the real getPrivateKey which can cause async
side-effects; add a vi.mock for the '../../lib/privateKey' module at the top of
the test file that exports getPrivateKey as a
vi.fn().mockResolvedValue('stub-key') so the component receives a predictable
key and the test remains synchronous; no WalletContext or NavigationContext
providers are required since they have no-op defaults.

@bordalix

bordalix commented Oct 9, 2025

Copy link
Copy Markdown
Collaborator Author

Substituted by #148

@bordalix bordalix closed this Oct 9, 2025
@bordalix
bordalix deleted the v8 branch October 15, 2025 13:52
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.

4 participants