refactor(identity): put an Identity in front of the wallet, and read the key off it
Phase 1 of docs/nsec-sign-in.md. No behaviour changes; this is the type the
rest of the plan stands on, landed alone so that its diff is boring.
Every consumer of "the active user" in the app was the same expression,
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
ten times in eight files, and none of them wanted anything from the node but
those 32 bytes. The new to.curare.compose.identity.Identity carries the nostr
private key, the x-only public key (computed once), the per-id preferences and
an optional PhoenixBusiness, with an IdentityKind saying whether it came from
twelve words or a bare secret. It replaces fr.acinq.phoenix.data.ActiveWallet
in the app rather than wrapping it: ActiveWallet is declared in the library but
nothing in the library reads it, and flattening its fields into Identity is
what lets the recovery screens, which read internalPrefs off the active
wallet, keep working for an identity that will never have a wallet.
SovereignWalletViewModel.activeWalletInUI becomes activeIdentity;
setActiveWallet(walletId, business) builds the Mnemonic identity from the node
it just started and delegates to a new setActiveIdentity(identity), which is
the entry the nsec kind will use. The twenty-five StateFlow<ActiveWallet?>
parameters across eighteen files become StateFlow<Identity?>, a rename.
The ten sites become a read of the identity's key. Two got simpler:
NotaryViewModel dropped the flatMapLatest from the wallet flow into the node's
key-manager flow, which only existed because the key arrived after the node,
and RelaysSocketManager likewise; both keep their per-key cancellation.
NavigationViewModel.observeProfile lost its `business == null -> Startup`
branch -- the one thing that would have made a nodeless identity loop -- and
the "Couldn't get the nostrKey" dead end, since an identity is whole from the
moment it exists. The unconstructed NostrNotaryRepository now takes the
identity flow instead of a WalletManager, so that it compiles against the
same seam.
WalletId stays as the id for both kinds. Every preference in the library keys
on it, and a second id type would mean a second copy of each; for an nsec
identity XonlyPublicKey.toWalletId() gives hash160 of the x-only key, the same
forty-hex shape as hash160 of a node id.
Tests: NavigationRoutingTest keeps passing with the flow retyped, and a new
jvm test pins the two things this refactor is for. A NostrSecret identity with
business = null and a synced account on the device routes to ProfileLoaded
rather than back to startup; and Identity.nostrPublicKey equals the key quartz
derives for the same secret -- the x-only form, sixty-four hex characters,
which is not what the library's own LocalKeyManager.nostrPublicKey() returns.
That test runs under runBlocking with a real-time timeout because the
observer waits 2.1 seconds on Dispatchers.IO, and runTest's virtual clock
would time out first.
The app's WalletManagerExtension.nostrPublicKey() stays: CreateProfileViewModel
still derives a fresh key's pubkey with it. The plan said it could go; it was
wrong by one caller.
Verified with :composeApp:compileDebugKotlinAndroid, :composeApp:jvmTest (863
tests) and :composeApp:m3Audit.
Replayed onto Mantra by docs/curated-to-mantra.md: RelaysSocketManager.kt: this commit's import block taken whole -- Mantra had already moved the nostrPublicKey() import to the library's (39fb64b6), and this commit removes the call it served; NavigationViewModel.kt: this commit's import block taken whole -- Mantra had already moved the nostrPublicKey() import to the library's (39fb64b6), and this commit removes the call it served.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pulled-From: curated/curated@42f3a6971a
This commit is contained in:
1 parent
c49914818a
commit
bfba7e186f
25 files changed
+406
-165
No files matched your search
@@ -0,0 +1,78 @@
|
||||
package press.mantra.compose.identity
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.bitcoin.Crypto
|
||||
import fr.acinq.bitcoin.PrivateKey
|
||||
import fr.acinq.bitcoin.XonlyPublicKey
|
||||
import fr.acinq.bitcoin.byteVector
|
||||
import fr.acinq.phoenix.PhoenixBusiness
|
||||
import fr.acinq.phoenix.data.WalletId
|
||||
import fr.acinq.phoenix.utils.preferences.InternalPrefs
|
||||
import fr.acinq.phoenix.utils.preferences.UserPrefs
|
||||
|
||||
/**
|
||||
* Where an identity's key came from, which decides what else it can have.
|
||||
*
|
||||
* The distinction is not cosmetic: the nostr key is a BIP32 leaf of the seed at
|
||||
* `m/44'/1237'/0'/0/0`, and that derivation runs one way. Twelve words yield the
|
||||
* key and a wallet; a bare key yields nothing further, so an identity made from
|
||||
* one can never grow a node behind it. See docs/nsec-sign-in.md.
|
||||
*/
|
||||
enum class IdentityKind {
|
||||
/** Twelve words. The nostr key is derived from them, and so is a wallet. */
|
||||
Mnemonic,
|
||||
|
||||
/** A bare nostr secret. Nothing else can be derived from it. */
|
||||
NostrSecret,
|
||||
}
|
||||
|
||||
/**
|
||||
* The signing identity the app is running as.
|
||||
*
|
||||
* This is what every consumer of "the active user" reads, in place of reaching
|
||||
* through the wallet -- `business.walletManager.keyManager.value.nostrPrivateKey()`
|
||||
* -- for a key that only some identities derive that way. Everything downstream of
|
||||
* the key is already independent of the seed: the notary and the sync pumps build
|
||||
* a quartz `KeyPair` from its bytes, the ChillDKG host key is a hash of it, and the
|
||||
* Marmot bundle password is `hash160` of it. None of them can tell where it came
|
||||
* from, and after this type none of them need to.
|
||||
*
|
||||
* @param id keys the per-identity preference files and the wallet metadata. For a
|
||||
* [IdentityKind.Mnemonic] identity it is the hash160 of the node id, as it always
|
||||
* was; for a [IdentityKind.NostrSecret] identity it is the hash160 of the x-only
|
||||
* public key ([toWalletId]). Same shape, so nothing downstream can tell the kinds
|
||||
* apart by the id -- the kind is here, where it can be asked.
|
||||
* @param business the running node. Non-null only for [IdentityKind.Mnemonic].
|
||||
*/
|
||||
data class Identity(
|
||||
val id: WalletId,
|
||||
val kind: IdentityKind,
|
||||
val nostrPrivateKey: PrivateKey,
|
||||
val userPrefs: UserPrefs,
|
||||
val internalPrefs: InternalPrefs,
|
||||
val business: PhoenixBusiness?,
|
||||
) {
|
||||
/**
|
||||
* X-only, hex -- the form every nostr call site wants. Computed once.
|
||||
*
|
||||
* Not `publicKey().toHex()`: that is the 33-byte compressed encoding, sixty-six
|
||||
* hex characters, and it is what the library's own `nostrPublicKey()` returns.
|
||||
* Nostr keys are the 32-byte x coordinate.
|
||||
*/
|
||||
val nostrPublicKey: HexKey = nostrPrivateKey.publicKey().xOnly().value.toHex()
|
||||
|
||||
/** The key must never reach a log. `data class` would print it. */
|
||||
override fun toString(): String = "Identity(id=$id, kind=$kind, key=<redacted>)"
|
||||
}
|
||||
|
||||
/**
|
||||
* The [WalletId] of an identity that has no node id to hash.
|
||||
*
|
||||
* `WalletId(nodeId: PublicKey)` is the hash160 of the compressed node key; this is
|
||||
* the hash160 of the x-only nostr key. The two are over different keys -- the node
|
||||
* key sits at `m/50'/0'`, the nostr key at `m/44'/1237'/...` -- so a mnemonic wallet
|
||||
* and its own nostr identity would never share an id even if both were registered.
|
||||
* Which is exactly why a duplicate has to be caught by public key instead.
|
||||
*/
|
||||
fun XonlyPublicKey.toWalletId(): WalletId =
|
||||
WalletId(Crypto.hash160(value).byteVector().toHex())
|
||||
+10
-12
@@ -8,8 +8,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
|
||||
import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd
|
||||
import com.vitorpamplona.quartz.nip77Negentropy.NegMsgCmd
|
||||
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPublicKey
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -29,7 +28,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
* https://github.com/PrimalHQ/primal-android-app/blob/6db2e6862239335dede4162338d7c4f10ad35031/app/src/main/kotlin/net/primal/android/networking/relays/RelaysSocketManager.kt
|
||||
*/
|
||||
class RelaysSocketManager(
|
||||
private val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
private val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
private val nostrSocketClientFactory: press.mantra.compose.network.sockets.NostrSocketClientFactory,
|
||||
private val cachingImportRepository: press.mantra.compose.repository.CachingImportRepository,
|
||||
private val relayRepository: press.mantra.compose.repository.RelayRepository,
|
||||
@@ -61,19 +60,18 @@ class RelaysSocketManager(
|
||||
|
||||
private fun observeActiveUserId() =
|
||||
scope.launch {
|
||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
||||
if (activeWallet == null) {
|
||||
activeIdentityStateFlow.collectLatest { identity ->
|
||||
if (identity == null) {
|
||||
// TODO: Cancel all pending jobs?
|
||||
}
|
||||
activeWallet?.business?.walletManager?.keyManager?.collectLatest { keyManager ->
|
||||
keyManager?.nostrPublicKey()?.let { pubkey ->
|
||||
observeRelayJobs[pubkey]?.cancel()
|
||||
observeRelayJobs[pubkey] = observeRelays(pubkey)
|
||||
}
|
||||
// The identity carries its pubkey, so this no longer waits on the node's key
|
||||
// manager to fill in. The relay observer is still a job on `scope` keyed by
|
||||
// pubkey and replaced per pubkey, exactly as before.
|
||||
identity?.nostrPublicKey?.let { pubkey ->
|
||||
observeRelayJobs[pubkey]?.cancel()
|
||||
observeRelayJobs[pubkey] = observeRelays(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun observeRelays(publicKey: String): Job =
|
||||
|
||||
+4
-4
@@ -4,8 +4,8 @@ import press.mantra.compose.cryptography.signOrThrow
|
||||
import press.mantra.compose.network.asClientTag
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNsec
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
|
||||
import fr.acinq.phoenix.managers.WalletManager
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -23,7 +23,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
*/
|
||||
class NostrNotaryRepository(
|
||||
private val nostrRepository: NostrRepository,
|
||||
private val walletManager: WalletManager,
|
||||
private val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
@@ -67,7 +67,7 @@ class NostrNotaryRepository(
|
||||
|
||||
private fun findNsecOrThrow(activeUserPublicKey: String): String =
|
||||
runCatching {
|
||||
walletManager.keyManager.value?.nostrPrivateKey()?.value?.toByteArray()?.toNsec()
|
||||
activeIdentityStateFlow.value?.nostrPrivateKey?.value?.toByteArray()?.toNsec()
|
||||
}.getOrNull() ?: throw press.mantra.compose.exceptions.SigningKeyNotFoundException()
|
||||
|
||||
private fun signNostrEvent(publicKey: String, event: press.mantra.compose.database.model.UnsignedNostrEvent): press.mantra.compose.database.model.NostrEvent {
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ import press.mantra.compose.repository.DkgRepository
|
||||
import press.mantra.compose.ui.view.model.DkgRitualViewModel
|
||||
import press.mantra.compose.ui.view.state.DkgRitualUIState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||
@@ -65,7 +65,7 @@ internal fun DkgApprovalScaffold(
|
||||
activeUserPublicKey: HexKey,
|
||||
chatRoomId: String,
|
||||
dkgSessionId: String? = null,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
onDone: () -> Unit,
|
||||
@@ -76,7 +76,7 @@ internal fun DkgApprovalScaffold(
|
||||
chatRoomId = chatRoomId,
|
||||
dkgSessionId = dkgSessionId,
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
)
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import press.mantra.compose.database.model.types.DkgApprovalStep
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.DkgRepository
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
|
||||
@@ -33,7 +33,7 @@ fun DkgJoinApprovalScreen(
|
||||
activeUserPublicKey: HexKey,
|
||||
chatRoomId: String,
|
||||
dkgSessionId: String? = null,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
onDone: () -> Unit,
|
||||
@@ -45,7 +45,7 @@ fun DkgJoinApprovalScreen(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
chatRoomId = chatRoomId,
|
||||
dkgSessionId = dkgSessionId,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository,
|
||||
onDone = onDone
|
||||
|
||||
+4
-4
@@ -73,7 +73,7 @@ import press.mantra.compose.ui.view.model.DkgRitualViewModel
|
||||
import press.mantra.compose.ui.view.state.DkgRitualUIState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import press.mantra.compose.database.model.FrostSigningSession
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
@@ -135,7 +135,7 @@ fun DkgRitualScreen(
|
||||
/** Which of the room's ceremonies to show; null takes the newest. */
|
||||
dkgSessionId: String? = null,
|
||||
initialDkgRitualUIState: DkgRitualUIState = DkgRitualUIState.Loading,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
onNavigateToRoute: (Route) -> Unit = {},
|
||||
@@ -146,7 +146,7 @@ fun DkgRitualScreen(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
dkgSessionId = dkgSessionId,
|
||||
initialDkgRitualUIState = initialDkgRitualUIState,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
)
|
||||
@@ -882,7 +882,7 @@ private fun DkgRitualScreenPreview() {
|
||||
),
|
||||
)
|
||||
),
|
||||
activeWalletStateFlow = MutableStateFlow(null),
|
||||
activeIdentityStateFlow = MutableStateFlow(null),
|
||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||
dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY
|
||||
)
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import press.mantra.compose.database.model.types.DkgApprovalStep
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.DkgRepository
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import mantra.composeapp.generated.resources.Res
|
||||
@@ -37,7 +37,7 @@ fun DkgRound1ApprovalScreen(
|
||||
activeUserPublicKey: HexKey,
|
||||
chatRoomId: String,
|
||||
dkgSessionId: String? = null,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
onDone: () -> Unit,
|
||||
@@ -49,7 +49,7 @@ fun DkgRound1ApprovalScreen(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
chatRoomId = chatRoomId,
|
||||
dkgSessionId = dkgSessionId,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository,
|
||||
onDone = onDone
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import press.mantra.compose.database.model.types.DkgApprovalStep
|
||||
import press.mantra.compose.repository.ChatRepository
|
||||
import press.mantra.compose.repository.DkgRepository
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
import mantra.composeapp.generated.resources.Res
|
||||
@@ -38,7 +38,7 @@ fun DkgRound2ApprovalScreen(
|
||||
activeUserPublicKey: HexKey,
|
||||
chatRoomId: String,
|
||||
dkgSessionId: String? = null,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
onDone: () -> Unit,
|
||||
@@ -50,7 +50,7 @@ fun DkgRound2ApprovalScreen(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
chatRoomId = chatRoomId,
|
||||
dkgSessionId = dkgSessionId,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository,
|
||||
onDone = onDone
|
||||
|
||||
+6
-6
@@ -41,7 +41,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -81,16 +81,16 @@ import press.mantra.compose.ui.theme.spacing
|
||||
@Composable
|
||||
fun KeyRecoveryScreen(
|
||||
activeUserPublicKey: HexKey,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToRoute: (Route) -> Unit,
|
||||
) {
|
||||
val activeWallet by activeWalletStateFlow.collectAsState()
|
||||
val activeIdentity by activeIdentityStateFlow.collectAsState()
|
||||
|
||||
// The same preference the recovery phrase screen writes, so the state of the backup is
|
||||
// visible before the user goes looking for it.
|
||||
val showBackupNotice by remember(activeWallet) {
|
||||
activeWallet?.internalPrefs?.showSeedBackupNotice ?: flowOf(false)
|
||||
val showBackupNotice by remember(activeIdentity) {
|
||||
activeIdentity?.internalPrefs?.showSeedBackupNotice ?: flowOf(false)
|
||||
}.collectAsState(initial = false)
|
||||
|
||||
Scaffold(
|
||||
@@ -305,7 +305,7 @@ private fun KeyRecoveryScreenPreview() {
|
||||
) {
|
||||
KeyRecoveryScreen(
|
||||
activeUserPublicKey = "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
||||
activeWalletStateFlow = MutableStateFlow(null),
|
||||
activeIdentityStateFlow = MutableStateFlow(null),
|
||||
onNavigateBack = {},
|
||||
onNavigateToRoute = {}
|
||||
)
|
||||
|
||||
+3
-3
@@ -42,7 +42,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import fr.acinq.phoenix.PhoenixGlobal
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import mantra.composeapp.generated.resources.Res
|
||||
import mantra.composeapp.generated.resources.backup_confirmation
|
||||
@@ -82,13 +82,13 @@ import press.mantra.compose.ui.view.state.RecoveryPhraseUIState
|
||||
@Composable
|
||||
fun RecoveryPhraseScreen(
|
||||
phoenixGlobal: PhoenixGlobal,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
val recoveryPhraseViewModel: RecoveryPhraseViewModel = viewModel(
|
||||
factory = RecoveryPhraseViewModel.factory(
|
||||
phoenixGlobal = phoenixGlobal,
|
||||
activeWalletStateFlow = activeWalletStateFlow
|
||||
activeIdentityStateFlow = activeIdentityStateFlow
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+4
-4
@@ -51,7 +51,7 @@ import press.mantra.compose.ui.theme.MantraTheme
|
||||
import press.mantra.compose.ui.view.model.SelectChatRoomTypeViewModel
|
||||
import press.mantra.compose.ui.view.state.SelectChatRoomTypeUIState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import press.mantra.compose.ui.theme.spacing
|
||||
@@ -87,7 +87,7 @@ fun SelectChatRoomTypeScreen(
|
||||
description: String?,
|
||||
memberPublicKeys: List<HexKey>,
|
||||
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState = SelectChatRoomTypeUIState.Loading,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
nostrRepository: NostrRepository,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
@@ -100,7 +100,7 @@ fun SelectChatRoomTypeScreen(
|
||||
description = description,
|
||||
memberPublicKeys = memberPublicKeys,
|
||||
initialSelectChatRoomTypeUIState = initialSelectChatRoomTypeUIState,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
nostrRepository = nostrRepository,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
@@ -490,7 +490,7 @@ private fun SelectChatRoomTypeScreenPreview() {
|
||||
)
|
||||
)
|
||||
),
|
||||
activeWalletStateFlow = MutableStateFlow(null),
|
||||
activeIdentityStateFlow = MutableStateFlow(null),
|
||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
|
||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||
dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY,
|
||||
|
||||
+4
-4
@@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import mantra.composeapp.generated.resources.Res
|
||||
@@ -81,7 +81,7 @@ fun SelectSubgroupAdminsScreen(
|
||||
activeUserPublicKey: HexKey,
|
||||
parentChatRoomId: String,
|
||||
initialUIState: SelectSubgroupAdminsUIState = SelectSubgroupAdminsUIState.Loading,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository,
|
||||
onNavigateToRoute: (Route) -> Unit
|
||||
@@ -91,7 +91,7 @@ fun SelectSubgroupAdminsScreen(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
parentChatRoomId = parentChatRoomId,
|
||||
initialUIState = initialUIState,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
)
|
||||
@@ -331,7 +331,7 @@ private fun SelectSubgroupAdminsScreenPreview() {
|
||||
SelectSubgroupAdminsScreen(
|
||||
activeUserPublicKey = "a".repeat(64),
|
||||
parentChatRoomId = "b".repeat(64),
|
||||
activeWalletStateFlow = kotlinx.coroutines.flow.MutableStateFlow(null),
|
||||
activeIdentityStateFlow = kotlinx.coroutines.flow.MutableStateFlow(null),
|
||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||
dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY,
|
||||
onNavigateToRoute = {}
|
||||
|
||||
+6
-6
@@ -49,8 +49,8 @@ fun SovereignWalletStartupScreen(
|
||||
// Deliberately no intro gate. `getShowIntro` defaults to true and nothing in this app ever
|
||||
// writes it back to false, so gating on it navigated away from this screen on every cold boot
|
||||
// — and a screen that has been left behind never gets to start the wallet. Startup has to run
|
||||
// to completion here: everything downstream reads the node's key manager. An intro screen has
|
||||
// to be a step inside this flow, not a detour around it.
|
||||
// to completion here: everything downstream reads the active identity, and nothing sets one
|
||||
// but this screen. An intro screen has to be a step inside this flow, not a detour around it.
|
||||
|
||||
val listWalletState by sovereignWalletViewModel.listWalletState.collectAsState()
|
||||
|
||||
@@ -84,8 +84,8 @@ fun SovereignWalletStartupScreen(
|
||||
val desiredWalletIdFlow = sovereignWalletViewModel.desiredWalletId.collectAsState()
|
||||
val desiredWalletId = desiredWalletIdFlow.value
|
||||
|
||||
val activeWalletFlow = sovereignWalletViewModel.activeWalletInUI.collectAsState()
|
||||
val activeWallet = activeWalletFlow.value
|
||||
val activeIdentityFlow = sovereignWalletViewModel.activeIdentity.collectAsState()
|
||||
val activeIdentity = activeIdentityFlow.value
|
||||
|
||||
when {
|
||||
availableWallets.isEmpty() -> {
|
||||
@@ -99,12 +99,12 @@ fun SovereignWalletStartupScreen(
|
||||
text = "Preparing wallet..."
|
||||
)
|
||||
}
|
||||
activeWallet != null -> {
|
||||
activeIdentity != null -> {
|
||||
LoadingDataIndicator(
|
||||
text = "Opening wallet"
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
sovereignWalletViewModel.loadSovereignData(activeWallet.id)
|
||||
sovereignWalletViewModel.loadSovereignData(activeIdentity.id)
|
||||
onSuccessfulStartup.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -230,7 +230,7 @@ fun MantraNavHost(
|
||||
|
||||
val navigationViewModel: NavigationViewModel = viewModel (
|
||||
factory = NavigationViewModel.factory(
|
||||
activeWallet = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentity = sovereignWalletViewModel.activeIdentity,
|
||||
initialNavigationUIState = NavigationUIState.Loading(),
|
||||
nostrRepository = databaseNostrRepository,
|
||||
scope = applicationIOScope
|
||||
@@ -239,7 +239,7 @@ fun MantraNavHost(
|
||||
|
||||
val notaryViewModel: NotaryViewModel = viewModel(
|
||||
factory = NotaryViewModel.factory(
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
nostrRepository = databaseNostrRepository,
|
||||
chatRepository = databaseChatRepository,
|
||||
marmotRepository = databaseMarmotRepository,
|
||||
@@ -250,7 +250,7 @@ fun MantraNavHost(
|
||||
// TODO: Produce a notary UI Element...
|
||||
val synchronizationViewModel: SynchronizationViewModel = viewModel(
|
||||
factory = SynchronizationViewModel.factory(
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
nostrRepository = databaseNostrRepository,
|
||||
chatRepository = databaseChatRepository,
|
||||
relayRepository = databaseNostrRepository,
|
||||
@@ -548,7 +548,7 @@ fun MantraNavHost(
|
||||
SelectSubgroupAdminsScreen(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
parentChatRoomId = route.parentChatRoomId,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
chatRepository = databaseChatRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
onNavigateToRoute = { nextRoute ->
|
||||
@@ -563,7 +563,7 @@ fun MantraNavHost(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
chatRoomId = route.chatRoomId,
|
||||
dkgSessionId = route.dkgSessionId,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
chatRepository = databaseChatRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
onNavigateToRoute = { approvalRoute ->
|
||||
@@ -578,7 +578,7 @@ fun MantraNavHost(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
chatRoomId = route.chatRoomId,
|
||||
dkgSessionId = route.dkgSessionId,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
chatRepository = databaseChatRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
onDone = { navController.popBackStack() }
|
||||
@@ -591,7 +591,7 @@ fun MantraNavHost(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
chatRoomId = route.chatRoomId,
|
||||
dkgSessionId = route.dkgSessionId,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
chatRepository = databaseChatRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
onDone = { navController.popBackStack() }
|
||||
@@ -604,7 +604,7 @@ fun MantraNavHost(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
chatRoomId = route.chatRoomId,
|
||||
dkgSessionId = route.dkgSessionId,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
chatRepository = databaseChatRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
onDone = { navController.popBackStack() }
|
||||
@@ -618,7 +618,7 @@ fun MantraNavHost(
|
||||
name = route.name,
|
||||
description = route.description,
|
||||
memberPublicKeys = route.memberPublicKeys,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
nostrRepository = databaseNostrRepository,
|
||||
chatRepository = databaseChatRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
@@ -815,7 +815,7 @@ fun MantraNavHost(
|
||||
|
||||
KeyRecoveryScreen(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
@@ -829,7 +829,7 @@ fun MantraNavHost(
|
||||
composable<RecoveryPhraseRoute> {
|
||||
RecoveryPhraseScreen(
|
||||
phoenixGlobal = phoenixGlobal,
|
||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
||||
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
+7
-8
@@ -32,8 +32,7 @@ import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -56,7 +55,7 @@ class DkgRitualViewModel(
|
||||
*/
|
||||
val dkgSessionId: String? = null,
|
||||
initialDkgRitualUIState: DkgRitualUIState,
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
val chatRepository: ChatRepository,
|
||||
val dkgRepository: DkgRepository,
|
||||
): ViewModel() {
|
||||
@@ -473,7 +472,7 @@ class DkgRitualViewModel(
|
||||
|
||||
val loaded = dkgRitualUIState as? DkgRitualUIState.Loaded ?: return
|
||||
|
||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
||||
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||
if (nostrPrivateKey == null) {
|
||||
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
||||
return
|
||||
@@ -513,7 +512,7 @@ class DkgRitualViewModel(
|
||||
val loaded = dkgRitualUIState as? DkgRitualUIState.Loaded ?: return
|
||||
val session = loaded.session ?: return
|
||||
|
||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
||||
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||
if (nostrPrivateKey == null) {
|
||||
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
||||
return
|
||||
@@ -567,7 +566,7 @@ class DkgRitualViewModel(
|
||||
// guard rather than a path anybody takes.
|
||||
if (!loaded.isKeyStateSigned) return
|
||||
|
||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
||||
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||
if (nostrPrivateKey == null) {
|
||||
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
||||
return
|
||||
@@ -678,7 +677,7 @@ class DkgRitualViewModel(
|
||||
activeUserPublicKey: HexKey,
|
||||
dkgSessionId: String? = null,
|
||||
initialDkgRitualUIState: DkgRitualUIState = DkgRitualUIState.Loading,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
@@ -688,7 +687,7 @@ class DkgRitualViewModel(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
dkgSessionId = dkgSessionId,
|
||||
initialDkgRitualUIState = initialDkgRitualUIState,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
)
|
||||
|
||||
+15
-28
@@ -8,8 +8,7 @@ import press.mantra.compose.database.model.intermdiate.LocalAccount
|
||||
import press.mantra.compose.ui.composable.navigation.routes.SovereignWalletStartupRoute
|
||||
import press.mantra.compose.ui.view.state.NavigationUIState
|
||||
import co.touchlab.kermit.Logger
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPublicKey
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
@@ -25,7 +24,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class NavigationViewModel(
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
initialNavigationUIState: NavigationUIState,
|
||||
val nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||
val scope: CoroutineScope,
|
||||
@@ -38,14 +37,14 @@ class NavigationViewModel(
|
||||
private val PENDING_BROADCAST_STATUSES = setOf("pending", "processing")
|
||||
|
||||
fun factory(
|
||||
activeWallet: StateFlow<ActiveWallet?>,
|
||||
activeIdentity: StateFlow<Identity?>,
|
||||
initialNavigationUIState: NavigationUIState,
|
||||
nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||
scope: CoroutineScope,
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
NavigationViewModel(
|
||||
activeWallet,
|
||||
activeIdentity,
|
||||
initialNavigationUIState = initialNavigationUIState,
|
||||
nostrRepository = nostrRepository,
|
||||
scope = scope
|
||||
@@ -176,34 +175,22 @@ class NavigationViewModel(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(2_100.milliseconds) // Looking busy...
|
||||
|
||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
||||
logger.d("Active Wallet: $activeWallet")
|
||||
if (activeWallet == null) {
|
||||
|
||||
activeIdentityStateFlow.collectLatest { identity ->
|
||||
logger.d("Active identity: $identity")
|
||||
if (identity == null) {
|
||||
// Nothing has been started: go and start something.
|
||||
_navigationUIState.getAndUpdate {
|
||||
NavigationUIState.StartupPhoenix
|
||||
}
|
||||
} else {
|
||||
activeWallet.business.let { business ->
|
||||
if (business == null) {
|
||||
_navigationUIState.getAndUpdate {
|
||||
NavigationUIState.StartupPhoenix
|
||||
}
|
||||
} else {
|
||||
val activeUserPublicKey = business.walletManager.keyManager.value?.nostrPublicKey()
|
||||
// The identity carries its own key, so there is no node to wait on and no
|
||||
// "couldn't get the nostrKey" dead end. The branch that used to send a
|
||||
// nodeless identity back to startup went with it: an identity without a
|
||||
// node is now a kind of identity, not a wallet that has not finished
|
||||
// starting.
|
||||
logger.i("Observing: ${identity.nostrPublicKey}")
|
||||
|
||||
if (activeUserPublicKey == null) {
|
||||
_navigationUIState.getAndUpdate {
|
||||
NavigationUIState.Loading(text = "Couldn't get the nostrKey")
|
||||
}
|
||||
} else {
|
||||
logger.i("Observing: $activeUserPublicKey")
|
||||
|
||||
loadNostrProfile(activeUserPublicKey)
|
||||
}
|
||||
}
|
||||
logger.i("Navigation UI State is Landing")
|
||||
}
|
||||
loadNostrProfile(identity.nostrPublicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-14
@@ -11,19 +11,15 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import press.mantra.compose.identity.Identity
|
||||
import fr.acinq.phoenix.managers.nsecPassword
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.database.model.NostrEvent
|
||||
@@ -31,7 +27,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
class NotaryViewModel(
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
val nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||
val chatRepository: press.mantra.compose.repository.ChatRepository,
|
||||
val marmotRepository: MarmotRepository,
|
||||
@@ -43,7 +39,7 @@ class NotaryViewModel(
|
||||
|
||||
|
||||
fun factory(
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||
chatRepository: press.mantra.compose.repository.ChatRepository,
|
||||
marmotRepository: MarmotRepository,
|
||||
@@ -51,7 +47,7 @@ class NotaryViewModel(
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
NotaryViewModel(
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
nostrRepository = nostrRepository,
|
||||
chatRepository = chatRepository,
|
||||
marmotRepository = marmotRepository,
|
||||
@@ -63,14 +59,14 @@ class NotaryViewModel(
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun observeActiveNostrKey() {
|
||||
scope.launch {
|
||||
activeWalletStateFlow
|
||||
.flatMapLatest { activeWallet ->
|
||||
activeWallet?.business?.walletManager?.keyManager ?: flowOf(null)
|
||||
}
|
||||
.map { keyManager -> keyManager?.nostrPrivateKey() }
|
||||
// One flow, not a `flatMapLatest` into the node's key manager: the identity
|
||||
// carries its key from the moment it exists, so there is nothing to wait for.
|
||||
// `distinctUntilChanged` on the key stays -- an identity re-emitted with the same
|
||||
// key must not tear down and restart the observers below.
|
||||
activeIdentityStateFlow
|
||||
.map { identity -> identity?.nostrPrivateKey }
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { nostrPrivateKey ->
|
||||
if (nostrPrivateKey == null) return@collectLatest
|
||||
|
||||
+10
-10
@@ -7,7 +7,7 @@ import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import fr.acinq.phoenix.PhoenixGlobal
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import press.mantra.compose.identity.Identity
|
||||
import fr.acinq.phoenix.data.DecryptSeedResult
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -39,7 +39,7 @@ import press.mantra.compose.ui.view.state.RecoveryPhraseUIState
|
||||
*/
|
||||
class RecoveryPhraseViewModel(
|
||||
val phoenixGlobal: PhoenixGlobal,
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
): ViewModel() {
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
@@ -60,18 +60,18 @@ class RecoveryPhraseViewModel(
|
||||
* instead of drawing an unticked box over a ticked preference.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val isBackupDone: StateFlow<Boolean?> = activeWalletStateFlow
|
||||
val isBackupDone: StateFlow<Boolean?> = activeIdentityStateFlow
|
||||
.flatMapLatest { it?.internalPrefs?.isManualSeedBackupDone ?: flowOf(null) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val isDisclaimerRead: StateFlow<Boolean?> = activeWalletStateFlow
|
||||
val isDisclaimerRead: StateFlow<Boolean?> = activeIdentityStateFlow
|
||||
.flatMapLatest { it?.internalPrefs?.isSeedLossDisclaimerRead ?: flowOf(null) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
/** True until the user has both written the phrase down and read the disclaimer. */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val showBackupNotice: StateFlow<Boolean> = activeWalletStateFlow
|
||||
val showBackupNotice: StateFlow<Boolean> = activeIdentityStateFlow
|
||||
.flatMapLatest { it?.internalPrefs?.showSeedBackupNotice ?: flowOf(false) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
||||
|
||||
@@ -86,7 +86,7 @@ class RecoveryPhraseViewModel(
|
||||
fun revealRecoveryPhrase() {
|
||||
if (_recoveryPhraseUIState.value is RecoveryPhraseUIState.Revealing) return
|
||||
|
||||
val walletId = activeWalletStateFlow.value?.id
|
||||
val walletId = activeIdentityStateFlow.value?.id
|
||||
if (walletId == null) {
|
||||
logger.e { "no active wallet, cannot read a recovery phrase" }
|
||||
_recoveryPhraseUIState.value = RecoveryPhraseUIState.Error.NoActiveWallet
|
||||
@@ -128,7 +128,7 @@ class RecoveryPhraseViewModel(
|
||||
}
|
||||
|
||||
fun setBackupDone(isDone: Boolean) {
|
||||
val internalPrefs = activeWalletStateFlow.value?.internalPrefs ?: return
|
||||
val internalPrefs = activeIdentityStateFlow.value?.internalPrefs ?: return
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
|
||||
logger.e("could not save the backup flag", throwable)
|
||||
}) {
|
||||
@@ -137,7 +137,7 @@ class RecoveryPhraseViewModel(
|
||||
}
|
||||
|
||||
fun setDisclaimerRead(isRead: Boolean) {
|
||||
val internalPrefs = activeWalletStateFlow.value?.internalPrefs ?: return
|
||||
val internalPrefs = activeIdentityStateFlow.value?.internalPrefs ?: return
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
|
||||
logger.e("could not save the disclaimer flag", throwable)
|
||||
}) {
|
||||
@@ -150,12 +150,12 @@ class RecoveryPhraseViewModel(
|
||||
|
||||
fun factory(
|
||||
phoenixGlobal: PhoenixGlobal,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
RecoveryPhraseViewModel(
|
||||
phoenixGlobal = phoenixGlobal,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-6
@@ -30,8 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.async
|
||||
@@ -58,7 +57,7 @@ class SelectChatRoomTypeViewModel(
|
||||
val description: String?,
|
||||
val memberPublicKeys: List<HexKey>,
|
||||
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState,
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
val nostrRepository: NostrRepository,
|
||||
val chatRepository: ChatRepository,
|
||||
val dkgRepository: DkgRepository,
|
||||
@@ -190,7 +189,7 @@ class SelectChatRoomTypeViewModel(
|
||||
return
|
||||
}
|
||||
|
||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
||||
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||
if (nostrPrivateKey == null) {
|
||||
selectChatRoomTypeUIState = SelectChatRoomTypeUIState.Error(
|
||||
"Couldn't read your keys. Please try again."
|
||||
@@ -447,7 +446,7 @@ class SelectChatRoomTypeViewModel(
|
||||
description: String?,
|
||||
memberPublicKeys: List<HexKey>,
|
||||
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState = SelectChatRoomTypeUIState.Loading,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
nostrRepository: NostrRepository,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository
|
||||
@@ -459,7 +458,7 @@ class SelectChatRoomTypeViewModel(
|
||||
description = description,
|
||||
memberPublicKeys = memberPublicKeys,
|
||||
initialSelectChatRoomTypeUIState = initialSelectChatRoomTypeUIState,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
nostrRepository = nostrRepository,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
|
||||
+5
-7
@@ -12,8 +12,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -51,7 +50,7 @@ class SelectSubgroupAdminsViewModel(
|
||||
val activeUserPublicKey: HexKey,
|
||||
val parentChatRoomId: String,
|
||||
initialUIState: SelectSubgroupAdminsUIState,
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
val chatRepository: ChatRepository,
|
||||
val dkgRepository: DkgRepository,
|
||||
): ViewModel() {
|
||||
@@ -204,8 +203,7 @@ class SelectSubgroupAdminsViewModel(
|
||||
|
||||
val loaded = uiState as? SelectSubgroupAdminsUIState.Loaded ?: return
|
||||
|
||||
val nostrPrivateKey = activeWalletStateFlow.value
|
||||
?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
||||
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||
if (nostrPrivateKey == null) {
|
||||
uiState = SelectSubgroupAdminsUIState.Error("Couldn't read your keys. Please try again.")
|
||||
return
|
||||
@@ -293,7 +291,7 @@ class SelectSubgroupAdminsViewModel(
|
||||
activeUserPublicKey: HexKey,
|
||||
parentChatRoomId: String,
|
||||
initialUIState: SelectSubgroupAdminsUIState = SelectSubgroupAdminsUIState.Loading,
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
chatRepository: ChatRepository,
|
||||
dkgRepository: DkgRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
@@ -302,7 +300,7 @@ class SelectSubgroupAdminsViewModel(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
parentChatRoomId = parentChatRoomId,
|
||||
initialUIState = initialUIState,
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
chatRepository = chatRepository,
|
||||
dkgRepository = dkgRepository
|
||||
)
|
||||
|
||||
+49
-14
@@ -12,13 +12,13 @@ import co.touchlab.kermit.Logger
|
||||
import fr.acinq.lightning.logging.error
|
||||
import fr.acinq.phoenix.PhoenixBusiness
|
||||
import fr.acinq.phoenix.PhoenixGlobal
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.data.DecryptSeedResult
|
||||
import fr.acinq.phoenix.data.ElectrumConfig
|
||||
import fr.acinq.phoenix.data.ListWalletState
|
||||
import fr.acinq.phoenix.data.UserWallet
|
||||
import fr.acinq.phoenix.data.WalletId
|
||||
import fr.acinq.phoenix.managers.DataStoreManager
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
|
||||
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
@@ -33,6 +33,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import press.mantra.compose.identity.Identity
|
||||
import press.mantra.compose.identity.IdentityKind
|
||||
|
||||
sealed class WritingSeedState {
|
||||
data object Init : WritingSeedState()
|
||||
@@ -83,8 +85,17 @@ class SovereignWalletViewModel(
|
||||
val desiredWalletId = _desiredWalletId.asStateFlow()
|
||||
val startWalletImmediately = MutableStateFlow(true)
|
||||
|
||||
private val _activeWalletInUI = MutableStateFlow<ActiveWallet?>(null)
|
||||
val activeWalletInUI = _activeWalletInUI.asStateFlow()
|
||||
/**
|
||||
* The identity the app is running as, or null while nothing has been started.
|
||||
*
|
||||
* This replaces what used to be a flow of the active *wallet*: every reader of it
|
||||
* only ever wanted the nostr key, and reached it through the node --
|
||||
* `business.walletManager.keyManager.value.nostrPrivateKey()`. An [Identity]
|
||||
* carries the key directly, so a reader no longer needs a node to exist, and an
|
||||
* identity that never has one (a bare nsec) reads the same as one that does.
|
||||
*/
|
||||
private val _activeIdentity = MutableStateFlow<Identity?>(null)
|
||||
val activeIdentity = _activeIdentity.asStateFlow()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val exchangeRates = phoenixGlobal.currencyManager.ratesFlow
|
||||
@@ -115,14 +126,38 @@ class SovereignWalletViewModel(
|
||||
listAvailableWallets(onDone = {})
|
||||
}
|
||||
|
||||
/** Makes [identity] the one the app runs as. Everything reading [activeIdentity] follows. */
|
||||
fun setActiveIdentity(identity: Identity) {
|
||||
_activeIdentity.value = identity
|
||||
// scheduleAutoLock()
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the identity a just-started node derives: the mnemonic kind, with the
|
||||
* node behind it.
|
||||
*/
|
||||
fun setActiveWallet(walletId: WalletId, business: PhoenixBusiness) {
|
||||
val keyManager = business.walletManager.keyManager.value
|
||||
if (keyManager == null) {
|
||||
// `startNewBusiness` loads the wallet before it reports success, so a business
|
||||
// handed here without a key manager is a programming error rather than a state
|
||||
// to route around. Leaving the identity unset sends the UI back to startup.
|
||||
log.e { "business for wallet=$walletId started without a key manager" }
|
||||
return
|
||||
}
|
||||
val dataStoreManager = DataStoreManager(business)
|
||||
|
||||
val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = walletId)
|
||||
val internalPrefs = dataStoreManager.loadInternalPrefsForWallet( walletId = walletId)
|
||||
_activeWalletInUI.value = ActiveWallet(id = walletId, business = business, userPrefs = userPrefs, internalPrefs = internalPrefs)
|
||||
setActiveIdentity(
|
||||
Identity(
|
||||
id = walletId,
|
||||
kind = IdentityKind.Mnemonic,
|
||||
nostrPrivateKey = keyManager.nostrPrivateKey(),
|
||||
userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = walletId),
|
||||
internalPrefs = dataStoreManager.loadInternalPrefsForWallet(walletId = walletId),
|
||||
business = business,
|
||||
)
|
||||
)
|
||||
updateBusinessActiveInUI(walletId)
|
||||
// scheduleAutoLock()
|
||||
}
|
||||
|
||||
fun listAvailableWallets(onDone: () -> Unit) {
|
||||
@@ -186,7 +221,7 @@ class SovereignWalletViewModel(
|
||||
// // TODO: Run this through an expect...
|
||||
// viewModelScope.launch {
|
||||
// autoLockHandler.removeCallbacksAndMessages(null)
|
||||
// val activeUserPrefs = activeWalletInUI.first()?.userPrefs ?: return@launch
|
||||
// val activeUserPrefs = activeIdentity.first()?.userPrefs ?: return@launch
|
||||
//
|
||||
// val biometricLockEnabled = activeUserPrefs.getLockBiometricsEnabled.first()
|
||||
// val customPinLockEnabled = activeUserPrefs.getLockPinEnabled.first()
|
||||
@@ -198,21 +233,21 @@ class SovereignWalletViewModel(
|
||||
// }
|
||||
// }
|
||||
|
||||
/** Clears the active wallet and signals the startup screen to load the given [walletId]. */
|
||||
/** Clears the active identity and signals the startup screen to load the given [walletId]. */
|
||||
fun switchToWallet(walletId: WalletId) {
|
||||
_desiredWalletId.value = walletId
|
||||
_activeWalletInUI.value = null
|
||||
_activeIdentity.value = null
|
||||
}
|
||||
|
||||
/** Clears the active wallet. It does not affect [desiredWalletId]. The UI may still auto-open a specific wallet, if [desiredWalletId] is not null. */
|
||||
/** Clears the active identity. It does not affect [desiredWalletId]. The UI may still auto-open a specific wallet, if [desiredWalletId] is not null. */
|
||||
fun clearActiveWallet() {
|
||||
_activeWalletInUI.value = null
|
||||
_activeIdentity.value = null
|
||||
}
|
||||
|
||||
/** Resets the active wallet and [desiredWalletId]. The UI will redirect to the startup screen with the wallets selector prompt. */
|
||||
/** Resets the active identity and [desiredWalletId]. The UI will redirect to the startup screen with the wallets selector prompt. */
|
||||
fun resetToSelector() {
|
||||
_desiredWalletId.value = null
|
||||
_activeWalletInUI.value = null
|
||||
_activeIdentity.value = null
|
||||
startWalletImmediately.value = false
|
||||
}
|
||||
|
||||
|
||||
+8
-9
@@ -29,8 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd
|
||||
import com.vitorpamplona.quartz.nip77Negentropy.NegMsgCmd
|
||||
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import press.mantra.compose.identity.Identity
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -59,7 +58,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class SynchronizationViewModel(
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
val nostrRepository: NostrRepository,
|
||||
val chatRepository: ChatRepository,
|
||||
val relayRepository: RelayRepository,
|
||||
@@ -67,7 +66,7 @@ class SynchronizationViewModel(
|
||||
): ViewModel() {
|
||||
|
||||
val relaysSocketManager = RelaysSocketManager(
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
nostrSocketClientFactory = DefaultNostrSocketClientFactory,
|
||||
cachingImportRepository = CachingImportRepository.NO_OP_CACHING_IMPORT_REPOSITORY,
|
||||
relayRepository = relayRepository,
|
||||
@@ -130,7 +129,7 @@ class SynchronizationViewModel(
|
||||
private val mutex = Mutex()
|
||||
|
||||
fun factory(
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||
nostrRepository: NostrRepository,
|
||||
chatRepository: ChatRepository,
|
||||
relayRepository: RelayRepository,
|
||||
@@ -138,7 +137,7 @@ class SynchronizationViewModel(
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
SynchronizationViewModel(
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||
nostrRepository = nostrRepository,
|
||||
chatRepository = chatRepository,
|
||||
relayRepository = relayRepository,
|
||||
@@ -188,9 +187,9 @@ class SynchronizationViewModel(
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
||||
logger.d("Synchronization ActiveWallet: $activeWallet")
|
||||
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
|
||||
activeIdentityStateFlow.collectLatest { identity ->
|
||||
logger.d("Synchronization identity: $identity")
|
||||
identity?.nostrPrivateKey?.let { nostrPrivateKey ->
|
||||
val keyPair = KeyPair(
|
||||
privKey = nostrPrivateKey.value.toByteArray()
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user