Skip to content

refactor nostr backups to use chunks - #235

Merged
bordalix merged 13 commits into
masterfrom
nostr_backup_chunks
Nov 21, 2025
Merged

bordalix merged 13 commits into
masterfrom
nostr_backup_chunks

Conversation

@bordalix

@bordalix bordalix commented Nov 17, 2025 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Dependencies

    • Updated @arkade-os/boltz-swap to 0.2.9
  • New Features

    • Centralized Nostr backup/restore provider and new backupConfig API
    • Restore flow shows a loading state; settings can trigger full backups
    • Chat support integration and a new Support screen
    • Input components accept optional name attributes
    • Added local relayer Dockerfile/service
  • Tests

    • New e2e tests for backup, reset, and restore flows
  • Bug Fixes

    • Improved backup error messages and resilience

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Nov 17, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Refactors Nostr backup/restore to a BackupProvider class (pubkey/seckey init), updates NostrStorage to pool-based publish/load and per-event decryption, integrates provider across lightning, config, and UI, adds Playwright e2e tests and NAK Docker service, and bumps @arkade-os/boltz-swap to 0.2.9. (50 words)

Changes

Cohort / File(s) Summary
Dependency
package.json
Bumped @arkade-os/boltz-swap from 0.2.8 → 0.2.9.
Backup provider
src/lib/backup.ts
Added BackupProvider class and NostrStorageData type; methods: backupConfig, backupReverseSwap, backupSubmarineSwap, fullBackup, restore, private loadData; implements multi-event split/merge, deduplication, deterministic ordering, and error handling; removed legacy function-based handlers.
Nostr storage layer
src/lib/nostr.ts
Converted NostrStorage to use a persistent SimplePool, internalized nostrAppName, changed API to save(payload: string) and load(): Promise<Event[]>, added per-event decryptEvent, adjusted tag/filtering and publish/load flows.
Lightning integration
src/lib/lightning.ts
Injected BackupProvider into LightningSwapProvider; backup now accepts a swap and routes to backupSubmarineSwap/backupReverseSwap via type guards; added someError helper; updated call sites to pass swap and improved error messaging.
Config provider
src/providers/config.tsx
Exposed backupConfig(config): Promise<void> via ConfigContextProps; updateConfig signature simplified to updateConfig(config); replaced inline nostr calls with BackupProvider.backupConfig.
UI — Restore screen
src/screens/Init/Restore.tsx
Replaced nostr restore call with new BackupProvider({ seckey }).restore(updateConfig); added restoring state, Loading render branch, and finalization/navigation in finally.
UI — Settings / Backup
src/screens/Settings/Backup.tsx
Toggle enabling backup now instantiates BackupProvider and calls fullBackup(config) on enable; added async error handling, runtime error state and ErrorMessage UI.
Inputs — name prop passthrough
src/components/Input.tsx, src/components/InputWithScanner.tsx, src/components/InputAddress.tsx, src/screens/Wallet/Send/Form.tsx
Added optional name?: string prop to inputs, forwarded to IonInput; set name='send-address' in Send form.
E2E tests
src/test/e2e/nostr.test.ts
New Playwright e2e tests validating nostr backup/restore flows across wallet reset and UI flows, and swap backup persistence.
Playwright config
playwright.config.ts
Prefixed webServer command with VITE_NOSTR_RELAY_URL=ws://localhost:10547 pnpm start; minor formatting changes.
Devops — Docker / Compose
nak.Dockerfile, test.docker-compose.yml
Added nak.Dockerfile multi-stage build and added nak service to test.docker-compose.yml (exposes port 10547 for local nostr relay).
Setup / Test harness
src/test/setup.mjs
Added setupNostr step to start Nost relayer container during test setup and integrated it into overall setup flow.
Chatwoot / Support
src/lib/chatwoot.ts, src/components/ChatWoot.tsx, src/screens/Settings/Support.tsx, src/icons/Support.tsx, src/providers/options.tsx, README.md
Added Chatwoot integration library, widget component, Support screen and icon, support menu option, and README env var docs.
Settings / various handlers
multiple files under src/screens/* and src/providers/*
Reworked settings change handlers and provider usages to call backupConfig(newConfig) when config.nostrBackup is enabled; switched call sites to single-argument updateConfig(config).
UI padding tweaks
src/components/Menu.tsx, src/components/Select.tsx, src/screens/Settings/General.tsx
Increased vertical padding from 0.5rem → 0.8rem in several menu/select/row elements.
Misc small changes
src/components/Balance.tsx, src/test/e2e/swap.test.ts, src/icons/*
Adjusted updateConfig callsites to single-arg, replaced some Playwright locators for Boltz header, added Support icon component.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Lightning as LightningSwapProvider
    participant Backup as BackupProvider
    participant Nostr as NostrStorage
    participant Pool as SimplePool

    User->>Lightning: createReverseSwap()/createSubmarineSwap()
    Lightning->>Lightning: build pendingSwap
    Lightning->>Backup: backup(pendingSwap)
    alt reverse swap
        Backup->>Backup: backupReverseSwap(pendingSwap)
    else submarine swap
        Backup->>Backup: backupSubmarineSwap(pendingSwap)
    end
    Backup->>Nostr: save(payload string)
    Nostr->>Pool: publish event(s)
    Pool-->>Nostr: publish success
    Nostr-->>Backup: resolved
    Backup-->>Lightning: resolved
    Lightning-->>User: operation complete
Loading
sequenceDiagram
    autonumber
    participant User
    participant UI as Restore Screen
    participant Backup as BackupProvider
    participant Nostr as NostrStorage
    participant Pool as SimplePool

    User->>UI: trigger restore with seckey
    UI->>UI: set restoring = true (show Loading)
    UI->>Backup: restore(updateConfig)
    Backup->>Nostr: load()
    Nostr->>Pool: subscribe for events
    Pool-->>Nostr: events[]
    Nostr->>Nostr: decrypt & merge events → Event[]
    Nostr-->>Backup: return Event[]
    Backup->>UI: call updateConfig(config)
    UI->>UI: set restoring = false (navigate)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to:
    • BackupProvider: multi-event split/merge, deterministic ordering, deduplication and JSON parsing/error handling.
    • NostrStorage: pool lifecycle, per-event encryption/decryption, and changed method signatures (save/load).
    • Call-site propagation: updateConfig signature changes and integration of backupConfig across many UI/provider handlers.
    • New e2e tests and test environment assumptions (nostr relay, docker compose additions).

Possibly related PRs

Suggested reviewers

  • louisinger
  • tiero

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring effort—restructuring Nostr backups to handle data in chunks rather than monolithic blocks, which is the primary theme across the modified backup files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nostr_backup_chunks

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

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 17, 2025 •

Copy link
Copy Markdown

Deploying wallet-signet with  Cloudflare Pages  Cloudflare Pages

Latest commit: f9bd623
Status: ✅  Deploy successful!
Preview URL: https://cd2f58d9.wallet-23u.pages.dev
Branch Preview URL: https://nostr-backup-chunks.wallet-23u.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 17, 2025 •

Copy link
Copy Markdown

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: f9bd623
Status: ✅  Deploy successful!
Preview URL: https://557875d7.arkade-wallet.pages.dev
Branch Preview URL: https://nostr-backup-chunks.arkade-wallet.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 17, 2025 •

Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0adaa60
Status: ✅  Deploy successful!
Preview URL: https://0d663882.wallet-bitcoin.pages.dev
Branch Preview URL: https://nostr-backup-chunks.wallet-bitcoin.pages.dev

View logs

@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

Caution

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

⚠️ Outside diff range comments (2)
src/providers/config.tsx (1)

21-41: Config context type for updateConfig should reflect async implementation

updateConfig is now declared as async, so its actual type is (config: Config, backup: boolean) => Promise<void>, but ConfigContextProps.updateConfig (and the default context value) still declare it as returning void. This is likely a TypeScript type error when passing updateConfig into the provider.

Recommend updating the type and default to match:

 interface ConfigContextProps {
   config: Config
   configLoaded: boolean
   resetConfig: () => void
   setConfig: (c: Config) => void
   showConfig: boolean
   toggleShowConfig: () => void
-  updateConfig: (config: Config, backup: boolean) => void
+  updateConfig: (config: Config, backup: boolean) => Promise<void>
   useFiat: boolean
 }

 export const ConfigContext = createContext<ConfigContextProps>({
   config: defaultConfig,
   configLoaded: false,
   resetConfig: () => {},
   setConfig: () => {},
   showConfig: false,
   toggleShowConfig: () => {},
-  updateConfig: () => {},
+  updateConfig: async () => {},
   useFiat: false,
 })

Call sites that don’t need to await it can continue to ignore the returned promise.

src/lib/nostr.ts (1)

81-117: Close subscription on timeout to avoid leaking relay subscriptions

The timeout handler resolves [] without closing the active subscription. If relays never send EOSE, repeated calls accumulate dangling subscriptions and open relay connections.

The fix is valid: move sub to outer scope and call sub?.close() in the timeout handler (line 115), mirroring the cleanup in oneose() (line 103).

(Note: The secondary reference "Also applies to: 142-150" is incorrect—those lines contain unrelated methods; only lines 81-117 have the subscription issue.)

🧹 Nitpick comments (5)
src/lib/backup.ts (2)

75-97: Consider factoring out size limit and guarding partial failures in fullBackup

The fullBackup implementation is correct in terms of splitting large backups into multiple events, but two improvements would make it more robust:

  • Extract the 65000 threshold into a named constant (e.g. MAX_NOSTR_PAYLOAD_SIZE) shared with any other Nostr backup logic, so the limit is easy to tune and documented.
  • In the split path, any failure mid‑loop will stop backing up remaining swaps; depending on expectations, you may want to wrap each backup* call in its own try/catch and log errors per‑swap while continuing, or abort early with a clear error message.

Not strictly required for correctness, but would harden the backup process against transient failures.


103-117: restore API shape is fine; type of updateConfig could be widened

restore correctly calls updateConfig(config, false) before replaying swap collections, and then writes swaps via contractRepo. Given updateConfig is now async in the config provider, you might want to widen this parameter’s type to accept Promise<void> (e.g. (config: Config, backup: boolean) => void | Promise<void>) or at least document that callers may pass an async function whose promise will be ignored. Functionally fine as‑is because backup is always false in restore flows so no async work is actually awaited.

src/screens/Settings/Backup.tsx (1)

74-81: ErrorMessage integration is correct; consider clearing stale errors on successful backup

Rendering <ErrorMessage error={Boolean(error)} text={error} /> here correctly surfaces both password and backup failures. To avoid showing stale error messages after a subsequent successful backup, you might want to clear error (e.g. setError('')) on success inside toggleNostrBackup.

src/screens/Init/Restore.tsx (1)

53-63: Restore flow always navigates to success screen, even on failure

The new restore logic:

new BackupProvider({ seckey: privateKey! })
  .restore(updateConfig)
  .catch((err) => consoleError(err, 'Error restoring from nostr'))
  .finally(() => {
    setRestoring(false)
    navigate(Pages.InitSuccess)
  })

will navigate to InitSuccess regardless of whether restore succeeds or throws. That means a failed Nostr restore still lands the user on a “success” screen, with only a log entry indicating failure.

If prior behaviour was to only navigate on success, consider moving navigate(Pages.InitSuccess) into the success path and surfacing a user‑visible error (e.g. via setError) on failure. If always navigating is intentional, a small text change on the destination screen clarifying that restore may have partially succeeded could help avoid confusion.

src/providers/config.tsx (1)

53-68: Backup integration in updateConfig is correct; consider gating on nostrBackup and handling missing pubkey

The async updateConfig flow is generally good: it normalizes aspUrl, updates state and theme, persists to storage, and only then (optionally) performs a Nostr backup via BackupProvider.backupConfig(config).

Two potential refinements:

  1. Respect the global nostrBackup flag
    Currently, backups run whenever backup is true, regardless of config.nostrBackup. If the intent is “backup only when the user has Nostr backups enabled”, you might want:

    if (backup && config.nostrBackup) {
      const backupProvider = new BackupProvider({ pubkey: config.pubkey })
      await backupProvider.backupConfig(config).catch((error) => {
        consoleError(error, 'Backup to Nostr failed')
      })
    }
  2. Guard against missing/invalid pubkey
    BackupProvider will throw on an invalid pubkey (length not 64 or 66). If there’s any path where updateConfig could be called with backup === true before config.pubkey is populated, you may want to add a simple precondition check (e.g. if (!config.pubkey) return;) or log a clearer error.

These aren’t correctness blockers, but they’ll help align behaviour with user expectations and make failures easier to reason about.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bb383f and e239f23.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • package.json (1 hunks)
  • src/lib/backup.ts (1 hunks)
  • src/lib/lightning.ts (5 hunks)
  • src/lib/nostr.ts (5 hunks)
  • src/providers/config.tsx (3 hunks)
  • src/screens/Init/Restore.tsx (3 hunks)
  • src/screens/Settings/Backup.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-06T09:53:28.428Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/screens/Settings/Backup.tsx
  • src/screens/Init/Restore.tsx
📚 Learning: 2025-07-21T18:25:36.391Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/screens/Settings/Backup.tsx
  • src/screens/Init/Restore.tsx
📚 Learning: 2025-07-07T14:41:35.991Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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:

  • package.json
🧬 Code graph analysis (5)
src/screens/Settings/Backup.tsx (3)
src/lib/backup.ts (1)
  • BackupProvider (16-161)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
src/lib/lightning.ts (3)
src/lib/types.ts (1)
  • Config (8-23)
src/lib/backup.ts (1)
  • BackupProvider (16-161)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/lib/nostr.ts (1)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/lib/backup.ts (2)
src/lib/types.ts (1)
  • Config (8-23)
src/lib/nostr.ts (1)
  • NostrStorage (9-151)
src/screens/Init/Restore.tsx (3)
src/lib/backup.ts (1)
  • BackupProvider (16-161)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/components/Loading.tsx (1)
  • Loading (5-18)
⏰ 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: test
🔇 Additional comments (8)
package.json (1)

7-7: boltz-swap patch bump looks fine; confirm compatibility with new backup flow

The version bump to @arkade-os/boltz-swap@0.2.9 is reasonable for this PR’s backup refactor, assuming no breaking changes between 0.2.8 and 0.2.9. Please double‑check the package’s changelog/release notes for any behavioural changes that might affect PendingReverseSwap / PendingSubmarineSwap shapes used in src/lib/backup.ts.

src/lib/backup.ts (2)

27-41: Constructor seckey/pubkey handling is consistent with NostrStorage usage

The constructor’s handling of seckey vs pubkey, including 0x‑prefixed hex normalization and length checks, matches the expectations in NostrStorage and should behave correctly for both backup‑only (pubkey) and restore (seckey) flows.


47-68: Backup helpers are straightforward and type-safe

backupConfig, backupReverseSwap, and backupSubmarineSwap each wrap a minimal NostrStorageData envelope and delegate to nostrStorage.save, which keeps the payload shape simple and consistent with loadData. No issues here.

src/screens/Init/Restore.tsx (2)

32-49: New restoring state and key parsing flow look good

The additional restoring flag combined with the existing key parsing logic (nsecToPrivateKey / hex.decode / invalidPrivateKey) and disabled button state keeps the restore UX coherent while preventing multiple submissions. Input remains uncontrolled, which preserves the intended security property from earlier work. Based on learnings


67-67: Loading indicator gating on restoring is appropriate

The early return if (restoring) return <Loading text='Restoring wallet...' /> correctly blocks interaction during restore and avoids multiple submissions. No issues here.

src/lib/nostr.ts (2)

5-5: Consistent app tagging for backup events looks good

Using a single nostrAppName constant for both publish (['t', nostrAppName]) and subscribe filters ('#t': [nostrAppName]) is a clean way to scope backup events to this app and avoid cross-app collisions. No issues here.

Also applies to: 54-55, 92-93


13-13: Pool reuse and raw string payloads are reasonable

Initializing a single SimplePool per NostrStorage instance and reusing it for save avoids repeated connection setup and is a good direction. Taking a raw string payload (with callers handling JSON serialization) also keeps this class focused on transport concerns instead of data shape. The publish timeout guard is appropriate for preventing hangs.

Also applies to: 23-23, 46-47, 57-58, 65-65

src/lib/lightning.ts (1)

9-11: Backup routing by swap type is sound and keeps failures non-fatal

Using isPendingSubmarineSwap / isPendingReverseSwap and re-fetching pending swaps before backing up ensures you persist the freshest view of each swap. Calling backup after creation, claim, creation of submarine swaps, and refund covers the important lifecycle transitions, and the nostrBackup guard plus catch block keep backup failures from impacting swap flows.

Once the constructor concern above is addressed, this backup integration looks solid.

Also applies to: 40-57, 100-101, 118-119, 155-156, 170-171

Comment thread src/lib/backup.ts Resolved
Comment thread src/lib/lightning.ts
Comment on lines +17 to 18
import { BackupProvider } from './backup'
import * as Sentry from '@sentry/react'

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 | 🟠 Major

🧩 Analysis chain

Guard BackupProvider construction to avoid hard failures when Nostr backup is disabled or misconfigured

LightningSwapProvider now always constructs a BackupProvider with config.pubkey. Because BackupProvider validates pubkey length and throws on invalid input, any config with an empty or malformed pubkey will now cause the Lightning provider constructor to fail, even if config.nostrBackup is false and backup would never be used.

To keep Lightning usable when Nostr backup is turned off (or before a pubkey is configured), consider instantiating BackupProvider only when backup is enabled and the pubkey is valid, e.g.:

-  private readonly backupProvider: BackupProvider
+  private readonly backupProvider: BackupProvider | null
@@
-    const indexerProvider = new RestIndexerProvider(aspInfo.url)
-    this.backupProvider = new BackupProvider({ pubkey: config.pubkey })
+    const indexerProvider = new RestIndexerProvider(aspInfo.url)
+    this.backupProvider = config.nostrBackup ? new BackupProvider({ pubkey: config.pubkey }) : null

and then in backup:

-  private backup = async (swap: PendingReverseSwap | PendingSubmarineSwap) => {
-    if (!this.config.nostrBackup) return
+  private backup = async (swap: PendingReverseSwap | PendingSubmarineSwap) => {
+    if (!this.config.nostrBackup || !this.backupProvider) return

This preserves the new behavior when backup is enabled but avoids introducing a new hard dependency on a valid Nostr pubkey for all Lightning usage.

Also applies to: 25-26, 32-32


🏁 Script executed:

#!/bin/bash
# Examine the current state of lightning.ts around the identified lines and constructor
fd -t f 'lightning\.ts$' | head -5

Length of output: 20


🏁 Script executed:

#!/bin/bash
# Look at the constructor and backupProvider initialization
rg -n 'class LightningSwapProvider|backupProvider|BackupProvider' src/lib/lightning.ts -A 3 -B 1

Length of output: 1294


🏁 Script executed:

#!/bin/bash
# Find the BackupProvider implementation to understand validation
fd -t f 'backup\.ts$' -o -name '*backup*' | grep -v node_modules | head -10

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Check the backup() method and its call sites
rg -n 'private backup|createReverseSwap|claimVHTLC|createSubmarineSwap|refundVHTLC' src/lib/lightning.ts -B 2 -A 5

Length of output: 2574


🏁 Script executed:

#!/bin/bash
# Find backup file(s) more carefully
find src -type f -name '*backup*' 2>/dev/null

Length of output: 17


🏁 Script executed:

#!/bin/bash
# Search for BackupProvider class definition
rg -n 'class BackupProvider|export.*BackupProvider' -A 10

Length of output: 716


🏁 Script executed:

#!/bin/bash
# Check the Config type definition
rg -n 'type Config|interface Config' src/lib/types.ts -A 15

Length of output: 311


🏁 Script executed:

#!/bin/bash
# Look at the BackupProvider constructor to see validation logic
rg -n 'constructor|seckey|pubkey.*=' src/lib/backup.ts -A 5 -B 2 | head -50

Length of output: 1255


🏁 Script executed:

#!/bin/bash
# Find where BackupProvider throws or validates
rg -n 'throw|if.*pubkey|pubkey.*length' src/lib/backup.ts -B 2 -A 2

Length of output: 915


🏁 Script executed:

#!/bin/bash
# Check if config.pubkey is validated or checked before line 32 in lightning.ts
rg -n 'config\.pubkey' src/lib/lightning.ts -B 5 -A 2

Length of output: 462


Fix BackupProvider instantiation to prevent constructor failure when Nostr backup is disabled or pubkey is invalid

BackupProvider validates pubkey length and throws on invalid input (lines 35, 39 of src/lib/backup.ts). Currently, LightningSwapProvider unconditionally constructs a BackupProvider at line 32 with config.pubkey, causing the entire Lightning provider to fail during construction if the pubkey is empty or malformed—even when config.nostrBackup is false.

The backup() method's guard at line 41 (if (!this.config.nostrBackup) return) only protects the backup operations, not the constructor. To keep Lightning usable when backup is disabled, gate BackupProvider instantiation:

-  private readonly backupProvider: BackupProvider
+  private readonly backupProvider: BackupProvider | null
   
   constructor(apiUrl: string, aspInfo: AspInfo, wallet: Wallet | ServiceWorkerWallet, config: Config) {
     const indexerProvider = new RestIndexerProvider(aspInfo.url)
-    this.backupProvider = new BackupProvider({ pubkey: config.pubkey })
+    this.backupProvider = config.nostrBackup ? new BackupProvider({ pubkey: config.pubkey }) : null

and update the backup method guard:

   private backup = async (swap: PendingReverseSwap | PendingSubmarineSwap) => {
-    if (!this.config.nostrBackup) return
+    if (!this.config.nostrBackup || !this.backupProvider) return

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/lib/lightning.ts around lines 17–41, the code currently constructs
BackupProvider unconditionally which throws if config.nostrBackup is false or
config.pubkey is empty/invalid; change this so BackupProvider is only
instantiated when this.config.nostrBackup is true and the pubkey passes the
length/format check (e.g., non-empty and expected length) — make the
BackupProvider field optional/nullable, initialize it inside the constructor
only when nostrBackup is enabled and pubkey is valid, and update the backup()
method guard to first check that this.config.nostrBackup is true AND that
this.backupProvider (or equivalent) exists before calling into it.

Comment on lines +51 to 63
const toggleNostrBackup = async () => {
const newConfig = { ...config, nostrBackup: !config.nostrBackup }
if (newConfig.nostrBackup) {
const backupProvider = new BackupProvider({ pubkey: config.pubkey })
await backupProvider.fullBackup(newConfig).catch((error) => {
consoleError(error, 'Backup to Nostr failed')
setError('Backup to Nostr failed')
return
})
} else {
updateConfig(newConfig, true)
}
present(backupToNostr)

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 | 🔴 Critical

Enabling Nostr backup doesn’t persist config change; disabling still triggers backup

There are a couple of behavioural issues in toggleNostrBackup:

  1. When enabling (newConfig.nostrBackup === true), you call fullBackup(newConfig) but never call updateConfig(newConfig, ...), so:

    • config.nostrBackup in context remains unchanged;
    • the toggle will not stay “on” after the re-render or app restart.
  2. When disabling (newConfig.nostrBackup === false), you call updateConfig(newConfig, true), which still triggers a backup via BackupProvider.backupConfig, even though the user just turned Nostr backups off.

  3. Errors thrown synchronously by new BackupProvider({ pubkey: config.pubkey }) (e.g. invalid/missing pubkey) are not caught by the .catch attached to fullBackup, so they will surface as unhandled rejections and won’t set the error state.

A more consistent flow would be:

  • Always persist the new config (in both branches).
  • Only perform a backup when enabling (or when explicitly desired).
  • Wrap the whole block in try/catch so all errors are handled uniformly and the toast isn’t shown on failure.

For example:

-  const toggleNostrBackup = async () => {
-    const newConfig = { ...config, nostrBackup: !config.nostrBackup }
-    if (newConfig.nostrBackup) {
-      const backupProvider = new BackupProvider({ pubkey: config.pubkey })
-      await backupProvider.fullBackup(newConfig).catch((error) => {
-        consoleError(error, 'Backup to Nostr failed')
-        setError('Backup to Nostr failed')
-        return
-      })
-    } else {
-      updateConfig(newConfig, true)
-    }
-    present(backupToNostr)
-  }
+  const toggleNostrBackup = async () => {
+    const newConfig = { ...config, nostrBackup: !config.nostrBackup }
+    try {
+      if (newConfig.nostrBackup) {
+        const backupProvider = new BackupProvider({ pubkey: config.pubkey })
+        await backupProvider.fullBackup(newConfig)
+      }
+      await updateConfig(newConfig, false)
+      setError('')
+      present(backupToNostr)
+    } catch (error) {
+      consoleError(error, 'Backup to Nostr failed')
+      setError('Backup to Nostr failed')
+    }
+  }

Adjust the backup flag in updateConfig according to the semantics you want, but the key is to ensure the config change is always persisted and backups match the user’s intent.

🤖 Prompt for AI Agents
In src/screens/Settings/Backup.tsx around lines 51 to 63, toggleNostrBackup
currently only persists config when disabling, triggers an unwanted backup when
disabling, and does not catch synchronous errors from BackupProvider; change it
to always persist the newConfig (call updateConfig(newConfig, <backupFlag>) in
both branches), set the updateConfig backup flag to true only when you intend to
perform a backup (false when disabling), run the actual Nostr backup only when
enabling (call BackupProvider.fullBackup after persisting), wrap the entire
enable/backup flow in try/catch so both constructor errors and async errors are
caught and setError/consoleError, and only call present(backupToNostr) after the
backup succeeds (or immediately after persisting when no backup is requested).

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

Is it possible to add a "restore" e2e test using nak (nak serve starts a nostr relay "inmemory" so easy setup).

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

Caution

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

⚠️ Outside diff range comments (1)
src/screens/Settings/Backup.tsx (1)

31-43: Clear error on successful unlock/backup to avoid stale or misleading messages

With the new ErrorMessage at line 73 and NeedsPassword still consuming the same error state (line 99), stale errors can now leak into the main backup view:

  • After a wrong password, setError('Invalid password') is called in the catch block (lines 37‑41), but error is never cleared on a subsequent successful getPrivateKey call. Once nsec is set, the main view renders and still shows “Invalid password”.
  • Similarly, after a transient backup failure, “Backup to Nostr failed” will remain visible even after later successful backups unless you reset error.

Consider explicitly clearing error on success in both flows, e.g.:

  useEffect(() => {
    const pass = password ? password : defaultPassword
    getPrivateKey(pass)
      .then((privateKey) => {
+        setError('')
         setNsec(privateKeyToNsec(privateKey))
      })
      .catch((err) => {
        if (password) {
          consoleError(err, 'error unlocking wallet')
          setError('Invalid password')
        }
      })
  }, [password])

(and as suggested in toggleNostrBackup, call setError('') in the success path there as well).

Also applies to: 73-73, 99-99

♻️ Duplicate comments (2)
src/lib/backup.ts (1)

127-166: loadData’s merge strategy and JSON.parse error handling look robust

The new loadData implementation addresses the previous hard‑failure risk:

  • Events are sorted by created_at for deterministic replay.
  • JSON.parse is wrapped in a try/catch and logs via consoleError, so a single malformed event no longer aborts the whole restore.
  • Config uses “last write wins”, while swaps are de‑duplicated by id using maps.

This is a solid approach for dealing with mixed legacy and chunked backup events on unreliable relays.

src/screens/Settings/Backup.tsx (1)

51-63: Fix backup flag inversion and wrap the entire Nostr backup flow in try/catch

The current toggleNostrBackup still has the behavioural problems from the earlier review:

  • Line 53: updateConfig(newConfig, !newConfig.nostrBackup) inverts the backup flag, so you request a backup when disabling and explicitly skip it when enabling.
  • Lines 55‑60: only fullBackup’s promise rejection is caught; synchronous errors from new BackupProvider(...) will escape and won’t set error or avoid the success toast.
  • Line 62: present(backupToNostr) runs even if the backup failed.

A safer, more consistent flow is:

  • Always persist newConfig.
  • Only run a Nostr backup when enabling.
  • Treat the whole enable/backup + persist sequence as one try/catch, clearing error on success and setting it on any failure.
  • Only show the success toast when everything succeeds.

For example:

-  const toggleNostrBackup = async () => {
-    const newConfig = { ...config, nostrBackup: !config.nostrBackup }
-    updateConfig(newConfig, !newConfig.nostrBackup)
-    if (newConfig.nostrBackup) {
-      const backupProvider = new BackupProvider({ pubkey: config.pubkey })
-      await backupProvider.fullBackup(newConfig).catch((error) => {
-        consoleError(error, 'Backup to Nostr failed')
-        setError('Backup to Nostr failed')
-        return
-      })
-    }
-    present(backupToNostr)
-  }
+  const toggleNostrBackup = async () => {
+    const newConfig = { ...config, nostrBackup: !config.nostrBackup }
+    try {
+      // Always persist config; do not trigger implicit backup from updateConfig here.
+      await updateConfig(newConfig, false)
+
+      if (newConfig.nostrBackup) {
+        const backupProvider = new BackupProvider({ pubkey: config.pubkey })
+        await backupProvider.fullBackup(newConfig)
+      }
+
+      setError('')
+      present(backupToNostr)
+    } catch (error) {
+      consoleError(error, 'Backup to Nostr failed')
+      setError('Backup to Nostr failed')
+    }
+  }

This ensures config persistence regardless of backup success, avoids unwanted backups on disable, and keeps user feedback consistent.

🧹 Nitpick comments (4)
nak.Dockerfile (1)

2-26: Prefer pinning Go/nak versions and a non-latest runtime image for reproducible builds

The multi‑stage build looks fine functionally, but using golang:1.25.3-alpine, alpine:latest, and go install ...@latest makes builds non‑deterministic and potentially brittle when upstreams change or tags disappear.

Consider pinning explicit, known‑good versions instead (both for the base images and the nak module tag), e.g.:

-FROM golang:1.25.3-alpine AS builder
+FROM golang:1.2x-alpine AS builder  # pin to the Go version you actually support

-# Install nak
-RUN go install github.com/fiatjaf/nak@latest
+# Install a specific nak release for reproducibility
+RUN go install github.com/fiatjaf/nak@vX.Y.Z

-# runtime stage
-FROM alpine:latest
+# runtime stage
+FROM alpine:3.19

Adjust versions to whatever you’re testing against in CI.

test.docker-compose.yml (1)

248-255: Consider adding a restart policy for the nak test service

The nak service definition (build + port + command) is straightforward and matches the Dockerfile, but unlike most other services in this file it has no restart policy.

If you want parity with the rest of the test stack and more resilient local runs, you might add:

  nak:
     build:
       context: .
       dockerfile: nak.Dockerfile
     container_name: nak
+    restart: unless-stopped
     ports:
       - '10547:10547'
     command: ['nak', 'serve', '--hostname', '0.0.0.0', '--port', '10547']

Optional, but can reduce flakiness in iterative testing.

src/lib/backup.ts (2)

28-42: Constructor key handling is sound but duplicates NostrStorage’s validation

The constructor correctly supports both { seckey } and { pubkey } and enforces pubkey length, which matches how NostrStorage works. Right now, though:

  • The key‑validation logic is largely duplicated with NostrStorage’s constructor.
  • seckey is stored on BackupProvider but never used outside constructing NostrStorage.

If you want to reduce duplication, you could consider delegating more of the key normalization/validation to NostrStorage and either dropping the seckey field here or exposing it only if you plan to use it later (e.g. for additional Nostr features). Not urgent, but it would simplify this class a bit.


76-97: Chunking logic is correct; consider parallelizing per-swap saves for large backups

The fullBackup method correctly:

  • Builds a single snapshot of config + swaps.
  • Falls back to per‑piece events when the payload exceeds ~65 KB.
  • Uses maps on restore to de‑duplicate by swap id.

In the chunked branch you send each swap sequentially:

for (const reverseSwap of data.reverseSwaps ?? []) {
  await this.backupReverseSwap(reverseSwap)
}

for (const submarineSwap of data.submarineSwaps ?? []) {
  await this.backupSubmarineSwap(submarineSwap)
}

For many swaps this can add noticeable latency. If NostrStorage.save and the relays can tolerate some parallelism, you could optionally batch with Promise.all:

await Promise.all((data.reverseSwaps ?? []).map((swap) => this.backupReverseSwap(swap)))
await Promise.all((data.submarineSwaps ?? []).map((swap) => this.backupSubmarineSwap(swap)))

Not required, but it would make full backups scale better as history grows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e239f23 and 401b77e.

📒 Files selected for processing (4)
  • nak.Dockerfile (1 hunks)
  • src/lib/backup.ts (1 hunks)
  • src/screens/Settings/Backup.tsx (3 hunks)
  • test.docker-compose.yml (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-06T09:53:28.428Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/screens/Settings/Backup.tsx
📚 Learning: 2025-07-21T18:25:36.391Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/screens/Settings/Backup.tsx
🧬 Code graph analysis (2)
src/screens/Settings/Backup.tsx (3)
src/lib/backup.ts (1)
  • BackupProvider (17-168)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
src/lib/backup.ts (3)
src/lib/types.ts (1)
  • Config (8-23)
src/lib/nostr.ts (1)
  • NostrStorage (9-151)
src/lib/logs.ts (1)
  • consoleError (40-44)
🔇 Additional comments (3)
src/screens/Settings/Backup.tsx (2)

19-20: BackupProvider/ErrorMessage imports are consistent with the new backup flow

The new imports cleanly align this screen with the centralized BackupProvider and shared ErrorMessage component; no issues here.


73-73: Nice reuse of ErrorMessage to surface backup/password issues in the backup UI

Hooking ErrorMessage into this screen gives users immediate feedback when something goes wrong; once the stale‑error handling is addressed, this should improve UX significantly.

test.docker-compose.yml (1)

45-47: Image pinning for core services looks consistent and improves reproducibility

Switching arkd-wallet, arkd, boltz, and boltz-fulmine to explicit tagged images makes the test stack more predictable and easier to reason about across environments; the wiring (ports/env/volumes) remains unchanged, which is good.

Also applies to: 63-63, 124-124, 228-228

@bordalix

Copy link
Copy Markdown
Collaborator Author

@louisinger it took me way longer to make the test than to fix the bug 🤣

Please double check, specially cae38eb

@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 (2)
src/screens/Init/Restore.tsx (1)

29-49: Reset privateKey/validation when the input is cleared or invalid

In the useEffect on someKey, when someKey is falsy you return early without resetting any state:

if (!someKey) return
...
setPrivateKey(privateKey)

This means:

  • If the user enters a valid key (setting privateKey and clearing error), then clears the field, privateKey and error keep their previous values.
  • disabled is computed as Boolean(!privateKey || error), so with a cleared field but a stale privateKey and no error, the “Continue” button stays enabled and will restore with the old key.

Consider explicitly clearing state when the field is empty and only setting privateKey on valid parses, e.g.:

useEffect(() => {
  if (!someKey) {
    setPrivateKey(undefined)
    setLabel(buttonLabel)
    setError('')
    return
  }

  let pk: Uint8Array | undefined
  try {
    pk = someKey.match(/^nsec/) ? nsecToPrivateKey(someKey) : hex.decode(someKey)
    const invalid = invalidPrivateKey(pk)
    setLabel(invalid ? 'Unable to validate private key format' : buttonLabel)
    setError(invalid)
    setPrivateKey(invalid ? undefined : pk)
  } catch (err) {
    setLabel('Unable to validate key format')
    setError(extractError(err))
    setPrivateKey(undefined)
  }
}, [someKey])

Also, adding name='private-key' while keeping the Input uncontrolled (no value prop) preserves the earlier security decision to avoid re‑rendering the secret, which is good. Based on learnings.

Also applies to: 65-66, 76-76

src/lib/nostr.ts (1)

81-117: Close the Nostr subscription when load() times out to avoid resource leaks

SimplePool.subscribeMany returns a SubCloser object with a .close() method you call to end that subscription, and without calling it, subscriptions persist and connections remain open.

In the current load() implementation (lines 81–117), the success path closes the subscription via sub.close() in the oneose callback (line 103), but the timeout promise (line 111–113) resolves without closing it. Since sub is scoped to the first Promise, the timeout path cannot access it. This leaves dangling subscriptions and open WebSocket connections.

Repeated load() calls that hit the timeout will accumulate unclosed subscriptions in the SimplePool. Capture the subscription reference at a higher scope so both success and timeout paths can clean up:

async load(): Promise<Event[]> {
  if (!this.seckey) throw new Error('Secret key is required for loading data')

  const events: Event[] = []
  let timeoutHandler: ReturnType<typeof setTimeout> | undefined
  let sub: ReturnType<SimplePool['subscribeMany']> | undefined

  const loadPromise = new Promise<Event[]>((resolve) => {
    sub = this.pool.subscribeMany(
      this.relays,
      { kinds: [4], '#p': [this.pubkey], '#t': [nostrAppName] },
      {
        onevent: (event: Event) => {
          try {
            const content = this.decryptEvent(event)
            events.push({ ...event, content })
          } catch (error) {
            consoleError(error, 'Failed to decrypt event')
          }
        },
        oneose: () => {
          if (sub) sub.close()
          if (timeoutHandler) clearTimeout(timeoutHandler)
          resolve(events)
        },
      },
    )
  })

  const timeoutPromise = new Promise<Event[]>((resolve) => {
    timeoutHandler = setTimeout(() => {
      consoleError(new Error('Load timeout'), 'Failed to load backup data')
      if (sub) sub.close()
      resolve([])
    }, 10000)
  })

  return Promise.race([loadPromise, timeoutPromise])
}
🧹 Nitpick comments (3)
playwright.config.ts (1)

16-22: Consider making the webServer.command env setup cross‑platform

command: 'VITE_NOSTR_RELAY_URL=ws://localhost:10547 pnpm start' relies on POSIX shell syntax and will fail on Windows unless you always run tests in a POSIX environment. To make this portable, consider either:

  • Using Playwright’s webServer.env:
webServer: {
  command: 'pnpm start',
  port: 3002,
  env: { VITE_NOSTR_RELAY_URL: 'ws://localhost:10547' },
}

or

  • Moving the env var into the start script via something like cross-env.
src/test/e2e/nostr.test.ts (1)

3-90: Harden E2E selectors and waits to reduce flakiness

The flow this test covers is excellent, but a few selector patterns are quite brittle:

  • const nsec = await page.locator('p').nth(2).innerText() depends on the exact <p> order in “Backup and privacy”. Any content change can break this. Prefer a more specific locator (e.g., a data-testid, a label near the nsec, or a text pattern that clearly identifies the key).
  • Many steps use getByText('Settings') and similar generic text; if multiple elements share the same label, Playwright may click the wrong one. Using roles or test IDs (e.g., getByRole('button', { name: 'Settings' })) tends to be more stable.
  • The page.waitForTimeout(1000) after resetting the wallet is a fixed delay. It’d be more robust to wait for a deterministic condition, such as the first onboarding screen becoming visible or a URL change.
  • For await page.locator('ion-input[name="private-key"] input').fill(nsec), double‑check this works with Ionic’s Shadow DOM. In some setups you may need a >> combinator (e.g., page.locator('ion-input[name="private-key"] >> input')) or a test ID on a higher‑level React component instead of reaching into the internal <input>.

These changes should make the test less brittle as the UI evolves.

src/lib/nostr.ts (1)

5-8: Nostr relay/env wiring and nip44 usage look coherent

Using a single nostrAppName tag and relays derived from VITE_NOSTR_RELAY_URL (with a sensible default list) makes the storage layer predictable and easy to point at test relays. Likewise, save(payload: string) using:

  • an ephemeral sk/pk pair,
  • nip44.getConversationKey(ephemeralSk, this.pubkey) on write, and
  • nip44.getConversationKey(this.seckey, event.pubkey) on read

is consistent with NIP‑44 DM semantics and matches the filters { kinds: [4], '#p': [this.pubkey], '#t': [nostrAppName] }.

One minor nit: for log consistency with the rest of the codebase you might want to use consoleError instead of raw console.error in the publish error path, but that’s purely cosmetic.

Also applies to: 22-25, 50-58

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 401b77e and cae38eb.

📒 Files selected for processing (8)
  • playwright.config.ts (3 hunks)
  • src/components/Input.tsx (2 hunks)
  • src/components/InputAddress.tsx (3 hunks)
  • src/components/InputWithScanner.tsx (3 hunks)
  • src/lib/nostr.ts (5 hunks)
  • src/screens/Init/Restore.tsx (3 hunks)
  • src/screens/Wallet/Send/Form.tsx (1 hunks)
  • src/test/e2e/nostr.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-21T18:25:36.391Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/Input.tsx
  • src/screens/Wallet/Send/Form.tsx
  • src/components/InputWithScanner.tsx
  • src/components/InputAddress.tsx
  • src/screens/Init/Restore.tsx
📚 Learning: 2025-08-06T09:53:28.428Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/screens/Init/Restore.tsx
🧬 Code graph analysis (2)
src/screens/Init/Restore.tsx (4)
src/lib/backup.ts (1)
  • BackupProvider (17-168)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/components/Loading.tsx (1)
  • Loading (5-18)
src/components/Input.tsx (1)
  • Input (13-40)
src/lib/nostr.ts (1)
src/lib/logs.ts (1)
  • consoleError (40-44)
⏰ 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: test
🔇 Additional comments (4)
src/screens/Wallet/Send/Form.tsx (1)

367-375: Adding name='send-address' to InputAddress is a safe enhancement

Wiring a name onto the recipient InputAddress improves testability/identification and doesn’t change behavior, since the component just forwards this to the underlying input.

src/components/Input.tsx (1)

8-8: LGTM! Clean prop threading for form accessibility.

The addition of the optional name prop and its forwarding to IonInput is correct and maintains backward compatibility. This enhances form handling and accessibility without affecting existing behavior.

Also applies to: 13-13, 32-32

src/components/InputWithScanner.tsx (1)

12-12: LGTM! Consistent prop forwarding.

The name prop addition follows the same pattern as Input.tsx and is correctly threaded through to the underlying IonInput. The changes integrate well with the existing props and functionality.

Also applies to: 25-25, 51-51

src/components/InputAddress.tsx (1)

15-15: LGTM! Completes the prop propagation chain.

The name prop is correctly added and forwarded to InputWithScanner, completing the propagation chain down to the native input element. The changes are consistent with the pattern established in the other input components.

Also applies to: 27-27, 51-51

Comment on lines 53 to 63
const handleProceed = () => {
setInitInfo({ privateKey, password: defaultPassword, restoring: true })
handleNostrRestore(privateKey!, updateConfig)
.then(() => navigate(Pages.InitSuccess))
setRestoring(true)
new BackupProvider({ seckey: privateKey! })
.restore(updateConfig)
.catch((err) => consoleError(err, 'Error restoring from nostr'))
.finally(() => {
setRestoring(false)
navigate(Pages.InitSuccess)
})
}

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 | 🟠 Major

Avoid navigating to “success” when Nostr restore fails

BackupProvider({ seckey: privateKey! }).restore(updateConfig) can reject, but navigate(Pages.InitSuccess) is called in .finally, so you’ll show the success screen (and “Wallet restored successfully!”) even if restore failed or no backup was applied.

It would be safer to:

  • Navigate to Pages.InitSuccess only on successful restore, and
  • On error, clear restoring, surface an error message, and stay on this screen.

For example:

setRestoring(true)
new BackupProvider({ seckey: privateKey! })
  .restore(updateConfig)
  .then(() => {
    setRestoring(false)
    navigate(Pages.InitSuccess)
  })
  .catch((err) => {
    consoleError(err, 'Error restoring from nostr')
    setRestoring(false)
    setError('Failed to restore from Nostr backup')
  })

You could also add a quick guard at the top of handleProceed:

if (restoring) return

to prevent double‑clicks from firing multiple restore requests.

Also applies to: 67-67

🤖 Prompt for AI Agents
In src/screens/Init/Restore.tsx around lines 53 to 63 (and also at line 67), the
current flow calls navigate(Pages.InitSuccess) in a .finally block which will
show success even when BackupProvider.restore rejects; change the promise
handling to call navigate only on successful .then, move setRestoring(false)
into both success and error branches (or use finally only after resolving
whether to navigate), and in the .catch log the error, clear restoring and call
setError('Failed to restore from Nostr backup') (or a suitable message) instead
of navigating; also add a guard at the top of handleProceed (if (restoring)
return) to prevent duplicate submissions.

Comment thread src/lib/backup.ts
* @param options.pubkey - Optional public key (hex string). Required if seckey not provided.
* @throws Error if neither seckey nor pubkey is provided, or if pubkey format is invalid
*/
constructor(options: { pubkey?: string; seckey?: Uint8Array }) {

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.

now I understand why we need arkade-os/ts-sdk#223
we need to think about it

Comment thread src/lib/backup.ts

const dataSize = JSON.stringify(data).length

if (dataSize > 65000) {

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.

Can't we backup 1 event / swap whatever the datasize ? easier to have only 1 unique format no ? also are we sure this 65000 is a constant, relay can't configure it ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm trying not to spam Nostr with a lot of events and also for fastest backup/restore.

For most cases 64KB should be enough.

We only control one relay, and we are saving the backup to 3 relays for redundancy, so we need to respect the limit.

Comment thread src/lib/nostr.ts
* @param event event to decrypt
* @returns string encrypted in the event
*/
private decryptEvent(event: Event): string {

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.

it seems decryptData is only used here, it should become decryptEvent(Event) so we can remove this extra method

Comment thread src/test/e2e/nostr.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: 0

♻️ Duplicate comments (1)
src/screens/Settings/Backup.tsx (1)

51-64: Still backing up when disabling; toast shown regardless of backup success.

The previous review comment was only partially addressed. Issues remaining:

  1. Line 62: When disabling Nostr backup, backupConfig(newConfig) is called, which backs up the config (with nostrBackup=false) to Nostr. This is contradictory—if the user is disabling Nostr backups, we shouldn't be writing to Nostr at all.

  2. Line 64: The toast is shown unconditionally, even if the backup fails or is skipped. It should only be shown after a successful operation.

  3. Line 55: The BackupProvider constructor can throw synchronously (e.g., if pubkey is invalid), but it's not wrapped in try/catch, so those errors won't be caught by the .catch() on Line 56 and won't set the error state.

Apply this diff to fix the logic:

   const toggleNostrBackup = async () => {
     const newConfig = { ...config, nostrBackup: !config.nostrBackup }
-    updateConfig(newConfig)
-    if (newConfig.nostrBackup) {
-      const backupProvider = new BackupProvider({ pubkey: config.pubkey })
-      await backupProvider.fullBackup(newConfig).catch((error) => {
-        consoleError(error, 'Backup to Nostr failed')
-        setError('Backup to Nostr failed')
-        return
-      })
-    } else {
-      backupConfig(newConfig)
-    }
-    present(backupToNostr)
+    try {
+      if (newConfig.nostrBackup) {
+        const backupProvider = new BackupProvider({ pubkey: config.pubkey })
+        await backupProvider.fullBackup(newConfig)
+      }
+      updateConfig(newConfig)
+      setError('')
+      present(backupToNostr)
+    } catch (error) {
+      consoleError(error, 'Backup to Nostr failed')
+      setError('Backup to Nostr failed')
+    }
   }

This ensures:

  • No backup is performed when disabling
  • Config is persisted only after successful backup (when enabling) or immediately (when disabling)
  • All errors (constructor and async) are caught
  • Toast is shown only on success
🧹 Nitpick comments (7)
test.docker-compose.yml (1)

255-262: Add missing restart policy and consider health checks for consistency.

The new nak service is missing the restart: unless-stopped policy present in all other services, which could impact reliability during local test runs if the container unexpectedly exits. Additionally, depending on how Playwright e2e tests interact with this service, consider adding a healthcheck (like other backend services) and/or depends_on declarations in dependent services.

Apply this diff to add the restart policy and align with service patterns:

  nak:
+   restart: unless-stopped
    build:
      context: .
      dockerfile: nak.Dockerfile

If other services in this compose file (such as UI tests or integration tests) require nak to be ready before starting, declare that dependency explicitly in those services:

  some-test-service:
    depends_on:
      nak:
        condition: service_started  # or service_healthy if you add a healthcheck
src/screens/Settings/Notifications.tsx (1)

14-28: Consider adding error handling for backup operations.

The backup operations in both branches (lines 19 and 25) could fail, but there's no user-facing error handling. If backupConfig rejects, the promise chain will continue and the config will still be updated. Consider wrapping the backup calls in try-catch blocks to provide user feedback if the backup fails.

-  const handleChange = async () => {
+  const handleChange = async () => {
     if (!notificationApiSupport) return
     if (!config.notifications) {
       requestPermission().then(async (notifications) => {
         const newConfig = { ...config, notifications }
-        if (config.nostrBackup) await backupConfig(newConfig)
+        if (config.nostrBackup) {
+          try {
+            await backupConfig(newConfig)
+          } catch (error) {
+            console.error('Backup failed:', error)
+            // Optionally show error to user
+          }
+        }
         if (notifications) sendTestNotification()
         updateConfig(newConfig)
       })
     } else {
       const newConfig = { ...config, notifications: false }
-      if (config.nostrBackup) await backupConfig(newConfig)
+      if (config.nostrBackup) {
+        try {
+          await backupConfig(newConfig)
+        } catch (error) {
+          console.error('Backup failed:', error)
+          // Optionally show error to user
+        }
+      }
       updateConfig(newConfig)
     }
   }
src/test/e2e/nostr.test.ts (1)

136-141: Potential race condition: exec is not awaited.

Line 137 uses exec() without awaiting the command completion, while line 154 correctly uses execAsync() with await. Although the test waits for the "Payment received!" message on line 140, which should handle the async nature of the payment, this inconsistency could lead to flaky tests if the payment processing is slow.

Consider using execAsync consistently for both invoice payments to ensure deterministic test execution.

-  // pay invoice with lnd
-  exec(`docker exec lnd lncli --network=regtest payinvoice ${receiveInvoice} --force`)
+  // pay invoice with lnd
+  await execAsync(`docker exec lnd lncli --network=regtest payinvoice ${receiveInvoice} --force`)
 
   // wait for payment received
   await page.waitForSelector('text=Payment received!')
src/providers/config.tsx (1)

50-55: Consider validating pubkey before creating BackupProvider.

Line 51 creates a BackupProvider with config.pubkey, but during initial wallet creation or reset, the pubkey might be empty or invalid. While the error is caught and logged (lines 52-54), the BackupProvider constructor will throw if the pubkey is invalid, which could cause confusion in error logs.

Consider adding a guard to skip backup if pubkey is not set:

 const backupConfig = async (config: Config) => {
+  if (!config.pubkey) {
+    console.warn('Backup skipped: pubkey not set')
+    return
+  }
   const backupProvider = new BackupProvider({ pubkey: config.pubkey })
   await backupProvider.backupConfig(config).catch((error) => {
     consoleError(error, 'Backup to Nostr failed')
   })
 }
src/lib/backup.ts (3)

76-98: Remove redundant config check and clarify chunking strategy.

Line 86 checks if (config), but config is a required parameter of the function, making this check redundant. The chunking logic at line 85 uses a 65KB threshold, which was intentionally chosen as discussed in previous reviews.

     const dataSize = JSON.stringify(data).length
 
     if (dataSize > 65000) {
-      if (config) await this.backupConfig(config)
+      await this.backupConfig(config)
 
       for (const reverseSwap of data.reverseSwaps ?? []) {
         await this.backupReverseSwap(reverseSwap)

109-117: Use consistent logging utility instead of console.log.

Lines 110 and 115 use console.log directly, while the rest of the codebase uses the consoleLog utility from src/lib/logs.ts for consistent log management. Consider updating these for consistency.

+import { consoleError, consoleLog } from './logs'
...
     for (const swap of data?.reverseSwaps ?? []) {
-      console.log('restoring reverse swap:', swap.id)
+      consoleLog('restoring reverse swap:', swap.id)
       await contractRepo.saveToContractCollection('reverseSwaps', swap, 'id')
     }
 
     for (const swap of data?.submarineSwaps ?? []) {
-      console.log('restoring submarine swap:', swap.id)
+      consoleLog('restoring submarine swap:', swap.id)
       await contractRepo.saveToContractCollection('submarineSwaps', swap, 'id')
     }

136-160: Sort stability: events with identical timestamps may not have deterministic ordering.

Line 137 sorts events by created_at, but if multiple events share the same timestamp (e.g., during bulk backup operations), the sort order becomes non-deterministic. While the deduplication logic using Maps (lines 153-159) handles conflicts by keeping the last entry seen, the final result could vary across restores.

Consider adding a secondary sort key (e.g., event.id) to ensure consistent ordering:

     // Events are sorted by created_at to have a deterministic order.
-    const sorted = events.sort((a, b) => a.created_at - b.created_at)
+    const sorted = events.sort((a, b) => {
+      if (a.created_at !== b.created_at) return a.created_at - b.created_at
+      return a.id.localeCompare(b.id)
+    })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cae38eb and 7f2f502.

📒 Files selected for processing (16)
  • package.json (1 hunks)
  • src/components/Balance.tsx (1 hunks)
  • src/lib/backup.ts (1 hunks)
  • src/lib/nostr.ts (5 hunks)
  • src/providers/config.tsx (5 hunks)
  • src/providers/lightning.tsx (2 hunks)
  • src/providers/wallet.tsx (1 hunks)
  • src/screens/Settings/Backup.tsx (3 hunks)
  • src/screens/Settings/Display.tsx (1 hunks)
  • src/screens/Settings/Fiat.tsx (1 hunks)
  • src/screens/Settings/Notifications.tsx (1 hunks)
  • src/screens/Settings/Server.tsx (2 hunks)
  • src/screens/Settings/Theme.tsx (1 hunks)
  • src/test/e2e/nostr.test.ts (1 hunks)
  • src/test/setup.mjs (1 hunks)
  • test.docker-compose.yml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/nostr.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-08-06T09:53:28.428Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/screens/Settings/Backup.tsx
  • src/screens/Settings/Server.tsx
📚 Learning: 2025-07-21T18:25:36.391Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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/screens/Settings/Backup.tsx
  • src/screens/Settings/Display.tsx
📚 Learning: 2025-06-30T18:33:29.839Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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:

  • src/providers/wallet.tsx
📚 Learning: 2025-07-07T14:41:35.991Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 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:

  • package.json
📚 Learning: 2025-11-20T12:08:19.349Z
Learnt from: bordalix
Repo: arkade-os/wallet PR: 230
File: test.docker-compose.yml:46-46
Timestamp: 2025-11-20T12:08:19.349Z
Learning: The Docker image `boltz/boltz:ark` exists and is available for the arkade-os/wallet repository.

Applied to files:

  • package.json
  • test.docker-compose.yml
🧬 Code graph analysis (10)
src/screens/Settings/Fiat.tsx (2)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/components/Select.tsx (1)
  • Select (12-43)
src/screens/Settings/Notifications.tsx (2)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/lib/notifications.ts (3)
  • notificationApiSupport (1-2)
  • requestPermission (4-7)
  • sendTestNotification (29-33)
src/providers/lightning.tsx (1)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/screens/Settings/Backup.tsx (4)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/lib/backup.ts (1)
  • BackupProvider (17-168)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
src/lib/backup.ts (3)
src/lib/types.ts (1)
  • Config (8-23)
src/lib/nostr.ts (1)
  • NostrStorage (9-141)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/test/e2e/nostr.test.ts (2)
src/test/e2e/utils.ts (1)
  • readClipboard (3-16)
src/test/setup.mjs (1)
  • invoice (243-243)
src/screens/Settings/Server.tsx (1)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/screens/Settings/Display.tsx (2)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/components/Select.tsx (1)
  • Select (12-43)
src/providers/config.tsx (3)
src/lib/types.ts (1)
  • Config (8-23)
src/lib/backup.ts (1)
  • BackupProvider (17-168)
src/lib/logs.ts (1)
  • consoleError (40-44)
src/screens/Settings/Theme.tsx (2)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/components/Select.tsx (1)
  • Select (12-43)
⏰ 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). (2)
  • GitHub Check: Cloudflare Pages: wallet-bitcoin
  • GitHub Check: test
🔇 Additional comments (14)
test.docker-compose.yml (1)

255-262: nak.Dockerfile verified as valid and properly formatted.

The file exists at the repository root and contains valid Dockerfile syntax with no errors. The multi-stage build structure (golang:1.25.3-alpine → alpine:latest) is correct, all directives are syntactically valid, and the file is complete with proper ending. Security best practices are followed (non-root user, binary permissions).

src/components/Balance.tsx (1)

30-30: LGTM!

The change correctly aligns with the refactored updateConfig API that now accepts a single argument.

src/screens/Settings/Display.tsx (1)

10-16: LGTM!

The change correctly integrates backup handling with config updates. The pattern of conditionally backing up when nostrBackup is enabled, then updating the config, is consistent with the new BackupProvider-based architecture.

src/screens/Settings/Fiat.tsx (1)

10-16: LGTM!

Consistent implementation of the backup-before-update pattern.

src/test/setup.mjs (1)

300-312: LGTM!

The setupNostr function follows the same pattern as setupBoltz and integrates cleanly into the test setup flow.

src/screens/Settings/Theme.tsx (1)

10-16: LGTM!

Consistent implementation matching the backup pattern in Display.tsx and Fiat.tsx.

src/providers/wallet.tsx (1)

256-256: LGTM!

The change correctly updates to the new single-argument updateConfig API.

package.json (1)

7-7: No issues found. The package version is valid and has no known vulnerabilities.

The verification confirms that @arkade-os/boltz-swap version 0.2.9 exists on npm and has no known security vulnerabilities.

src/screens/Settings/Server.tsx (1)

54-68: LGTM! Backup timing is correct.

The backup flow here is implemented correctly: backup is performed before updateConfig, which ensures consistency. The try-catch block provides appropriate error handling for the entire operation.

src/test/e2e/nostr.test.ts (1)

20-94: Well-structured E2E test for config backup/restore.

This test thoroughly validates the config backup and restore flow by:

  1. Enabling backups and changing settings (EUR)
  2. Disabling backups and changing settings again (USD)
  3. Restoring from backup and verifying the EUR setting persists

The test design ensures that the restored config comes from Nostr backup and not from local persistence.

src/providers/config.tsx (1)

62-71: LGTM! Simplified updateConfig signature.

Removing the backup parameter from updateConfig and centralizing backup logic through backupConfig is a cleaner design. The function now has a single responsibility: updating and persisting the config to local storage.

src/lib/backup.ts (2)

28-42: LGTM! Well-structured constructor with proper validation.

The constructor correctly handles both seckey and pubkey initialization paths, validates inputs, and throws appropriate errors for invalid configurations. The pubkey normalization (removing '0x' prefix and length validation) is robust.


142-148: LGTM! Proper error handling for JSON parsing.

The try-catch block around JSON parsing addresses the previous review concern about malformed events causing restore failures. The code now gracefully skips invalid events and logs errors using the appropriate utility.

src/providers/lightning.tsx (1)

73-78: Standardize backup timing and async handling across all settings screens.

The review comment's observation is verified as accurate. The ordering and async handling of backupConfig is inconsistent:

  • lightning.tsx (line 77): calls backupConfig without await after updateConfig
  • Server.tsx (line 60), Notifications.tsx (lines 19, 25): call backupConfig with await before updateConfig
  • Display.tsx, Theme.tsx, Fiat.tsx: all follow the backup-first pattern with await

The difference matters: lightning.tsx doesn't wait for the backup to complete before the function returns, whereas other screens wait for the backup operation to finish. This could lead to the backup running asynchronously in the background without error handling synchronization.

Decide whether the async-without-await pattern in lightning.tsx is intentional (fire-and-forget backup) or should be standardized with the rest of the codebase. If standardizing, align both the ordering (backup before update) and async handling (await keyword) across all files.

bordalix and others added 3 commits November 20, 2025 17:10
* Add ChatWoot integration for customer support

- Added Support settings page under Settings menu
- Implemented ChatWoot widget integration with secure chat functionality
- Users can now report bugs and track support conversations from the wallet
- Added useChatwoot hook for managing widget lifecycle
- Support page displays when ChatWoot is configured via environment variables
- Added VITE_CHATWOOT_WEBSITE_TOKEN and VITE_CHATWOOT_BASE_URL to configuration
- Integration includes user identification and custom attributes (wallet pubkey, network)

* Include wallet addresses in ChatWoot support integration

- Added ARK address (offchainAddr) to ChatWoot custom attributes
- Added BTC boarding address (boardingAddr) to custom attributes
- Included explorer URL for the current network
- Support operators can now search user addresses in the blockchain explorer
- Addresses are automatically fetched when support page is opened

* Add comprehensive service URLs and addresses to ChatWoot

- Added location origin (window.location.origin) for deployment context
- Added ARK server URL and indexer URL (both use aspInfo.url)
- Added wallet ARK address (offchainAddr) and BTC boarding address
- Added blockchain explorer URL for the user's network
- Added Boltz Lightning swap provider URL
- Added LendaSat and LendaSwap application URLs
- Support operators can now see all service endpoints and addresses
- Enables quick troubleshooting by showing user's complete configuration

* add chatwoot support

* deal with error loading chatwoot

* Add safety check before using non-null assertion

* Fix the timeout logic bug

* console.log to debug

* console.log for debug

* increase menu row height

* remove debug console logs

* increase height of menus

---------

Co-authored-by: Claude <noreply@anthropic.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: 1

🧹 Nitpick comments (8)
src/test/e2e/swap.test.ts (1)

19-19: Consider using a more specific locator.

The tests now use getByText('Boltz') instead of the previous getByRole('heading', { name: 'Boltz' }). While this works given the UI changes, text-based locators are less specific and don't follow accessibility testing best practices.

Consider using a more specific locator if the Header component renders with identifiable attributes. For example:

-await expect(page.getByText('Boltz')).toBeVisible()
+await expect(page.getByRole('banner').getByText('Boltz')).toBeVisible()

Or add a test ID to the Header component for more robust testing:

-await expect(page.getByText('Boltz')).toBeVisible()
+await expect(page.getByTestId('app-header')).toContainText('Boltz')

Also applies to: 80-80, 143-143, 210-210

src/components/Select.tsx (1)

30-40: Consistent spacing; consider adding effect dependencies

The padding change on the option row matches the rest of the settings UI and is fine. To avoid a stale closure if options or onChange can change over time, you might optionally include them in the effect deps:

-  }, [selected])
+  }, [selected, options, onChange])
src/icons/Support.tsx (1)

1-10: SupportIcon SVG component looks good

The icon component is straightforward and consistent with typical inline SVG usage. If this icon is purely decorative and other icons follow suit, you could optionally add aria-hidden='true'/focusable='false' to reduce noise for screen readers, but it’s not required.

src/components/ChatWoot.tsx (1)

1-25: Chatwoot widget integration and cleanup look solid

The initialization flow (env vars → settings → script injection) and script cleanup on unmount are well-contained in this component. If there’s any chance this widget could mount more than once during an app session, you might optionally short‑circuit before injectAndRunChatwootScript when a matching SDK <script> tag already exists to avoid duplicate loads, but it’s not blocking.

src/screens/Settings/Support.tsx (2)

60-60: Potential stale closure with timeout callback.

The useEffect has an empty dependency array, but the timeout callback on line 48 references supportChatLoaded. While the current logic likely works (since the timeout is cleared when the event fires), this is fragile—if the effect were refactored or the state update order changed, the stale closure could cause the error to be set incorrectly.

Consider using a ref for the timeout flag or restructuring to avoid the stale closure:

 useEffect(() => {
   if (!hasChatwootVars()) {
     setError('Support chat is not configured')
     return
   }
 
+  let loaded = false
   const loadTimeout = setTimeout(() => {
-    if (!supportChatLoaded) setError('Failed to load support chat')
+    if (!loaded) setError('Failed to load support chat')
   }, 5_000)
 
   const eventHandler = () => {
+    loaded = true
     clearTimeout(loadTimeout)
     setSupportChatLoaded(true)
     window.$chatwoot?.toggleBubbleVisibility('hide')
   }
 
   const event = 'chatwoot:ready'
   window.addEventListener(event, eventHandler)
-  return () => window.removeEventListener(event, eventHandler)
+  return () => {
+    window.removeEventListener(event, eventHandler)
+    clearTimeout(loadTimeout)
+  }
 }, [])

67-68: User identifier truncation could cause collisions (low risk).

Using wallet.pubkey.substring(0, 16) as the user identifier creates a theoretical collision risk if multiple users share the first 16 characters of their pubkey. While this is extremely unlikely with cryptographic keys, consider using the full pubkey or a hash if Chatwoot's identifier length allows it.

src/lib/chatwoot.ts (2)

56-59: Incomplete JSDoc comment.

The JSDoc comment block is present but empty. Either document the function (parameters, return value, behavior) or remove the empty comment block.

Example documentation:

 /**
- *
- * @param settings
- */
+ * Dynamically injects the Chatwoot SDK script and initializes it with the provided configuration.
+ * Polls for SDK availability up to 5 seconds before timing out.
+ * @param vars - Chatwoot configuration containing websiteToken and baseUrl
+ */
 export const injectAndRunChatwootScript = (vars: ChatwootVars) => {

67-70: Consider handling missing parent node more gracefully.

The function logs an error and returns early if the parent node is missing, but this leaves the Chatwoot feature silently broken. Consider propagating the error or notifying the user that chat support is unavailable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2f502 and 0adaa60.

📒 Files selected for processing (13)
  • README.md (1 hunks)
  • src/components/ChatWoot.tsx (1 hunks)
  • src/components/Menu.tsx (1 hunks)
  • src/components/Select.tsx (1 hunks)
  • src/icons/Support.tsx (1 hunks)
  • src/lib/chatwoot.ts (1 hunks)
  • src/lib/types.ts (1 hunks)
  • src/providers/options.tsx (2 hunks)
  • src/screens/Apps/Boltz/Index.tsx (1 hunks)
  • src/screens/Settings/General.tsx (1 hunks)
  • src/screens/Settings/Index.tsx (2 hunks)
  • src/screens/Settings/Support.tsx (1 hunks)
  • src/test/e2e/swap.test.ts (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • README.md
🧰 Additional context used
🧬 Code graph analysis (7)
src/screens/Settings/General.tsx (1)
src/components/FlexRow.tsx (1)
  • FlexRow (17-48)
src/components/Select.tsx (1)
src/components/FlexRow.tsx (1)
  • FlexRow (17-48)
src/screens/Settings/Support.tsx (15)
src/providers/asp.tsx (1)
  • AspContext (15-20)
src/providers/config.tsx (1)
  • ConfigContext (33-43)
src/providers/lightning.tsx (1)
  • LightningContext (24-30)
src/providers/wallet.tsx (1)
  • WalletContext (42-56)
src/lib/types.ts (1)
  • Addresses (3-6)
src/lib/asp.ts (1)
  • getReceivingAddresses (144-150)
src/lib/chatwoot.ts (1)
  • hasChatwootVars (35-38)
src/lib/explorers.ts (1)
  • getWebExplorerURL (38-40)
src/components/FlexCol.tsx (1)
  • FlexCol (15-40)
src/components/Text.tsx (2)
  • Text (25-79)
  • TextSecondary (91-97)
src/components/Content.tsx (1)
  • Content (9-16)
src/components/Padded.tsx (1)
  • Padded (7-14)
src/components/Error.tsx (1)
  • ErrorMessage (10-21)
src/components/ButtonsOnBottom.tsx (1)
  • ButtonsOnBottom (10-26)
src/components/Button.tsx (1)
  • Button (21-64)
src/providers/options.tsx (1)
src/icons/Support.tsx (1)
  • SupportIcon (1-10)
src/components/ChatWoot.tsx (1)
src/lib/chatwoot.ts (3)
  • getChatwootVars (28-33)
  • getChatwootSettings (47-54)
  • injectAndRunChatwootScript (60-89)
src/screens/Settings/Index.tsx (1)
src/screens/Settings/Support.tsx (1)
  • Support (21-136)
src/lib/chatwoot.ts (1)
src/lib/logs.ts (1)
  • consoleError (40-44)
🔇 Additional comments (7)
src/screens/Apps/Boltz/Index.tsx (1)

33-33: LGTM!

The explicit header text consolidates the Boltz label into the Header component, improving consistency and eliminating duplicate headings.

src/components/Menu.tsx (1)

19-29: Menu row padding tweak looks good

The increased vertical padding improves touch targets and keeps styling consistent with other settings rows; no behavioral impact.

src/screens/Settings/General.tsx (1)

17-28: General settings row spacing is consistent

The updated padding aligns this row with other settings components and improves click/tap area without changing behavior.

src/screens/Settings/Index.tsx (1)

1-44: Support screen wiring is correct

Importing Support and rendering it when SettingsOptions.Support is selected matches the existing pattern for other settings screens and should integrate cleanly with the options provider.

src/lib/types.ts (1)

45-63: New SettingsOptions.Support enum entry is consistent

Adding Support = 'support' follows the existing naming and string conventions for settings options and cleanly supports the new Support screen and menu entry.

src/providers/options.tsx (1)

10-77: Support option registration is wired correctly

Importing SupportIcon and adding a SettingsOptions.Support entry in the General section keeps the options list coherent and ensures the new Support screen is discoverable from the main settings menu.

src/lib/chatwoot.ts (1)

72-87: Review timeout and polling logic.

The polling interval is set to 100ms with a 5-second timeout (50 iterations). This is reasonable, but ensure this timeout aligns with the timeout in Support.tsx (line 49, also 5 seconds). If both timeouts race, the user might see inconsistent error states.

Verify that the timeout coordination between injectAndRunChatwootScript (line 82) and the Support component's timeout (Support.tsx line 47-49) doesn't cause race conditions or duplicate error messages. Both use 5 seconds, but they may not start at exactly the same time.

lendaswap_url: import.meta.env.VITE_LENDASWAP_IFRAME_URL || 'not available',
explorer_url: wallet.network ? getWebExplorerURL(wallet.network as NetworkName) : 'not available',
})
}, [addresses, supportChatLoaded, wallet.pubkey, window.$chatwoot])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Invalid dependency in useEffect array.

window.$chatwoot is not a reactive value that React can track. Including it in the dependency array serves no purpose and may trigger linter warnings. Since window.$chatwoot becomes available when supportChatLoaded is set to true, the existing dependency on supportChatLoaded is sufficient.

Apply this diff:

-  }, [addresses, supportChatLoaded, wallet.pubkey, window.$chatwoot])
+  }, [addresses, supportChatLoaded, wallet.pubkey])
📝 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
}, [addresses, supportChatLoaded, wallet.pubkey, window.$chatwoot])
}, [addresses, supportChatLoaded, wallet.pubkey])
🤖 Prompt for AI Agents
In src/screens/Settings/Support.tsx around line 85, the useEffect dependency
array incorrectly includes window.$chatwoot (a non-reactive global); remove
window.$chatwoot from the dependency array and keep addresses,
supportChatLoaded, and wallet.pubkey only so React and the linter track real
reactive values—update the array to [, addresses, supportChatLoaded,
wallet.pubkey] (or the correct ordering your code uses) and run the linter to
confirm no remaining warnings.

@bordalix
bordalix merged commit 7cd21e1 into master Nov 21, 2025
1 check passed
@bordalix
bordalix deleted the nostr_backup_chunks branch November 26, 2025 14:29
@coderabbitai coderabbitai Bot mentioned this pull request Nov 27, 2025
pietro909 pushed a commit to pietro909/wallet that referenced this pull request Dec 8, 2025
* refactor nostr backups to use chunks

* Add error handling around JSON parsing

* fix enabling Nostr backup doesn’t persist config change

* use consoleError instead of console.error

* new regtest service: nak

* update docker images

* adds nostr test

* fixes

* tidy code

* Chatwoot (arkade-os#236)

* Add ChatWoot integration for customer support

- Added Support settings page under Settings menu
- Implemented ChatWoot widget integration with secure chat functionality
- Users can now report bugs and track support conversations from the wallet
- Added useChatwoot hook for managing widget lifecycle
- Support page displays when ChatWoot is configured via environment variables
- Added VITE_CHATWOOT_WEBSITE_TOKEN and VITE_CHATWOOT_BASE_URL to configuration
- Integration includes user identification and custom attributes (wallet pubkey, network)

* Include wallet addresses in ChatWoot support integration

- Added ARK address (offchainAddr) to ChatWoot custom attributes
- Added BTC boarding address (boardingAddr) to custom attributes
- Included explorer URL for the current network
- Support operators can now search user addresses in the blockchain explorer
- Addresses are automatically fetched when support page is opened

* Add comprehensive service URLs and addresses to ChatWoot

- Added location origin (window.location.origin) for deployment context
- Added ARK server URL and indexer URL (both use aspInfo.url)
- Added wallet ARK address (offchainAddr) and BTC boarding address
- Added blockchain explorer URL for the user's network
- Added Boltz Lightning swap provider URL
- Added LendaSat and LendaSwap application URLs
- Support operators can now see all service endpoints and addresses
- Enables quick troubleshooting by showing user's complete configuration

* add chatwoot support

* deal with error loading chatwoot

* Add safety check before using non-null assertion

* Fix the timeout logic bug

* console.log to debug

* console.log for debug

* increase menu row height

* remove debug console logs

* increase height of menus

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Fix UI incoherence on Boltz app page

---------

Co-authored-by: Claude <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Dec 8, 2025
This was referenced Feb 13, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants