Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
4c0d88c
Bump Android and iOS build numbers
praveenperera Sep 7, 2026
1c18472
Count confirmed cloud backups as recovery coverage
praveenperera Sep 9, 2026
a199379
Own cloud-only wallet dialogs per action button
praveenperera Sep 9, 2026
47871ce
Preserve coverage after wallet-set changes
praveenperera Sep 10, 2026
3254b9f
Invalidate coverage on integrity downgrade
praveenperera Sep 10, 2026
547f885
Bump iOS build number to 118
praveenperera Sep 11, 2026
bd999b6
Confirm trusted local cloud inventory with provider
praveenperera Sep 11, 2026
2d888b1
Fix wipe-PIN launch after full wipe
praveenperera Sep 11, 2026
5d5e09f
Preserve setup flag across full wipe
praveenperera Sep 11, 2026
6922b72
Reject redirected AASA fetches in iOS xtask
praveenperera Sep 12, 2026
77dc8c0
Require user verification for Android passkeys
praveenperera Sep 12, 2026
a9a3754
Add passkey request timing diagnostics on iOS
praveenperera Sep 12, 2026
e427ee4
Handoff cloud backup actions after prompt dismiss
praveenperera Sep 12, 2026
757f312
Harden cloud backup passkey match and restore
praveenperera Sep 12, 2026
c7aa988
Isolate Rust test databases under temp dirs
praveenperera Sep 12, 2026
560e834
Cover wallet clear cancel and send-flow cleanup
praveenperera Sep 12, 2026
58a86a8
Bump iOS build number to 119
praveenperera Sep 12, 2026
463fa7e
Add just recipe to upload TestFlight without bump
praveenperera Sep 12, 2026
b5995e0
Add tf and utf aliases for TestFlight recipes
praveenperera Sep 12, 2026
61e8e78
Initialize Rust test storage before first access
praveenperera Sep 12, 2026
cea3845
Remove wallet session data during deletion
praveenperera Sep 12, 2026
51bb68e
Clear wallet managers and sensitive sessions
praveenperera Sep 12, 2026
290d241
Wait for iOS prompts before starting actions
praveenperera Sep 12, 2026
4e243e7
Release iOS passkey requests on the main thread
praveenperera Sep 12, 2026
100f839
Bump iOS build number to 121
praveenperera Sep 12, 2026
d1f9e67
Simplify xtask argument parsing and ownership
praveenperera Sep 12, 2026
084ad44
Automate beta release distribution
praveenperera Sep 12, 2026
6958bb2
Preserve cloud backup passkey request errors
praveenperera Sep 12, 2026
815df66
Cancel stale actions when a new prompt is queued
praveenperera Sep 12, 2026
dbb114c
Preserve backup coverage after cancelled checks
praveenperera Sep 12, 2026
feb84cb
Remove timing from lifecycle preparation test
praveenperera Sep 12, 2026
93a779f
Reuse settled iCloud metadata generations
praveenperera Sep 14, 2026
f549848
Keep restore download order with open slots
praveenperera Sep 14, 2026
7bf345a
Compose iOS TestFlight from bump, bir, upload
praveenperera Sep 14, 2026
ba8bf37
Move Google Play release into xtask
praveenperera Sep 14, 2026
2e99c50
Bump iOS build and Android versionCode
praveenperera Sep 14, 2026
3c13272
Adopt matching leftover Keychain on restore
praveenperera Sep 17, 2026
025b054
Report local Keychain conflicts on restore
praveenperera Sep 17, 2026
6b68d9b
Soften cancelled passkey request copy
praveenperera Sep 17, 2026
320b6dd
Hide deleted iCloud paths from metadata
praveenperera Sep 18, 2026
02d898a
Bump iOS build number to 123
praveenperera Sep 18, 2026
69e7669
Bump iOS build number to 124
praveenperera Sep 18, 2026
b6ab63e
Reuse mock passkey auth default result
praveenperera Sep 18, 2026
029b0fc
Record coordinator-resolved iCloud delete URLs
praveenperera Sep 18, 2026
c2c3e4f
Add after-save hook to mock keychain
praveenperera Sep 18, 2026
e068f88
Commit restore keychain and state together
praveenperera Sep 18, 2026
f74450a
Make wallet conflict copy platform-neutral
praveenperera Sep 18, 2026
c453948
Harden legacy test dir cleanup logic
praveenperera Sep 18, 2026
8264bf9
Keep sticky auth default out of queue
praveenperera Sep 18, 2026
7169abd
Extract simplified cloud restore module
praveenperera Sep 18, 2026
141dc0f
Bundle restore namespace commit args
praveenperera Sep 18, 2026
0e3b991
Drop legacy home test dir cleanup
praveenperera Sep 18, 2026
61b0898
Share wallet test helpers in test_support
praveenperera Sep 18, 2026
12e9675
Isolate payjoin tests from lifecycle wipe
praveenperera Sep 18, 2026
98d44ad
Make TestFlight mock sockets blocking
praveenperera Sep 18, 2026
cf5e9ab
Roll back failed restore namespace commit
praveenperera Sep 18, 2026
891dc82
Keep matches after passkey auth failure
praveenperera Sep 21, 2026
5bb5b32
Share release tooling helpers in xtask
praveenperera Sep 30, 2026
8c85740
Simplify cloud backup restore and wipe paths
praveenperera Sep 30, 2026
8a957a7
Let Rust own the state after a full wipe
praveenperera Sep 30, 2026
a49849f
Delete KeyTeleport session under its store lock
praveenperera Sep 30, 2026
5514c94
Replace visible state first after Android wipe
praveenperera Sep 30, 2026
bbcb73e
Keep TestFlight build number after upload starts
praveenperera Sep 30, 2026
6609a43
Bump iOS build number to 125
praveenperera Sep 30, 2026
c5e8ed6
Drop redundant cloud backup passkey tests
praveenperera Oct 1, 2026
ae764c3
Drop xtask Google Play CLI parse tests
praveenperera Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export COVE_KEYSTORE_PASSWORD="your-keystore-password-here"
export COVE_KEY_ALIAS="upload"
export COVE_KEY_PASSWORD="your-key-password-here"

# Google Play internal releases: just rela (requires fastlane)
# grant this service account access to testing releases for org.bitcoinppl.cove
# export GOOGLE_PLAY_JSON_KEY_PATH="$HOME/.secrets/cove-google-play.json"

# iOS device aliases for: just brall --device main|se
# export IOS_DEVICE_MAIN="00008120-0006243420214032"
# export IOS_DEVICE_SE="00008xxx-xxxxxxxxxxxxxxxx"
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ This pattern is used throughout the codebase for shared resources and is safe to

redb stores typed table metadata for each table and validates the key and value `TypeName` on `open_table`. Treat changes to `TableDefinition`, `Value::type_name()`, persisted database type names, and module paths for persisted types as compatibility-sensitive. `std::any::type_name::<T>()` uses the type's defining module path, not a public re-export path, so moving a persisted type into or out of a nested module can change on-disk expectations even when serialized bytes stay identical. Preserve exact historical type names or add a compatibility/migration path, and test every install path that could have created the table, including short-lived beta/internal builds. See [docs/redb.md](docs/redb.md) for the redb compatibility checklist.

**Destructive operations.** The process-wide wallet lifecycle coordinator owns persistent wallet construction, writes, actor registration, deletion, and full wipe. Deletion cannot get a prepared capability until current construction and persistence operations drain and all registered actors terminate. A failed or timed-out terminal request is cancelled, resumes the manager when possible, and returns a typed retry authorization; it never permits a force-delete path. Full wipe also requires a drained Cloud Backup reset permit. Inventory covers every network and main or decoy mode, and deletion uses those exact durable locations. Wallet deletion removes secrets first, address-switch and BDK artifacts second, wallet data third, and exact metadata rows last. Missing artifacts are idempotent, but a missing prepared metadata row is an invariant failure. The main database resets only after every local secret, artifact, restore marker, lock, and diagnostics log is removed. Address-type switches need no journal: the replacement store is published with one atomic rename, every post-publication path moves live state forward, and wallet load heals metadata to match the store it finds.
**Destructive operations.** The process-wide wallet lifecycle coordinator owns persistent wallet construction, writes, actor registration, deletion, and full wipe. Deletion cannot get a prepared capability until current construction and persistence operations drain and all registered actors terminate. A failed or timed-out terminal request is cancelled, resumes the manager when possible, and returns a typed retry authorization; it never permits a force-delete path. Full wipe also requires a drained Cloud Backup reset permit. Inventory covers every network and main or decoy mode, and deletion uses those exact durable locations. Wallet deletion removes secrets first, address-switch and BDK artifacts second, wallet data third, and exact metadata rows last. Missing artifacts are idempotent, but a missing prepared metadata row is an invariant failure. The main database resets only after every local secret, artifact, restore marker, lock, and diagnostics log is removed. A successful full wipe returns a `FullWipeCompletion` with the committed post-wipe app and authentication state. Frontends apply it before releasing authentication and clear only the presentation state they own, so no frontend decides on its own what a wiped app looks like. Address-type switches need no journal: the replacement store is published with one atomic rename, every post-publication path moves live state forward, and wallet load heals metadata to match the store it finds.

**Database tables:**

Expand Down
8 changes: 6 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ Set `ASC_API_KEY_PATH`, `ASC_API_KEY_ID`, and `ASC_API_ISSUER_ID`. The API key m
### Android

```bash
just build-android-release # alias: just bar
just release-android # alias: just rela
```

Then build a signed APK/AAB via Android Studio (Build → Generate Signed Bundle/APK).
This bumps the Android versionCode, rebuilds the release Android bindings, builds the signed Play bundle, and uploads it to Google Play internal testing.

Set `GOOGLE_PLAY_JSON_KEY_PATH` to a readable Google Play service account JSON file. Install fastlane (`brew install fastlane`). The service account must have access to testing releases for `org.bitcoinppl.cove`.

Use `just upload-google-play` (`just ugp`) when the versionCode was already bumped and the signed bundle already exists.

## Development Workflow

Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ android {
applicationId = "org.bitcoinppl.cove"
minSdk = 33
targetSdk = 36
versionCode = 39
versionCode = 41
versionName = "1.4.0"
manifestPlaceholders["keyTeleportAppLinksEnabled"] = "false"

Expand Down
48 changes: 41 additions & 7 deletions android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -369,22 +369,58 @@ class AppManager private constructor() : FfiReconcile {
* clears all cached data and reinitializes
*/
fun reset() {
clearSessionForReset()
resetRustProjection()
}

private fun clearSessionForReset() {
// close managers before clearing them
clearWalletManager()
clearKeyTeleportManager()
corruptedWalletDeletionRetry = null
}

private fun resetRustProjection() {
database = Database()
needsOnboarding =
withRustOr(needsOnboarding) {
needsOnboarding()
}

val routerState =
withRustOr(null) {
withRustOr<AppState?>(null) {
state()
}
router.reset(routerState?.router)
} ?: return

router.reset(routerState.router)
}

/**
* Apply the committed post-wipe state from Rust before authentication is released
*
* Rust decides what a wiped app looks like; this only clears Android-owned presentation state
*/
internal fun applyWipeCompletion(completion: FullWipeCompletion) {
// replace everything visible first so a cleanup failure below cannot leave pre-wipe state on screen
router.isSidebarVisible = false
isLoading = false
alertState = null
sheetState = null
needsOnboarding = completion.needsOnboarding
selectedNetwork = completion.selectedNetwork
colorSchemeSelection = completion.colorScheme
selectedNode = completion.selectedNode
selectedFiatCurrency = completion.fiatCurrency
wallets = completion.wallets
router.reset(completion.router)

// the data is already gone, so a manager cleanup failure must not keep the app locked
runCatching {
clearSessionForReset()
database = Database()
}.onFailure { error ->
Log.e(tag, "failed to clear the app session after wipe", error)
}
}

val currentRoute: Route
Expand Down Expand Up @@ -822,17 +858,15 @@ class AppManager private constructor() : FfiReconcile {
unverifiedWalletIds()
}

internal fun dangerousWipeAllData() {
internal fun dangerousWipeAllData(): FullWipeCompletion =
withRust {
dangerousWipeAllData()
}
}

internal fun retryDangerousWipeAllData(attemptId: ShutdownAttemptId) {
internal fun retryDangerousWipeAllData(attemptId: ShutdownAttemptId): FullWipeCompletion =
withRust {
retryDangerousWipeAllData(attemptId)
}
}

internal fun cancelDangerousWipe(attemptId: ShutdownAttemptId) {
withRust {
Expand Down
63 changes: 32 additions & 31 deletions android/app/src/main/java/org/bitcoinppl/cove/AuthManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ sealed interface WipePresentationState {
data object Idle : WipePresentationState
data object Running : WipePresentationState
data class ShutdownBlocked(val attemptId: ShutdownAttemptId) : WipePresentationState
data class Failed(val message: String) : WipePresentationState
data object Failed : WipePresentationState
}

internal const val WIPE_FAILURE_TITLE = "Unable to Open Cove"
internal const val WIPE_FAILURE_MESSAGE = "Please try again."

/**
* auth manager - manages authentication state
* ported from iOS AuthManager.swift
Expand All @@ -72,8 +75,7 @@ class AuthManager internal constructor(
private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val wipeCommand = OwnerScopedCommand<UnlockMode>(mainScope)

private var rust: RustAuthManager = RustAuthManager()
private set
private val rust = RustAuthManager()
private val isRustClosed = AtomicBoolean(false)
private val rustGuard =
RustHandleGuard(
Expand Down Expand Up @@ -190,9 +192,7 @@ class AuthManager internal constructor(
}

internal fun completeMainBiometricAuthentication() {
if (isInDecoyMode()) {
switchToMainMode()
}
if (isInDecoyMode() && !switchToMainMode()) return

recordMainCredentialAuthentication()
unlock()
Expand Down Expand Up @@ -258,8 +258,8 @@ class AuthManager internal constructor(
}

private fun unlockWithMainPin(): UnlockMode {
if (Database().globalConfig().isInDecoyMode()) {
switchToMainMode()
if (Database().globalConfig().isInDecoyMode() && !switchToMainMode()) {
return UnlockMode.LOCKED
}

recordMainCredentialAuthentication()
Expand Down Expand Up @@ -318,49 +318,56 @@ class AuthManager internal constructor(
}
}

result.exceptionOrNull()?.let { error ->
val lifecycle = (error as? AppException.WalletLifecycle)?.v1
val wipeError = result.exceptionOrNull()
if (wipeError != null) {
val lifecycle = (wipeError as? AppException.WalletLifecycle)?.v1
if (lifecycle is WalletLifecycleFailure.ShutdownBlocked) {
wipePresentationState = WipePresentationState.ShutdownBlocked(lifecycle.attemptId)
} else {
android.util.Log.e(tag, "failed to wipe all data", error)
wipePresentationState =
WipePresentationState.Failed(error.message ?: "Unable to remove local data")
android.util.Log.e(tag, "failed to wipe all data", wipeError)
wipePresentationState = WipePresentationState.Failed
}

return UnlockMode.LOCKED
}

val oldRust = rust
rust = RustAuthManager()
rustGuard.markOpen()
rust.listenForUpdates(this)
oldRust.close()
val completion = result.getOrThrow()
App.applyWipeCompletion(completion)
apply(completion.auth)

unlock()
type = AuthType.NONE
wipePresentationState = WipePresentationState.Idle
App.reset()

return UnlockMode.WIPE
}

private fun apply(settings: AuthSettings) {
type = settings.authType
isWipeDataPinEnabled = settings.isWipeDataPinEnabled
isDecoyPinEnabled = settings.isDecoyPinEnabled
isUsingBiometrics = false
}

private fun recordMainCredentialAuthentication() {
mainCredentialGeneration += 1
}

/**
* switch to main mode from decoy mode
*
* returns false when the switch failed so callers keep the app locked
* instead of unlocking into the decoy projection with the main credential
*/
fun switchToMainMode() {
fun switchToMainMode(): Boolean =
try {
withRust {
switchToMainMode()
}
resetAppAndSelectWallet()
true
} catch (e: Exception) {
android.util.Log.e(tag, "failed to switch to main mode", e)
false
}
}

override fun reconcile(message: AuthManagerReconcileMessage) {
logDebug("reconcile: $message")
Expand All @@ -371,17 +378,11 @@ class AuthManager internal constructor(
}

is AuthManagerReconcileMessage.WipeDataPinChanged -> {
isWipeDataPinEnabled =
withRustOr(isWipeDataPinEnabled) {
isWipeDataPinEnabled()
}
isWipeDataPinEnabled = message.v1
}

is AuthManagerReconcileMessage.DecoyPinChanged -> {
isDecoyPinEnabled =
withRustOr(isDecoyPinEnabled) {
isDecoyPinEnabled()
}
isDecoyPinEnabled = message.v1
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,10 @@ class MainActivity : FragmentActivity() {
return@setContent
}

if (bdkMigrationWarning != null) {
val app = remember { AppManager.getInstance() }
val auth = remember { AuthManager.getInstance() }

if (bdkMigrationWarning != null && auth.wipePresentationState != WipePresentationState.Running) {
AlertDialog(
onDismissRequest = { bdkMigrationWarning = null },
title = { Text("Encryption Migration Issue") },
Expand All @@ -506,8 +509,6 @@ class MainActivity : FragmentActivity() {
)
}

val app = remember { AppManager.getInstance() }
val auth = remember { AuthManager.getInstance() }
val snackbarHostState = remember { SnackbarHostState() }
var startupMode by remember {
mutableStateOf(resolveStartupMode(app.needsOnboarding))
Expand Down
Loading
Loading