Skip to content

Commit cc6acac

Browse files
committed
Add watch-only wallet upgrade UI with import options
When a watch-only wallet tries to send, present options to upgrade it by importing hardware wallet keys (QR, NFC, paste) or seed words (QR, NFC, 12/24 words). Adds set_wallet_type FFI method, new WatchOnlyImportHardware/WatchOnlyImportWords alert states, and WalletNotFound database error.
1 parent fe2501a commit cc6acac

File tree

7 files changed

+336
-21
lines changed

7 files changed

+336
-21
lines changed

android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import org.bitcoinppl.cove_core.AfterPinAction
7777
import org.bitcoinppl.cove_core.AlertDisplayType
7878
import org.bitcoinppl.cove_core.AppAction
7979
import org.bitcoinppl.cove_core.AppAlertState
80+
import org.bitcoinppl.cove_core.ColdWalletRoute
8081
import org.bitcoinppl.cove_core.Database
8182
import org.bitcoinppl.cove_core.HotWalletRoute
8283
import org.bitcoinppl.cove_core.ImportType
@@ -85,6 +86,8 @@ import org.bitcoinppl.cove_core.NumberOfBip39Words
8586
import org.bitcoinppl.cove_core.Route
8687
import org.bitcoinppl.cove_core.RouteFactory
8788
import org.bitcoinppl.cove_core.TapSignerRoute
89+
import org.bitcoinppl.cove_core.Wallet
90+
import org.bitcoinppl.cove_core.WalletType
8891
import org.bitcoinppl.cove_core.types.ColorSchemeSelection
8992

9093
class MainActivity : FragmentActivity() {
@@ -415,6 +418,7 @@ private fun GlobalAlertDialog(
415418
}
416419

417420
is AppAlertState.HotWalletKeyMissing -> {
421+
val walletId = state.walletId
418422
AlertDialog(
419423
onDismissRequest = onDismiss,
420424
title = { Text(state.title()) },
@@ -429,6 +433,13 @@ private fun GlobalAlertDialog(
429433
onDismiss()
430434
app.resetRoute(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWENTY_FOUR, ImportType.MANUAL))))
431435
}) { Text("Import 24 Words") }
436+
TextButton(onClick = {
437+
onDismiss()
438+
try {
439+
app.rust.setWalletType(walletId, WalletType.COLD)
440+
} catch (_: Exception) {
441+
}
442+
}) { Text("Use with Hardware Wallet") }
432443
}
433444
},
434445
dismissButton = {
@@ -689,6 +700,105 @@ private fun GlobalAlertDialog(
689700
)
690701
}
691702

703+
is AppAlertState.CantSendOnWatchOnlyWallet -> {
704+
AlertDialog(
705+
onDismissRequest = onDismiss,
706+
title = { Text(state.title()) },
707+
text = { Text(state.message()) },
708+
confirmButton = {
709+
Column {
710+
TextButton(onClick = {
711+
onDismiss()
712+
app.alertState = TaggedItem(AppAlertState.WatchOnlyImportHardware)
713+
}) { Text("Import Hardware Wallet") }
714+
TextButton(onClick = {
715+
onDismiss()
716+
app.alertState = TaggedItem(AppAlertState.WatchOnlyImportWords)
717+
}) { Text("Import Words") }
718+
}
719+
},
720+
dismissButton = {
721+
TextButton(onClick = onDismiss) { Text("Cancel") }
722+
},
723+
)
724+
}
725+
726+
is AppAlertState.WatchOnlyImportHardware -> {
727+
AlertDialog(
728+
onDismissRequest = onDismiss,
729+
title = { Text(state.title()) },
730+
text = { Text(state.message()) },
731+
confirmButton = {
732+
Column {
733+
TextButton(onClick = {
734+
onDismiss()
735+
app.loadAndReset(Route.NewWallet(NewWalletRoute.ColdWallet(ColdWalletRoute.QR_CODE)))
736+
}) { Text("QR Code") }
737+
TextButton(onClick = {
738+
onDismiss()
739+
app.scanNfc()
740+
}) { Text("NFC") }
741+
TextButton(onClick = {
742+
onDismiss()
743+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
744+
val text =
745+
clipboard.primaryClip
746+
?.getItemAt(0)
747+
?.text
748+
?.toString()
749+
if (!text.isNullOrBlank()) {
750+
try {
751+
val wallet = Wallet.newFromXpub(xpub = text.trim())
752+
val id = wallet.id()
753+
app.rust.selectWallet(id)
754+
app.resetRoute(Route.SelectedWallet(id))
755+
} catch (e: Exception) {
756+
app.alertState =
757+
TaggedItem(
758+
AppAlertState.ErrorImportingHardwareWallet(e.message ?: "Unknown error"),
759+
)
760+
}
761+
}
762+
}) { Text("Paste") }
763+
}
764+
},
765+
dismissButton = {
766+
TextButton(onClick = onDismiss) { Text("Cancel") }
767+
},
768+
)
769+
}
770+
771+
is AppAlertState.WatchOnlyImportWords -> {
772+
AlertDialog(
773+
onDismissRequest = onDismiss,
774+
title = { Text(state.title()) },
775+
text = { Text(state.message()) },
776+
confirmButton = {
777+
Column {
778+
TextButton(onClick = {
779+
onDismiss()
780+
app.loadAndReset(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWENTY_FOUR, ImportType.QR))))
781+
}) { Text("Scan QR") }
782+
TextButton(onClick = {
783+
onDismiss()
784+
app.loadAndReset(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWENTY_FOUR, ImportType.NFC))))
785+
}) { Text("NFC") }
786+
TextButton(onClick = {
787+
onDismiss()
788+
app.loadAndReset(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWELVE, ImportType.MANUAL))))
789+
}) { Text("12 Words") }
790+
TextButton(onClick = {
791+
onDismiss()
792+
app.loadAndReset(Route.NewWallet(NewWalletRoute.HotWallet(HotWalletRoute.Import(NumberOfBip39Words.TWENTY_FOUR, ImportType.MANUAL))))
793+
}) { Text("24 Words") }
794+
}
795+
},
796+
dismissButton = {
797+
TextButton(onClick = onDismiss) { Text("Cancel") }
798+
},
799+
)
800+
}
801+
692802
else -> {
693803
AlertDialog(
694804
onDismissRequest = onDismiss,

android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,8 @@ external fun uniffi_cove_checksum_method_ffiapp_select_latest_or_new_wallet(
11431143
): Short
11441144
external fun uniffi_cove_checksum_method_ffiapp_select_wallet(
11451145
): Short
1146+
external fun uniffi_cove_checksum_method_ffiapp_set_wallet_type(
1147+
): Short
11461148
external fun uniffi_cove_checksum_method_ffiapp_state(
11471149
): Short
11481150
external fun uniffi_cove_checksum_method_ffiapp_unverified_wallet_ids(
@@ -1951,6 +1953,8 @@ external fun uniffi_cove_fn_method_ffiapp_select_latest_or_new_wallet(`ptr`: Lon
19511953
): Unit
19521954
external fun uniffi_cove_fn_method_ffiapp_select_wallet(`ptr`: Long,`id`: RustBufferWalletId.ByValue,`nextRoute`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus,
19531955
): Unit
1956+
external fun uniffi_cove_fn_method_ffiapp_set_wallet_type(`ptr`: Long,`id`: RustBufferWalletId.ByValue,`walletType`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus,
1957+
): Unit
19541958
external fun uniffi_cove_fn_method_ffiapp_state(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus,
19551959
): RustBuffer.ByValue
19561960
external fun uniffi_cove_fn_method_ffiapp_unverified_wallet_ids(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus,
@@ -3467,6 +3471,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
34673471
if (lib.uniffi_cove_checksum_method_ffiapp_select_wallet() != 51673.toShort()) {
34683472
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
34693473
}
3474+
if (lib.uniffi_cove_checksum_method_ffiapp_set_wallet_type() != 40849.toShort()) {
3475+
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
3476+
}
34703477
if (lib.uniffi_cove_checksum_method_ffiapp_state() != 49253.toShort()) {
34713478
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
34723479
}
@@ -9131,6 +9138,11 @@ public interface FfiAppInterface {
91319138
*/
91329139
fun `selectWallet`(`id`: WalletId, `nextRoute`: Route? = null)
91339140

9141+
/**
9142+
* Update a wallet's type and persist to database
9143+
*/
9144+
fun `setWalletType`(`id`: WalletId, `walletType`: WalletType)
9145+
91349146
fun `state`(): AppState
91359147

91369148
/**
@@ -9652,6 +9664,22 @@ open class FfiApp: Disposable, AutoCloseable, FfiAppInterface
96529664

96539665

96549666

9667+
9668+
/**
9669+
* Update a wallet's type and persist to database
9670+
*/
9671+
@Throws(DatabaseException::class)override fun `setWalletType`(`id`: WalletId, `walletType`: WalletType)
9672+
=
9673+
callWithHandle {
9674+
uniffiRustCallWithError(DatabaseException) { _status ->
9675+
UniffiLib.uniffi_cove_fn_method_ffiapp_set_wallet_type(
9676+
it,
9677+
FfiConverterTypeWalletId.lower(`id`),FfiConverterTypeWalletType.lower(`walletType`),_status)
9678+
}
9679+
}
9680+
9681+
9682+
96559683
override fun `state`(): AppState {
96569684
return FfiConverterTypeAppState.lift(
96579685
callWithHandle {
@@ -26940,6 +26968,12 @@ sealed class AppAlertState: Disposable {
2694026968
object ConfirmWatchOnly : AppAlertState()
2694126969

2694226970

26971+
object WatchOnlyImportHardware : AppAlertState()
26972+
26973+
26974+
object WatchOnlyImportWords : AppAlertState()
26975+
26976+
2694326977
data class UninitializedTapSigner(
2694426978
val `tapSigner`: org.bitcoinppl.cove_core.tapcard.TapSigner) : AppAlertState()
2694526979

@@ -27114,6 +27148,10 @@ sealed class AppAlertState: Disposable {
2711427148
}
2711527149
is AppAlertState.ConfirmWatchOnly -> {// Nothing to destroy
2711627150
}
27151+
is AppAlertState.WatchOnlyImportHardware -> {// Nothing to destroy
27152+
}
27153+
is AppAlertState.WatchOnlyImportWords -> {// Nothing to destroy
27154+
}
2711727155
is AppAlertState.UninitializedTapSigner -> {
2711827156

2711927157
Disposable.destroy(
@@ -27257,13 +27295,15 @@ public object FfiConverterTypeAppAlertState : FfiConverterRustBuffer<AppAlertSta
2725727295
)
2725827296
25 -> AppAlertState.Loading
2725927297
26 -> AppAlertState.ConfirmWatchOnly
27260-
27 -> AppAlertState.UninitializedTapSigner(
27298+
27 -> AppAlertState.WatchOnlyImportHardware
27299+
28 -> AppAlertState.WatchOnlyImportWords
27300+
29 -> AppAlertState.UninitializedTapSigner(
2726127301
FfiConverterTypeTapSigner.read(buf),
2726227302
)
27263-
28 -> AppAlertState.TapSignerWalletFound(
27303+
30 -> AppAlertState.TapSignerWalletFound(
2726427304
FfiConverterTypeWalletId.read(buf),
2726527305
)
27266-
29 -> AppAlertState.InitializedTapSigner(
27306+
31 -> AppAlertState.InitializedTapSigner(
2726727307
FfiConverterTypeTapSigner.read(buf),
2726827308
)
2726927309
else -> throw RuntimeException("invalid enum value, something is very wrong!!")
@@ -27449,6 +27489,18 @@ public object FfiConverterTypeAppAlertState : FfiConverterRustBuffer<AppAlertSta
2744927489
4UL
2745027490
)
2745127491
}
27492+
is AppAlertState.WatchOnlyImportHardware -> {
27493+
// Add the size for the Int that specifies the variant plus the size needed for all fields
27494+
(
27495+
4UL
27496+
)
27497+
}
27498+
is AppAlertState.WatchOnlyImportWords -> {
27499+
// Add the size for the Int that specifies the variant plus the size needed for all fields
27500+
(
27501+
4UL
27502+
)
27503+
}
2745227504
is AppAlertState.UninitializedTapSigner -> {
2745327505
// Add the size for the Int that specifies the variant plus the size needed for all fields
2745427506
(
@@ -27600,18 +27652,26 @@ public object FfiConverterTypeAppAlertState : FfiConverterRustBuffer<AppAlertSta
2760027652
buf.putInt(26)
2760127653
Unit
2760227654
}
27603-
is AppAlertState.UninitializedTapSigner -> {
27655+
is AppAlertState.WatchOnlyImportHardware -> {
2760427656
buf.putInt(27)
27657+
Unit
27658+
}
27659+
is AppAlertState.WatchOnlyImportWords -> {
27660+
buf.putInt(28)
27661+
Unit
27662+
}
27663+
is AppAlertState.UninitializedTapSigner -> {
27664+
buf.putInt(29)
2760527665
FfiConverterTypeTapSigner.write(value.`tapSigner`, buf)
2760627666
Unit
2760727667
}
2760827668
is AppAlertState.TapSignerWalletFound -> {
27609-
buf.putInt(28)
27669+
buf.putInt(30)
2761027670
FfiConverterTypeWalletId.write(value.`walletId`, buf)
2761127671
Unit
2761227672
}
2761327673
is AppAlertState.InitializedTapSigner -> {
27614-
buf.putInt(29)
27674+
buf.putInt(31)
2761527675
FfiConverterTypeTapSigner.write(value.`tapSigner`, buf)
2761627676
Unit
2761727677
}
@@ -30193,6 +30253,12 @@ sealed class DatabaseException: kotlin.Exception() {
3019330253
get() = "v1=${ v1 }"
3019430254
}
3019530255

30256+
class WalletNotFound(
30257+
) : DatabaseException() {
30258+
override val message
30259+
get() = ""
30260+
}
30261+
3019630262

3019730263

3019830264

@@ -30248,6 +30314,7 @@ public object FfiConverterTypeDatabaseError : FfiConverterRustBuffer<DatabaseExc
3024830314
9 -> DatabaseException.Serialization(
3024930315
FfiConverterTypeSerdeError.read(buf),
3025030316
)
30317+
10 -> DatabaseException.WalletNotFound()
3025130318
else -> throw RuntimeException("invalid error enum value, something is very wrong!!")
3025230319
}
3025330320
}
@@ -30299,6 +30366,10 @@ public object FfiConverterTypeDatabaseError : FfiConverterRustBuffer<DatabaseExc
3029930366
4UL
3030030367
+ FfiConverterTypeSerdeError.allocationSize(value.v1)
3030130368
)
30369+
is DatabaseException.WalletNotFound -> (
30370+
// Add the size for the Int that specifies the variant plus the size needed for all fields
30371+
4UL
30372+
)
3030230373
}
3030330374
}
3030430375

@@ -30349,6 +30420,10 @@ public object FfiConverterTypeDatabaseError : FfiConverterRustBuffer<DatabaseExc
3034930420
FfiConverterTypeSerdeError.write(value.v1, buf)
3035030421
Unit
3035130422
}
30423+
is DatabaseException.WalletNotFound -> {
30424+
buf.putInt(10)
30425+
Unit
30426+
}
3035230427
}.let { /* this makes the `when` an expression, which ensures it is exhaustive */ }
3035330428
}
3035430429

@@ -43482,9 +43557,6 @@ enum class WalletType {
4348243557
HOT,
4348343558
COLD,
4348443559
XPUB_ONLY,
43485-
/**
43486-
* deprecated, use XpubOnly instead
43487-
*/
4348843560
WATCH_ONLY;
4348943561

4349043562

0 commit comments

Comments
 (0)