Skip to content

Add ability to send directly to other imported wallets#697

Draft
oyetanishq wants to merge 1 commit into
bitcoinppl:masterfrom
oyetanishq:self-transfer
Draft

Add ability to send directly to other imported wallets#697
oyetanishq wants to merge 1 commit into
bitcoinppl:masterfrom
oyetanishq:self-transfer

Conversation

@oyetanishq
Copy link
Copy Markdown
Contributor

@oyetanishq oyetanishq commented Apr 20, 2026

Summary

  • Allow self-transfer of funds between different imported wallet.
  • Add a Show address toggle, allowing users to manually verify the underlying raw address.
  • closes: Allow sending between wallets #224

Images

IOS Android
Screenshot 2026-04-20 at 18 27 41 Screenshot 2026-04-20 at 18 25 12

Testing

  • Did manual testing on both platforms.

Platform Coverage

  • Tested on iOS device
  • Tested on Android device
  • Tested on iOS simulator
  • Tested on Android simulator
  • Not tested

Checklist

Summary by CodeRabbit

  • New Features
    • Added wallet selection functionality to the address entry screen in the Send flow: users can now select a wallet to automatically populate the recipient address
    • Selected wallet information is displayed with an option to deselect and clear the address field
    • Added toggle to show or hide the raw address text for transparency
    • Feature available on both iOS and Android platforms

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f0d1e3d-bed6-4694-b84a-75076cddd578

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds wallet-selection functionality to the send flow on both Android and iOS platforms. Users can now choose another wallet as the send destination, with its first address automatically fetched and displayed. The feature includes an optional toggle to show the raw address for verification purposes.

Changes

Cohort / File(s) Summary
Android: Wallet Picker in Send Flow
android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt
Added currentWalletId parameter and comprehensive wallet selection UI: Material3 ModalBottomSheet with filtered wallet list, async address loading via WalletManager, conditional rendering for selected vs. empty wallet states, and raw address toggle. Replaced static address input with dynamic content based on wallet selection status.
Android: Send Flow Integration
android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowSetAmountScreen.kt
Minimal change: passes currentWalletId = walletManager.id to EnterAddressView constructor to enable wallet self-exclusion in the picker.
iOS: Wallet Picker in Send Flow
ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift
iOS equivalent of Android wallet selection: added @State variables for picker visibility and wallet metadata, wallet icon button trigger, .sheet modal with navigation list, async address fetching on selection, and conditional rendering for selected wallet header with clear action.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant EnterAddressView as EnterAddressView (UI)
    participant Database
    participant WalletManager
    
    User->>EnterAddressView: Tap wallet picker button
    activate EnterAddressView
    EnterAddressView->>Database: Query allSortedActive() wallets<br/>(excluding currentWalletId)
    Database-->>EnterAddressView: Return wallet list
    EnterAddressView->>User: Display wallet picker modal
    deactivate EnterAddressView
    
    User->>EnterAddressView: Select wallet from list
    activate EnterAddressView
    EnterAddressView->>WalletManager: Load firstAddress() for wallet
    activate WalletManager
    WalletManager-->>EnterAddressView: Return address
    deactivate WalletManager
    EnterAddressView->>EnterAddressView: Update address state<br/>Set selectedWallet metadata<br/>Dismiss picker
    EnterAddressView->>User: Display selected wallet header<br/>with address and toggle
    deactivate EnterAddressView
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

android

Poem

🐰 Hop to send, from wallet to wallet,
No need to copy—let the system have it!
Peek the address, toggle it true,
Cross-wallet sending, shiny and new!
A feature so clever, a burrow so bright! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add ability to send directly to other imported wallets' clearly and concisely summarizes the main change—enabling self-transfer between imported wallets.
Description check ✅ Passed The PR description covers all required sections: a clear summary, testing details with platform coverage checkboxes, and confirmation of contributor guidelines compliance.
Linked Issues check ✅ Passed All coding requirements from #224 are met: wallet selection UI added, first address retrieval implemented, address abstracted into wallet display, and raw address toggle provided for verification.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue #224; modifications to EnterAddressView (iOS and Android) and SendFlowSetAmountScreen are scoped to wallet selection functionality.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR adds a wallet-picker button to the send flow's address entry UI, allowing users to self-transfer to any other imported wallet, and adds a "Show address" toggle to reveal the resolved raw address. The feature is implemented consistently across iOS and Android.

  • P1 (iOS): WalletManager is allocated inside the Task closure but never closed, leaking the underlying native Rust resource on every wallet selection. The Android code correctly calls wm.close().
  • P2: Both platforms load the wallet list via synchronous Database() calls on the main/UI thread (inline in the iOS List body; inside a remember {} block on Android), which should be moved to a background thread.
  • P2: firstAddress() is always used for the destination; for a privacy-focused Bitcoin wallet this risks address reuse across multiple sends to the same wallet.

Confidence Score: 4/5

Safe to merge after fixing the iOS WalletManager resource leak; remaining findings are non-blocking.

One P1 finding (iOS native resource leak on every wallet selection) should be resolved before shipping. All other comments are P2 style/quality suggestions that do not block correctness.

ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift — WalletManager must be closed after use.

Important Files Changed

Filename Overview
ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift Adds wallet picker sheet and 'Show address' toggle; WalletManager is never closed after fetching the address (resource leak), and Database I/O runs inline on every view render.
android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt Adds ModalBottomSheet wallet picker; WalletManager is correctly closed, but wallet list is loaded synchronously on the composition thread and errors are silently swallowed with no user feedback.
android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowSetAmountScreen.kt Passes currentWalletId to EnterAddressView; no issues in this file.

Sequence Diagram

sequenceDiagram
    actor User
    participant EnterAddressView
    participant ModalBottomSheet/Sheet
    participant Database
    participant WalletManager (dest)

    User->>EnterAddressView: Tap wallet icon
    EnterAddressView->>ModalBottomSheet/Sheet: showingWalletPicker = true
    ModalBottomSheet/Sheet->>Database: allSortedActive() filtered by currentWalletId
    Database-->>ModalBottomSheet/Sheet: WalletMetadata list
    User->>ModalBottomSheet/Sheet: Select wallet
    ModalBottomSheet/Sheet->>WalletManager (dest): WalletManager(id)
    WalletManager (dest)->>WalletManager (dest): firstAddress()
    WalletManager (dest)-->>ModalBottomSheet/Sheet: addressInfo
    ModalBottomSheet/Sheet->>EnterAddressView: address = unformatted address
    note over EnterAddressView: selectedWallet set, showingWalletPicker = false
    EnterAddressView-->>User: Shows wallet card + Show address toggle
    User->>EnterAddressView: Tap Show address
    EnterAddressView-->>User: Reveals raw Bitcoin address
Loading

Reviews (1): Last reviewed commit: "Add ability to send directly to other im..." | Re-trigger Greptile

Comment on lines +101 to +109
Task {
if let wm = try? WalletManager(id: wallet.id),
let addrInfo = try? await wm.firstAddress()
{
address = addrInfo.addressUnformatted()
selectedWallet = wallet
showRawAddress = false
showingWalletPicker = false
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 WalletManager not closed after use — resource leak

WalletManager is constructed and used to fetch the address, but there is no close() call (unlike the Android counterpart at EnterAddressView.kt:303 which calls wm.close()). Since WalletManager wraps a native Rust object via UniFFI, skipping cleanup leaks the underlying file handles / DB connections each time the user selects a wallet.

Task {
    guard let wm = try? WalletManager(id: wallet.id) else { return }
    defer { wm.close() }          // add this
    guard let addrInfo = try? await wm.firstAddress() else { return }
    address = addrInfo.addressUnformatted()
    selectedWallet = wallet
    showRawAddress = false
    showingWalletPicker = false
}

.sheet(isPresented: $showingWalletPicker) {
NavigationView {
List {
let wallets = (try? Database().wallets().allSortedActive()) ?? []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Database I/O runs on every view re-render

Database().wallets().allSortedActive() is placed directly inside the List body with no memoization. In SwiftUI, the body property is called on every state change (e.g., the showingWalletPicker sheet being animated open/closed), so this synchronous database call fires on the main thread repeatedly. Consider loading the wallet list once into a @State variable via .task on the sheet.

Comment on lines +294 to +306
coroutineScope.launch {
try {
val wm = WalletManager(wallet.id)
val addressInfo = wm.firstAddress()
address = addressInfo.address().unformatted()
onAddressChanged(address)
selectedWallet = wallet
showRawAddress = false
showingWalletPicker = false
wm.close()
} catch (e: Exception) {
Log.w(tag, "Error wallet items: ${e.message}")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Silent failure on address fetch — no user feedback

When WalletManager(wallet.id) or wm.firstAddress() throws, the error is logged but the bottom sheet remains open with no indication to the user that the selection failed. The same gap exists in the iOS code (try? swallows errors silently). Consider showing a toast or Snackbar so the user knows they need to retry.

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.

@oyetanishq we should address this

Comment on lines +297 to +298
val addressInfo = wm.firstAddress()
address = addressInfo.address().unformatted()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 firstAddress() reuses the same address on repeated sends

Both platforms call firstAddress() / wm.firstAddress() to populate the destination. HD wallets derive a fresh address per receipt for on-chain privacy. If the user sends to the same wallet multiple times, they always use the same address, linking transactions. Consider using nextUnusedAddress() (or equivalent) if the API provides it.

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.

@oyetanishq i think we should be using nextAddress not first address

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt (1)

146-167: Avoid !! on a var — capture to a local val for a clean smart-cast.

selectedWallet is a var, so the compiler can't smart-cast after the != null check, forcing selectedWallet!!.name. Idiomatic Kotlin is to capture to a local val and branch on that.

-        if (selectedWallet != null) {
+        val dest = selectedWallet
+        if (dest != null) {
             Column(modifier = Modifier.fillMaxWidth()) {
                 ...
                     Text(
-                        selectedWallet!!.name,
+                        dest.name,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt`
around lines 146 - 167, The code uses selectedWallet!! inside the UI block
because selectedWallet is a var and not smart-cast; fix this by immediately
capturing it into a local val (e.g., val wallet = selectedWallet) after the
null-check and use wallet.name (and any other wallet fields) throughout the
Column/Row/Text block (replace selectedWallet!! occurrences). Update any
references inside EnterAddressView's UI block to use the local val to avoid the
!! operator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt`:
- Around line 185-198: The "Show address" control currently only sets
showRawAddress = true and never collapses; update the clickable handler to
toggle showRawAddress instead of only setting true, and update the UI
text/content to reflect both states (e.g., show "Hide address" or the raw
address when showRawAddress is true, and "Show address" when false). Locate the
branch around showRawAddress in EnterAddressView (the Text composable with
modifier = Modifier.clickable { ... }) and change the onClick to toggle the
Boolean and adjust the visible text/content so the control mirrors a reveal/hide
toggle.
- Around line 280-286: The current catch in the remember block that reads
Database().wallets().allSortedActive() swallows exceptions and returns an empty
list; change the catch to log the throwable (not just .message) using the app's
logging utility (e.g., Timber.e or Log.e) including a clear context message like
"Failed to load wallets" and the caught exception, then return emptyList() as
fallback; update the catch around Database().wallets().allSortedActive() in
EnterAddressView's wallets remember block to include that logging of the
throwable.
- Around line 293-308: The WalletManager instance (WalletManager(wallet.id)) may
leak on exceptions because wm.close() is only called on the success path; wrap
its lifetime so it is always closed (either use Kotlin's use { } on
WalletManager if it implements Closeable/AutoCloseable, or create a try { ... }
finally { wm.close() } around the calls to firstAddress() and downstream logic
in the clickable coroutineScope block) and move the state updates (address,
onAddressChanged, selectedWallet, showRawAddress, showingWalletPicker) inside
that protected scope; also change the catch logging to pass the exception object
as the third argument to Log.w (e.g., Log.w(tag, "Error fetching first address
for ${wallet.id}", e)).

In `@ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift`:
- Around line 72-82: The current UI in EnterAddressView uses a one-way reveal
controlled by the showRawAddress Bool so once shown it cannot be hidden; change
the Button logic to toggle showRawAddress instead of only setting it true and
update the button label to show "Show address" when showRawAddress is false and
"Hide address" when true (the Text(address) branch stays the same), ensuring the
Button that currently reads "Show address" toggles the state; locate
showRawAddress and the Button in EnterAddressView.swift to implement this simple
toggle behavior.
- Around line 95-122: Replace the deprecated NavigationView with NavigationStack
for the wallet picker sheet; hoist the database query out of the List into an
`@State` var (e.g. wallets: [Wallet] = []) and populate it in a .task when
showingWalletPicker (call Database().wallets().allSortedActive() once) instead
of calling it inside the List body; remove the try? that swallows errors around
WalletManager(id:) and await wm.firstAddress() and surface failures via the
app's alertState (or at least log them) so that failures update alertState and
present to the user instead of silently failing; keep the existing assignments
to address, selectedWallet, showRawAddress and showingWalletPicker on success.

---

Nitpick comments:
In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt`:
- Around line 146-167: The code uses selectedWallet!! inside the UI block
because selectedWallet is a var and not smart-cast; fix this by immediately
capturing it into a local val (e.g., val wallet = selectedWallet) after the
null-check and use wallet.name (and any other wallet fields) throughout the
Column/Row/Text block (replace selectedWallet!! occurrences). Update any
references inside EnterAddressView's UI block to use the local val to avoid the
!! operator.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e07c400e-1c67-40db-83fb-455279fdeac7

📥 Commits

Reviewing files that changed from the base of the PR and between 6da45e9 and 1352321.

📒 Files selected for processing (3)
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt
  • android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowSetAmountScreen.kt
  • ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift

Comment on lines +185 to +198
if (showRawAddress) {
Text(
text = address,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
)
} else {
Text(
text = "Show address",
color = CoveColor.LinkBlue,
fontSize = 12.sp,
modifier = Modifier.clickable { showRawAddress = true }
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Show address" is one-way — mirror a hide/show toggle.

Once tapped, showRawAddress stays true with no way back to the collapsed state. A simple toggle is more user-friendly and matches the reveal/hide pattern used for sensitive data elsewhere.

-                if (showRawAddress) {
-                    Text(
-                        text = address,
-                        color = MaterialTheme.colorScheme.onSurfaceVariant,
-                        fontSize = 12.sp,
-                    )
-                } else {
-                    Text(
-                        text = "Show address",
-                        color = CoveColor.LinkBlue,
-                        fontSize = 12.sp,
-                        modifier = Modifier.clickable { showRawAddress = true }
-                    )
-                }
+                Text(
+                    text = if (showRawAddress) "Hide address" else "Show address",
+                    color = CoveColor.LinkBlue,
+                    fontSize = 12.sp,
+                    modifier = Modifier.clickable { showRawAddress = !showRawAddress }
+                )
+                if (showRawAddress) {
+                    Spacer(Modifier.height(4.dp))
+                    Text(
+                        text = address,
+                        color = MaterialTheme.colorScheme.onSurfaceVariant,
+                        fontSize = 12.sp,
+                    )
+                }
📝 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
if (showRawAddress) {
Text(
text = address,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
)
} else {
Text(
text = "Show address",
color = CoveColor.LinkBlue,
fontSize = 12.sp,
modifier = Modifier.clickable { showRawAddress = true }
)
}
Text(
text = if (showRawAddress) "Hide address" else "Show address",
color = CoveColor.LinkBlue,
fontSize = 12.sp,
modifier = Modifier.clickable { showRawAddress = !showRawAddress }
)
if (showRawAddress) {
Spacer(Modifier.height(4.dp))
Text(
text = address,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt`
around lines 185 - 198, The "Show address" control currently only sets
showRawAddress = true and never collapses; update the clickable handler to
toggle showRawAddress instead of only setting true, and update the UI
text/content to reflect both states (e.g., show "Hide address" or the raw
address when showRawAddress is true, and "Show address" when false). Locate the
branch around showRawAddress in EnterAddressView (the Text composable with
modifier = Modifier.clickable { ... }) and change the onClick to toggle the
Boolean and adjust the visible text/content so the control mirrors a reveal/hide
toggle.

Comment on lines +280 to +286
val wallets = remember {
try {
Database().wallets().allSortedActive().filter { it.id != currentWalletId }
} catch (e: Exception) {
emptyList()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Swallowed exception when loading wallets.

detekt flagged this correctly: on a DB read failure the user sees an empty list with no diagnostic. Log the exception (with the throwable, not just .message) so failures are diagnosable in the field.

                 val wallets = remember {
                     try {
                         Database().wallets().allSortedActive().filter { it.id != currentWalletId }
                     } catch (e: Exception) {
+                        Log.w(tag, "Failed to load wallets for picker", e)
                         emptyList()
                     }
                 }
📝 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
val wallets = remember {
try {
Database().wallets().allSortedActive().filter { it.id != currentWalletId }
} catch (e: Exception) {
emptyList()
}
}
val wallets = remember {
try {
Database().wallets().allSortedActive().filter { it.id != currentWalletId }
} catch (e: Exception) {
Log.w(tag, "Failed to load wallets for picker", e)
emptyList()
}
}
🧰 Tools
🪛 detekt (1.23.8)

[warning] 283-283: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt`
around lines 280 - 286, The current catch in the remember block that reads
Database().wallets().allSortedActive() swallows exceptions and returns an empty
list; change the catch to log the throwable (not just .message) using the app's
logging utility (e.g., Timber.e or Log.e) including a clear context message like
"Failed to load wallets" and the caught exception, then return emptyList() as
fallback; update the catch around Database().wallets().allSortedActive() in
EnterAddressView's wallets remember block to include that logging of the
throwable.

Comment on lines +293 to +308
.clickable {
coroutineScope.launch {
try {
val wm = WalletManager(wallet.id)
val addressInfo = wm.firstAddress()
address = addressInfo.address().unformatted()
onAddressChanged(address)
selectedWallet = wallet
showRawAddress = false
showingWalletPicker = false
wm.close()
} catch (e: Exception) {
Log.w(tag, "Error wallet items: ${e.message}")
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm WalletManager implements AutoCloseable (so .use{} or .close() is required/available)
rg -nP --type=kotlin -C2 '\bclass\s+WalletManager\b'
rg -nP -C3 'WalletManager' -g '*.kt' | rg -i 'AutoCloseable|Closeable|close\(\)' | head -40

Repository: bitcoinppl/cove

Length of output: 2886


WalletManager resource leak on the failure path — ensure .close() is always called.

wm.close() only executes after firstAddress() succeeds. If firstAddress() or any intermediate call throws, control jumps to the catch block and the WalletManager UniFFI handle is never released. Per the FFI Resource Management guideline, Closeable UniFFI objects must always be closed. Additionally, Log.w(tag, "Error wallet items: ${e.message}") should pass the exception object as the second argument: Log.w(tag, "Error wallet items: ${e.message}", e).

🛡️ Proposed fix using try/finally
                                .clickable {
                                    coroutineScope.launch {
-                                        try {
-                                            val wm = WalletManager(wallet.id)
-                                            val addressInfo = wm.firstAddress()
-                                            address = addressInfo.address().unformatted()
-                                            onAddressChanged(address)
-                                            selectedWallet = wallet
-                                            showRawAddress = false
-                                            showingWalletPicker = false
-                                            wm.close()
-                                        } catch (e: Exception) {
-                                            Log.w(tag, "Error wallet items: ${e.message}")
-                                        }
+                                        val wm = try {
+                                            WalletManager(wallet.id)
+                                        } catch (e: Exception) {
+                                            Log.w(tag, "Failed to open wallet ${wallet.id}", e)
+                                            return@launch
+                                        }
+                                        try {
+                                            val addressInfo = wm.firstAddress()
+                                            address = addressInfo.address().unformatted()
+                                            onAddressChanged(address)
+                                            selectedWallet = wallet
+                                            showRawAddress = false
+                                            showingWalletPicker = false
+                                        } catch (e: Exception) {
+                                            Log.w(tag, "Error fetching first address for ${wallet.id}", e)
+                                        } finally {
+                                            wm.close()
+                                        }
+                                    }
                                 }

Alternatively, use .use {} if WalletManager implements AutoCloseable (it does via Closeable):

WalletManager(wallet.id).use { wm ->
    try {
        val addressInfo = wm.firstAddress()
        address = addressInfo.address().unformatted()
        onAddressChanged(address)
        selectedWallet = wallet
        showRawAddress = false
        showingWalletPicker = false
    } catch (e: Exception) {
        Log.w(tag, "Error fetching first address for ${wallet.id}", e)
    }
}
📝 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
.clickable {
coroutineScope.launch {
try {
val wm = WalletManager(wallet.id)
val addressInfo = wm.firstAddress()
address = addressInfo.address().unformatted()
onAddressChanged(address)
selectedWallet = wallet
showRawAddress = false
showingWalletPicker = false
wm.close()
} catch (e: Exception) {
Log.w(tag, "Error wallet items: ${e.message}")
}
}
}
.clickable {
coroutineScope.launch {
val wm = try {
WalletManager(wallet.id)
} catch (e: Exception) {
Log.w(tag, "Failed to open wallet ${wallet.id}", e)
return@launch
}
try {
val addressInfo = wm.firstAddress()
address = addressInfo.address().unformatted()
onAddressChanged(address)
selectedWallet = wallet
showRawAddress = false
showingWalletPicker = false
} catch (e: Exception) {
Log.w(tag, "Error fetching first address for ${wallet.id}", e)
} finally {
wm.close()
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/EnterAddressView.kt`
around lines 293 - 308, The WalletManager instance (WalletManager(wallet.id))
may leak on exceptions because wm.close() is only called on the success path;
wrap its lifetime so it is always closed (either use Kotlin's use { } on
WalletManager if it implements Closeable/AutoCloseable, or create a try { ... }
finally { wm.close() } around the calls to firstAddress() and downstream logic
in the clickable coroutineScope block) and move the state updates (address,
onAddressChanged, selectedWallet, showRawAddress, showingWalletPicker) inside
that protected scope; also change the catch logging to pass the exception object
as the third argument to Log.w (e.g., Log.w(tag, "Error fetching first address
for ${wallet.id}", e)).

Comment on lines +72 to +82
if showRawAddress {
Text(address)
.font(.caption)
.foregroundColor(.secondary)
} else {
Button("Show address") {
showRawAddress = true
}
.font(.caption)
.foregroundColor(.blue)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Show address" is one-way — no way to hide it again.

Once the user taps "Show address", showRawAddress stays true for the rest of the flow (only reset on wallet selection). Consider making this a toggle (Show addressHide address) so users can collapse the raw string again after verifying, matching typical reveal/hide patterns for sensitive fields.

🔁 Proposed toggle
-                    if showRawAddress {
-                        Text(address)
-                            .font(.caption)
-                            .foregroundColor(.secondary)
-                    } else {
-                        Button("Show address") {
-                            showRawAddress = true
-                        }
-                        .font(.caption)
-                        .foregroundColor(.blue)
-                    }
+                    Button(showRawAddress ? "Hide address" : "Show address") {
+                        showRawAddress.toggle()
+                    }
+                    .font(.caption)
+                    .foregroundColor(.blue)
+                    if showRawAddress {
+                        Text(address)
+                            .font(.caption)
+                            .foregroundColor(.secondary)
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift` around lines
72 - 82, The current UI in EnterAddressView uses a one-way reveal controlled by
the showRawAddress Bool so once shown it cannot be hidden; change the Button
logic to toggle showRawAddress instead of only setting it true and update the
button label to show "Show address" when showRawAddress is false and "Hide
address" when true (the Text(address) branch stays the same), ensuring the
Button that currently reads "Show address" toggles the state; locate
showRawAddress and the Button in EnterAddressView.swift to implement this simple
toggle behavior.

Comment on lines +95 to +122
.sheet(isPresented: $showingWalletPicker) {
NavigationView {
List {
let wallets = (try? Database().wallets().allSortedActive()) ?? []
ForEach(wallets.filter { $0.id != presenter.manager.id }, id: \.id) { wallet in
Button(action: {
Task {
if let wm = try? WalletManager(id: wallet.id),
let addrInfo = try? await wm.firstAddress()
{
address = addrInfo.addressUnformatted()
selectedWallet = wallet
showRawAddress = false
showingWalletPicker = false
}
}
}) {
HStack {
Text(wallet.name)
Spacer()
}
}
}
}
.navigationTitle("Select Wallet")
.navigationBarItems(trailing: Button("Cancel") { showingWalletPicker = false })
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Is NavigationView deprecated in iOS 16+ and should new SwiftUI code use NavigationStack?

💡 Result:

Yes, NavigationView is deprecated in iOS 16+ (and equivalent versions on other platforms). Apple recommends that new SwiftUI code targeting iOS 16+ use NavigationStack for stack-based navigation (replacing NavigationView with .stack style) and NavigationSplitView for multi-column navigation.

Citations:


🏁 Script executed:

# Find iOS deployment target
find . -name "*.xcodeproj" -o -name "project.pbxproj" -o -name "*.plist" | head -20

Repository: bitcoinppl/cove

Length of output: 390


🏁 Script executed:

# Check for iOS minimum version in common config files
rg "IPHONEOS_DEPLOYMENT_TARGET|platform.*ios|mobileOS" --type swift --type yaml --type json ios/ | head -20

Repository: bitcoinppl/cove

Length of output: 41


🏁 Script executed:

# Search for NavigationStack usage in the codebase
rg "NavigationStack" ios/ --type swift | head -20

Repository: bitcoinppl/cove

Length of output: 1660


🏁 Script executed:

# Search for NavigationView usage patterns
rg "NavigationView" ios/ --type swift | head -20

Repository: bitcoinppl/cove

Length of output: 150


🏁 Script executed:

# Look for similar database query patterns
rg "Database()\." ios/ --type swift -A 2 | head -30

Repository: bitcoinppl/cove

Length of output: 475


🏁 Script executed:

# Extract iOS deployment target from project.pbxproj
grep -i "IPHONEOS_DEPLOYMENT_TARGET" ios/Cove.xcodeproj/project.pbxproj | head -5

Repository: bitcoinppl/cove

Length of output: 214


🏁 Script executed:

# Read the actual EnterAddressView.swift file
wc -l ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift

Repository: bitcoinppl/cove

Length of output: 125


🏁 Script executed:

# Search for `@State` patterns with task/onAppear for lazy loading
rg "@State.*\n.*task\(|@State.*\n.*onAppear" ios/Cove --type swift -A 3 | head -30

Repository: bitcoinppl/cove

Length of output: 250


🏁 Script executed:

# Look for error handling patterns in sheets
rg "\.sheet.*\{|\.alert.*error" ios/Cove --type swift -B 2 -A 5 | head -40

Repository: bitcoinppl/cove

Length of output: 3069


🏁 Script executed:

# Read the full EnterAddressView.swift file
cat -n ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift

Repository: bitcoinppl/cove

Length of output: 6523


🏁 Script executed:

# Search for `@State` patterns that load data
rg "@State.*var.*=" ios/Cove --type swift -A 2 | grep -A 2 "task\|onAppear" | head -30

Repository: bitcoinppl/cove

Length of output: 620


🏁 Script executed:

# Check how errors are typically surfaced in this app (alertState pattern)
rg "app.alertState.*=" ios/Cove --type swift | head -15

Repository: bitcoinppl/cove

Length of output: 2026


Replace deprecated NavigationView with NavigationStack and surface errors in wallet picker sheet.

The wallet picker sheet has three issues:

  1. NavigationView is deprecated as of iOS 16 and this project targets iOS 18.4; use NavigationStack instead.
  2. The database query on line 98 (Database().wallets().allSortedActive()) executes inside the List body and re-runs on every sheet body re-evaluation. Hoist it to an @State property and load it in .task to execute once when the sheet appears.
  3. Lines 102–103 silently swallow errors from WalletManager initialization and address derivation with try?. If either fails, the user sees no feedback. Surface errors using the app's alertState pattern or at minimum log them.
♻️ Suggested refactor
-    `@State` private var showingWalletPicker: Bool = false
+    `@State` private var showingWalletPicker: Bool = false
+    `@State` private var pickerWallets: [WalletMetadata] = []
+    `@State` private var pickerError: String?
...
-        .sheet(isPresented: $showingWalletPicker) {
-            NavigationView {
-                List {
-                    let wallets = (try? Database().wallets().allSortedActive()) ?? []
-                    ForEach(wallets.filter { $0.id != presenter.manager.id }, id: \.id) { wallet in
+        .sheet(isPresented: $showingWalletPicker) {
+            NavigationStack {
+                List {
+                    ForEach(pickerWallets.filter { $0.id != presenter.manager.id }, id: \.id) { wallet in
                         Button(action: {
                             Task {
-                                if let wm = try? WalletManager(id: wallet.id),
-                                   let addrInfo = try? await wm.firstAddress()
-                                {
+                                do {
+                                    let wm = try WalletManager(id: wallet.id)
+                                    let addrInfo = try await wm.firstAddress()
                                     address = addrInfo.addressUnformatted()
                                     selectedWallet = wallet
                                     showRawAddress = false
                                     showingWalletPicker = false
+                                } catch {
+                                    pickerError = error.localizedDescription
                                 }
                             }
                         }) { ... }
                     }
                 }
+                .task {
+                    pickerWallets = (try? Database().wallets().allSortedActive()) ?? []
+                }
                 .navigationTitle("Select Wallet")
                 .navigationBarItems(trailing: Button("Cancel") { showingWalletPicker = false })
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/Flows/SendFlow/SetAmountScreen/EnterAddressView.swift` around lines
95 - 122, Replace the deprecated NavigationView with NavigationStack for the
wallet picker sheet; hoist the database query out of the List into an `@State` var
(e.g. wallets: [Wallet] = []) and populate it in a .task when
showingWalletPicker (call Database().wallets().allSortedActive() once) instead
of calling it inside the List body; remove the try? that swallows errors around
WalletManager(id:) and await wm.firstAddress() and surface failures via the
app's alertState (or at least log them) so that failures update alertState and
present to the user instead of silently failing; keep the existing assignments
to address, selectedWallet, showRawAddress and showingWalletPicker on success.

@praveenperera
Copy link
Copy Markdown
Contributor

Thanks @oyetanishq this looks good, please address some of the issues, but I will work with the designer to improve the design before merging, so I will turn this into a draft PR for now.

@praveenperera praveenperera marked this pull request as draft April 20, 2026 19:03
@praveenperera praveenperera added needs design Needs design, UX discussions with Adrian before continuing BLOCKED Something is blocking this issue from being worked on labels Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BLOCKED Something is blocking this issue from being worked on needs design Needs design, UX discussions with Adrian before continuing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow sending between wallets

2 participants