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.NegCloseCmd
|
||||||
import com.vitorpamplona.quartz.nip77Negentropy.NegMsgCmd
|
import com.vitorpamplona.quartz.nip77Negentropy.NegMsgCmd
|
||||||
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
|
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.managers.nostrPublicKey
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
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
|
* https://github.com/PrimalHQ/primal-android-app/blob/6db2e6862239335dede4162338d7c4f10ad35031/app/src/main/kotlin/net/primal/android/networking/relays/RelaysSocketManager.kt
|
||||||
*/
|
*/
|
||||||
class RelaysSocketManager(
|
class RelaysSocketManager(
|
||||||
private val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
private val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
private val nostrSocketClientFactory: press.mantra.compose.network.sockets.NostrSocketClientFactory,
|
private val nostrSocketClientFactory: press.mantra.compose.network.sockets.NostrSocketClientFactory,
|
||||||
private val cachingImportRepository: press.mantra.compose.repository.CachingImportRepository,
|
private val cachingImportRepository: press.mantra.compose.repository.CachingImportRepository,
|
||||||
private val relayRepository: press.mantra.compose.repository.RelayRepository,
|
private val relayRepository: press.mantra.compose.repository.RelayRepository,
|
||||||
@@ -61,19 +60,18 @@ class RelaysSocketManager(
|
|||||||
|
|
||||||
private fun observeActiveUserId() =
|
private fun observeActiveUserId() =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
activeIdentityStateFlow.collectLatest { identity ->
|
||||||
if (activeWallet == null) {
|
if (identity == null) {
|
||||||
// TODO: Cancel all pending jobs?
|
// TODO: Cancel all pending jobs?
|
||||||
}
|
}
|
||||||
activeWallet?.business?.walletManager?.keyManager?.collectLatest { keyManager ->
|
// The identity carries its pubkey, so this no longer waits on the node's key
|
||||||
keyManager?.nostrPublicKey()?.let { pubkey ->
|
// manager to fill in. The relay observer is still a job on `scope` keyed by
|
||||||
observeRelayJobs[pubkey]?.cancel()
|
// pubkey and replaced per pubkey, exactly as before.
|
||||||
observeRelayJobs[pubkey] = observeRelays(pubkey)
|
identity?.nostrPublicKey?.let { pubkey ->
|
||||||
}
|
observeRelayJobs[pubkey]?.cancel()
|
||||||
|
observeRelayJobs[pubkey] = observeRelays(pubkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeRelays(publicKey: String): Job =
|
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 press.mantra.compose.network.asClientTag
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.toNsec
|
import com.vitorpamplona.quartz.nip19Bech32.toNsec
|
||||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
|
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
|
||||||
import fr.acinq.phoenix.managers.WalletManager
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
import press.mantra.compose.identity.Identity
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@@ -23,7 +23,7 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
*/
|
*/
|
||||||
class NostrNotaryRepository(
|
class NostrNotaryRepository(
|
||||||
private val nostrRepository: NostrRepository,
|
private val nostrRepository: NostrRepository,
|
||||||
private val walletManager: WalletManager,
|
private val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class NostrNotaryRepository(
|
|||||||
|
|
||||||
private fun findNsecOrThrow(activeUserPublicKey: String): String =
|
private fun findNsecOrThrow(activeUserPublicKey: String): String =
|
||||||
runCatching {
|
runCatching {
|
||||||
walletManager.keyManager.value?.nostrPrivateKey()?.value?.toByteArray()?.toNsec()
|
activeIdentityStateFlow.value?.nostrPrivateKey?.value?.toByteArray()?.toNsec()
|
||||||
}.getOrNull() ?: throw press.mantra.compose.exceptions.SigningKeyNotFoundException()
|
}.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 {
|
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.model.DkgRitualViewModel
|
||||||
import press.mantra.compose.ui.view.state.DkgRitualUIState
|
import press.mantra.compose.ui.view.state.DkgRitualUIState
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.flow.StateFlow
|
||||||
import press.mantra.compose.ui.theme.spacing
|
import press.mantra.compose.ui.theme.spacing
|
||||||
import press.mantra.compose.ui.composable.widgets.Decorative
|
import press.mantra.compose.ui.composable.widgets.Decorative
|
||||||
@@ -65,7 +65,7 @@ internal fun DkgApprovalScaffold(
|
|||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
chatRoomId: String,
|
chatRoomId: String,
|
||||||
dkgSessionId: String? = null,
|
dkgSessionId: String? = null,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository,
|
dkgRepository: DkgRepository,
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
@@ -76,7 +76,7 @@ internal fun DkgApprovalScaffold(
|
|||||||
chatRoomId = chatRoomId,
|
chatRoomId = chatRoomId,
|
||||||
dkgSessionId = dkgSessionId,
|
dkgSessionId = dkgSessionId,
|
||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository
|
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.ChatRepository
|
||||||
import press.mantra.compose.repository.DkgRepository
|
import press.mantra.compose.repository.DkgRepository
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.flow.StateFlow
|
||||||
import press.mantra.compose.ui.theme.spacing
|
import press.mantra.compose.ui.theme.spacing
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ fun DkgJoinApprovalScreen(
|
|||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
chatRoomId: String,
|
chatRoomId: String,
|
||||||
dkgSessionId: String? = null,
|
dkgSessionId: String? = null,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository,
|
dkgRepository: DkgRepository,
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
@@ -45,7 +45,7 @@ fun DkgJoinApprovalScreen(
|
|||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
chatRoomId = chatRoomId,
|
chatRoomId = chatRoomId,
|
||||||
dkgSessionId = dkgSessionId,
|
dkgSessionId = dkgSessionId,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository,
|
dkgRepository = dkgRepository,
|
||||||
onDone = onDone
|
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 press.mantra.compose.ui.view.state.DkgRitualUIState
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import press.mantra.compose.database.model.FrostSigningSession
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import press.mantra.compose.ui.theme.spacing
|
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. */
|
/** Which of the room's ceremonies to show; null takes the newest. */
|
||||||
dkgSessionId: String? = null,
|
dkgSessionId: String? = null,
|
||||||
initialDkgRitualUIState: DkgRitualUIState = DkgRitualUIState.Loading,
|
initialDkgRitualUIState: DkgRitualUIState = DkgRitualUIState.Loading,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository,
|
dkgRepository: DkgRepository,
|
||||||
onNavigateToRoute: (Route) -> Unit = {},
|
onNavigateToRoute: (Route) -> Unit = {},
|
||||||
@@ -146,7 +146,7 @@ fun DkgRitualScreen(
|
|||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
dkgSessionId = dkgSessionId,
|
dkgSessionId = dkgSessionId,
|
||||||
initialDkgRitualUIState = initialDkgRitualUIState,
|
initialDkgRitualUIState = initialDkgRitualUIState,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository
|
dkgRepository = dkgRepository
|
||||||
)
|
)
|
||||||
@@ -882,7 +882,7 @@ private fun DkgRitualScreenPreview() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
activeWalletStateFlow = MutableStateFlow(null),
|
activeIdentityStateFlow = MutableStateFlow(null),
|
||||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||||
dkgRepository = DkgRepository.NO_OP_DKG_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.ChatRepository
|
||||||
import press.mantra.compose.repository.DkgRepository
|
import press.mantra.compose.repository.DkgRepository
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.flow.StateFlow
|
||||||
import press.mantra.compose.ui.theme.spacing
|
import press.mantra.compose.ui.theme.spacing
|
||||||
import mantra.composeapp.generated.resources.Res
|
import mantra.composeapp.generated.resources.Res
|
||||||
@@ -37,7 +37,7 @@ fun DkgRound1ApprovalScreen(
|
|||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
chatRoomId: String,
|
chatRoomId: String,
|
||||||
dkgSessionId: String? = null,
|
dkgSessionId: String? = null,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository,
|
dkgRepository: DkgRepository,
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
@@ -49,7 +49,7 @@ fun DkgRound1ApprovalScreen(
|
|||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
chatRoomId = chatRoomId,
|
chatRoomId = chatRoomId,
|
||||||
dkgSessionId = dkgSessionId,
|
dkgSessionId = dkgSessionId,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository,
|
dkgRepository = dkgRepository,
|
||||||
onDone = onDone
|
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.ChatRepository
|
||||||
import press.mantra.compose.repository.DkgRepository
|
import press.mantra.compose.repository.DkgRepository
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.flow.StateFlow
|
||||||
import press.mantra.compose.ui.theme.spacing
|
import press.mantra.compose.ui.theme.spacing
|
||||||
import mantra.composeapp.generated.resources.Res
|
import mantra.composeapp.generated.resources.Res
|
||||||
@@ -38,7 +38,7 @@ fun DkgRound2ApprovalScreen(
|
|||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
chatRoomId: String,
|
chatRoomId: String,
|
||||||
dkgSessionId: String? = null,
|
dkgSessionId: String? = null,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository,
|
dkgRepository: DkgRepository,
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
@@ -50,7 +50,7 @@ fun DkgRound2ApprovalScreen(
|
|||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
chatRoomId = chatRoomId,
|
chatRoomId = chatRoomId,
|
||||||
dkgSessionId = dkgSessionId,
|
dkgSessionId = dkgSessionId,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository,
|
dkgRepository = dkgRepository,
|
||||||
onDone = onDone
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
@@ -81,16 +81,16 @@ import press.mantra.compose.ui.theme.spacing
|
|||||||
@Composable
|
@Composable
|
||||||
fun KeyRecoveryScreen(
|
fun KeyRecoveryScreen(
|
||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToRoute: (Route) -> 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
|
// The same preference the recovery phrase screen writes, so the state of the backup is
|
||||||
// visible before the user goes looking for it.
|
// visible before the user goes looking for it.
|
||||||
val showBackupNotice by remember(activeWallet) {
|
val showBackupNotice by remember(activeIdentity) {
|
||||||
activeWallet?.internalPrefs?.showSeedBackupNotice ?: flowOf(false)
|
activeIdentity?.internalPrefs?.showSeedBackupNotice ?: flowOf(false)
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -305,7 +305,7 @@ private fun KeyRecoveryScreenPreview() {
|
|||||||
) {
|
) {
|
||||||
KeyRecoveryScreen(
|
KeyRecoveryScreen(
|
||||||
activeUserPublicKey = "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
activeUserPublicKey = "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
||||||
activeWalletStateFlow = MutableStateFlow(null),
|
activeIdentityStateFlow = MutableStateFlow(null),
|
||||||
onNavigateBack = {},
|
onNavigateBack = {},
|
||||||
onNavigateToRoute = {}
|
onNavigateToRoute = {}
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -42,7 +42,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import fr.acinq.phoenix.PhoenixGlobal
|
import fr.acinq.phoenix.PhoenixGlobal
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import mantra.composeapp.generated.resources.Res
|
import mantra.composeapp.generated.resources.Res
|
||||||
import mantra.composeapp.generated.resources.backup_confirmation
|
import mantra.composeapp.generated.resources.backup_confirmation
|
||||||
@@ -82,13 +82,13 @@ import press.mantra.compose.ui.view.state.RecoveryPhraseUIState
|
|||||||
@Composable
|
@Composable
|
||||||
fun RecoveryPhraseScreen(
|
fun RecoveryPhraseScreen(
|
||||||
phoenixGlobal: PhoenixGlobal,
|
phoenixGlobal: PhoenixGlobal,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val recoveryPhraseViewModel: RecoveryPhraseViewModel = viewModel(
|
val recoveryPhraseViewModel: RecoveryPhraseViewModel = viewModel(
|
||||||
factory = RecoveryPhraseViewModel.factory(
|
factory = RecoveryPhraseViewModel.factory(
|
||||||
phoenixGlobal = phoenixGlobal,
|
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.model.SelectChatRoomTypeViewModel
|
||||||
import press.mantra.compose.ui.view.state.SelectChatRoomTypeUIState
|
import press.mantra.compose.ui.view.state.SelectChatRoomTypeUIState
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import press.mantra.compose.ui.theme.spacing
|
import press.mantra.compose.ui.theme.spacing
|
||||||
@@ -87,7 +87,7 @@ fun SelectChatRoomTypeScreen(
|
|||||||
description: String?,
|
description: String?,
|
||||||
memberPublicKeys: List<HexKey>,
|
memberPublicKeys: List<HexKey>,
|
||||||
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState = SelectChatRoomTypeUIState.Loading,
|
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState = SelectChatRoomTypeUIState.Loading,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
nostrRepository: NostrRepository,
|
nostrRepository: NostrRepository,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository,
|
dkgRepository: DkgRepository,
|
||||||
@@ -100,7 +100,7 @@ fun SelectChatRoomTypeScreen(
|
|||||||
description = description,
|
description = description,
|
||||||
memberPublicKeys = memberPublicKeys,
|
memberPublicKeys = memberPublicKeys,
|
||||||
initialSelectChatRoomTypeUIState = initialSelectChatRoomTypeUIState,
|
initialSelectChatRoomTypeUIState = initialSelectChatRoomTypeUIState,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
nostrRepository = nostrRepository,
|
nostrRepository = nostrRepository,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository
|
dkgRepository = dkgRepository
|
||||||
@@ -490,7 +490,7 @@ private fun SelectChatRoomTypeScreenPreview() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
activeWalletStateFlow = MutableStateFlow(null),
|
activeIdentityStateFlow = MutableStateFlow(null),
|
||||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
|
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
|
||||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||||
dkgRepository = DkgRepository.NO_OP_DKG_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.compose.ui.text.style.TextOverflow
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mantra.composeapp.generated.resources.Res
|
import mantra.composeapp.generated.resources.Res
|
||||||
@@ -81,7 +81,7 @@ fun SelectSubgroupAdminsScreen(
|
|||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
parentChatRoomId: String,
|
parentChatRoomId: String,
|
||||||
initialUIState: SelectSubgroupAdminsUIState = SelectSubgroupAdminsUIState.Loading,
|
initialUIState: SelectSubgroupAdminsUIState = SelectSubgroupAdminsUIState.Loading,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository,
|
dkgRepository: DkgRepository,
|
||||||
onNavigateToRoute: (Route) -> Unit
|
onNavigateToRoute: (Route) -> Unit
|
||||||
@@ -91,7 +91,7 @@ fun SelectSubgroupAdminsScreen(
|
|||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
parentChatRoomId = parentChatRoomId,
|
parentChatRoomId = parentChatRoomId,
|
||||||
initialUIState = initialUIState,
|
initialUIState = initialUIState,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository
|
dkgRepository = dkgRepository
|
||||||
)
|
)
|
||||||
@@ -331,7 +331,7 @@ private fun SelectSubgroupAdminsScreenPreview() {
|
|||||||
SelectSubgroupAdminsScreen(
|
SelectSubgroupAdminsScreen(
|
||||||
activeUserPublicKey = "a".repeat(64),
|
activeUserPublicKey = "a".repeat(64),
|
||||||
parentChatRoomId = "b".repeat(64),
|
parentChatRoomId = "b".repeat(64),
|
||||||
activeWalletStateFlow = kotlinx.coroutines.flow.MutableStateFlow(null),
|
activeIdentityStateFlow = kotlinx.coroutines.flow.MutableStateFlow(null),
|
||||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||||
dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY,
|
dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY,
|
||||||
onNavigateToRoute = {}
|
onNavigateToRoute = {}
|
||||||
|
|||||||
+6
-6
@@ -49,8 +49,8 @@ fun SovereignWalletStartupScreen(
|
|||||||
// Deliberately no intro gate. `getShowIntro` defaults to true and nothing in this app ever
|
// 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
|
// 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
|
// — 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 completion here: everything downstream reads the active identity, and nothing sets one
|
||||||
// to be a step inside this flow, not a detour around it.
|
// 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()
|
val listWalletState by sovereignWalletViewModel.listWalletState.collectAsState()
|
||||||
|
|
||||||
@@ -84,8 +84,8 @@ fun SovereignWalletStartupScreen(
|
|||||||
val desiredWalletIdFlow = sovereignWalletViewModel.desiredWalletId.collectAsState()
|
val desiredWalletIdFlow = sovereignWalletViewModel.desiredWalletId.collectAsState()
|
||||||
val desiredWalletId = desiredWalletIdFlow.value
|
val desiredWalletId = desiredWalletIdFlow.value
|
||||||
|
|
||||||
val activeWalletFlow = sovereignWalletViewModel.activeWalletInUI.collectAsState()
|
val activeIdentityFlow = sovereignWalletViewModel.activeIdentity.collectAsState()
|
||||||
val activeWallet = activeWalletFlow.value
|
val activeIdentity = activeIdentityFlow.value
|
||||||
|
|
||||||
when {
|
when {
|
||||||
availableWallets.isEmpty() -> {
|
availableWallets.isEmpty() -> {
|
||||||
@@ -99,12 +99,12 @@ fun SovereignWalletStartupScreen(
|
|||||||
text = "Preparing wallet..."
|
text = "Preparing wallet..."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
activeWallet != null -> {
|
activeIdentity != null -> {
|
||||||
LoadingDataIndicator(
|
LoadingDataIndicator(
|
||||||
text = "Opening wallet"
|
text = "Opening wallet"
|
||||||
)
|
)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
sovereignWalletViewModel.loadSovereignData(activeWallet.id)
|
sovereignWalletViewModel.loadSovereignData(activeIdentity.id)
|
||||||
onSuccessfulStartup.invoke()
|
onSuccessfulStartup.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -230,7 +230,7 @@ fun MantraNavHost(
|
|||||||
|
|
||||||
val navigationViewModel: NavigationViewModel = viewModel (
|
val navigationViewModel: NavigationViewModel = viewModel (
|
||||||
factory = NavigationViewModel.factory(
|
factory = NavigationViewModel.factory(
|
||||||
activeWallet = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentity = sovereignWalletViewModel.activeIdentity,
|
||||||
initialNavigationUIState = NavigationUIState.Loading(),
|
initialNavigationUIState = NavigationUIState.Loading(),
|
||||||
nostrRepository = databaseNostrRepository,
|
nostrRepository = databaseNostrRepository,
|
||||||
scope = applicationIOScope
|
scope = applicationIOScope
|
||||||
@@ -239,7 +239,7 @@ fun MantraNavHost(
|
|||||||
|
|
||||||
val notaryViewModel: NotaryViewModel = viewModel(
|
val notaryViewModel: NotaryViewModel = viewModel(
|
||||||
factory = NotaryViewModel.factory(
|
factory = NotaryViewModel.factory(
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
nostrRepository = databaseNostrRepository,
|
nostrRepository = databaseNostrRepository,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
marmotRepository = databaseMarmotRepository,
|
marmotRepository = databaseMarmotRepository,
|
||||||
@@ -250,7 +250,7 @@ fun MantraNavHost(
|
|||||||
// TODO: Produce a notary UI Element...
|
// TODO: Produce a notary UI Element...
|
||||||
val synchronizationViewModel: SynchronizationViewModel = viewModel(
|
val synchronizationViewModel: SynchronizationViewModel = viewModel(
|
||||||
factory = SynchronizationViewModel.factory(
|
factory = SynchronizationViewModel.factory(
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
nostrRepository = databaseNostrRepository,
|
nostrRepository = databaseNostrRepository,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
relayRepository = databaseNostrRepository,
|
relayRepository = databaseNostrRepository,
|
||||||
@@ -548,7 +548,7 @@ fun MantraNavHost(
|
|||||||
SelectSubgroupAdminsScreen(
|
SelectSubgroupAdminsScreen(
|
||||||
activeUserPublicKey = route.activeUserPublicKey,
|
activeUserPublicKey = route.activeUserPublicKey,
|
||||||
parentChatRoomId = route.parentChatRoomId,
|
parentChatRoomId = route.parentChatRoomId,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
dkgRepository = databaseDkgRepository,
|
dkgRepository = databaseDkgRepository,
|
||||||
onNavigateToRoute = { nextRoute ->
|
onNavigateToRoute = { nextRoute ->
|
||||||
@@ -563,7 +563,7 @@ fun MantraNavHost(
|
|||||||
activeUserPublicKey = route.activeUserPublicKey,
|
activeUserPublicKey = route.activeUserPublicKey,
|
||||||
chatRoomId = route.chatRoomId,
|
chatRoomId = route.chatRoomId,
|
||||||
dkgSessionId = route.dkgSessionId,
|
dkgSessionId = route.dkgSessionId,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
dkgRepository = databaseDkgRepository,
|
dkgRepository = databaseDkgRepository,
|
||||||
onNavigateToRoute = { approvalRoute ->
|
onNavigateToRoute = { approvalRoute ->
|
||||||
@@ -578,7 +578,7 @@ fun MantraNavHost(
|
|||||||
activeUserPublicKey = route.activeUserPublicKey,
|
activeUserPublicKey = route.activeUserPublicKey,
|
||||||
chatRoomId = route.chatRoomId,
|
chatRoomId = route.chatRoomId,
|
||||||
dkgSessionId = route.dkgSessionId,
|
dkgSessionId = route.dkgSessionId,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
dkgRepository = databaseDkgRepository,
|
dkgRepository = databaseDkgRepository,
|
||||||
onDone = { navController.popBackStack() }
|
onDone = { navController.popBackStack() }
|
||||||
@@ -591,7 +591,7 @@ fun MantraNavHost(
|
|||||||
activeUserPublicKey = route.activeUserPublicKey,
|
activeUserPublicKey = route.activeUserPublicKey,
|
||||||
chatRoomId = route.chatRoomId,
|
chatRoomId = route.chatRoomId,
|
||||||
dkgSessionId = route.dkgSessionId,
|
dkgSessionId = route.dkgSessionId,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
dkgRepository = databaseDkgRepository,
|
dkgRepository = databaseDkgRepository,
|
||||||
onDone = { navController.popBackStack() }
|
onDone = { navController.popBackStack() }
|
||||||
@@ -604,7 +604,7 @@ fun MantraNavHost(
|
|||||||
activeUserPublicKey = route.activeUserPublicKey,
|
activeUserPublicKey = route.activeUserPublicKey,
|
||||||
chatRoomId = route.chatRoomId,
|
chatRoomId = route.chatRoomId,
|
||||||
dkgSessionId = route.dkgSessionId,
|
dkgSessionId = route.dkgSessionId,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
dkgRepository = databaseDkgRepository,
|
dkgRepository = databaseDkgRepository,
|
||||||
onDone = { navController.popBackStack() }
|
onDone = { navController.popBackStack() }
|
||||||
@@ -618,7 +618,7 @@ fun MantraNavHost(
|
|||||||
name = route.name,
|
name = route.name,
|
||||||
description = route.description,
|
description = route.description,
|
||||||
memberPublicKeys = route.memberPublicKeys,
|
memberPublicKeys = route.memberPublicKeys,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
nostrRepository = databaseNostrRepository,
|
nostrRepository = databaseNostrRepository,
|
||||||
chatRepository = databaseChatRepository,
|
chatRepository = databaseChatRepository,
|
||||||
dkgRepository = databaseDkgRepository,
|
dkgRepository = databaseDkgRepository,
|
||||||
@@ -815,7 +815,7 @@ fun MantraNavHost(
|
|||||||
|
|
||||||
KeyRecoveryScreen(
|
KeyRecoveryScreen(
|
||||||
activeUserPublicKey = route.activeUserPublicKey,
|
activeUserPublicKey = route.activeUserPublicKey,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
@@ -829,7 +829,7 @@ fun MantraNavHost(
|
|||||||
composable<RecoveryPhraseRoute> {
|
composable<RecoveryPhraseRoute> {
|
||||||
RecoveryPhraseScreen(
|
RecoveryPhraseScreen(
|
||||||
phoenixGlobal = phoenixGlobal,
|
phoenixGlobal = phoenixGlobal,
|
||||||
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
|
activeIdentityStateFlow = sovereignWalletViewModel.activeIdentity,
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
navController.popBackStack()
|
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.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -56,7 +55,7 @@ class DkgRitualViewModel(
|
|||||||
*/
|
*/
|
||||||
val dkgSessionId: String? = null,
|
val dkgSessionId: String? = null,
|
||||||
initialDkgRitualUIState: DkgRitualUIState,
|
initialDkgRitualUIState: DkgRitualUIState,
|
||||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
val chatRepository: ChatRepository,
|
val chatRepository: ChatRepository,
|
||||||
val dkgRepository: DkgRepository,
|
val dkgRepository: DkgRepository,
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
@@ -473,7 +472,7 @@ class DkgRitualViewModel(
|
|||||||
|
|
||||||
val loaded = dkgRitualUIState as? DkgRitualUIState.Loaded ?: return
|
val loaded = dkgRitualUIState as? DkgRitualUIState.Loaded ?: return
|
||||||
|
|
||||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||||
if (nostrPrivateKey == null) {
|
if (nostrPrivateKey == null) {
|
||||||
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
||||||
return
|
return
|
||||||
@@ -513,7 +512,7 @@ class DkgRitualViewModel(
|
|||||||
val loaded = dkgRitualUIState as? DkgRitualUIState.Loaded ?: return
|
val loaded = dkgRitualUIState as? DkgRitualUIState.Loaded ?: return
|
||||||
val session = loaded.session ?: return
|
val session = loaded.session ?: return
|
||||||
|
|
||||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||||
if (nostrPrivateKey == null) {
|
if (nostrPrivateKey == null) {
|
||||||
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
||||||
return
|
return
|
||||||
@@ -567,7 +566,7 @@ class DkgRitualViewModel(
|
|||||||
// guard rather than a path anybody takes.
|
// guard rather than a path anybody takes.
|
||||||
if (!loaded.isKeyStateSigned) return
|
if (!loaded.isKeyStateSigned) return
|
||||||
|
|
||||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||||
if (nostrPrivateKey == null) {
|
if (nostrPrivateKey == null) {
|
||||||
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
dkgRitualUIState = DkgRitualUIState.Error("Couldn't read your keys. Please try again.")
|
||||||
return
|
return
|
||||||
@@ -678,7 +677,7 @@ class DkgRitualViewModel(
|
|||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
dkgSessionId: String? = null,
|
dkgSessionId: String? = null,
|
||||||
initialDkgRitualUIState: DkgRitualUIState = DkgRitualUIState.Loading,
|
initialDkgRitualUIState: DkgRitualUIState = DkgRitualUIState.Loading,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository
|
dkgRepository: DkgRepository
|
||||||
): ViewModelProvider.Factory = viewModelFactory {
|
): ViewModelProvider.Factory = viewModelFactory {
|
||||||
@@ -688,7 +687,7 @@ class DkgRitualViewModel(
|
|||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
dkgSessionId = dkgSessionId,
|
dkgSessionId = dkgSessionId,
|
||||||
initialDkgRitualUIState = initialDkgRitualUIState,
|
initialDkgRitualUIState = initialDkgRitualUIState,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository
|
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.composable.navigation.routes.SovereignWalletStartupRoute
|
||||||
import press.mantra.compose.ui.view.state.NavigationUIState
|
import press.mantra.compose.ui.view.state.NavigationUIState
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.managers.nostrPublicKey
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
@@ -25,7 +24,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class NavigationViewModel(
|
class NavigationViewModel(
|
||||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
initialNavigationUIState: NavigationUIState,
|
initialNavigationUIState: NavigationUIState,
|
||||||
val nostrRepository: press.mantra.compose.repository.NostrRepository,
|
val nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
@@ -38,14 +37,14 @@ class NavigationViewModel(
|
|||||||
private val PENDING_BROADCAST_STATUSES = setOf("pending", "processing")
|
private val PENDING_BROADCAST_STATUSES = setOf("pending", "processing")
|
||||||
|
|
||||||
fun factory(
|
fun factory(
|
||||||
activeWallet: StateFlow<ActiveWallet?>,
|
activeIdentity: StateFlow<Identity?>,
|
||||||
initialNavigationUIState: NavigationUIState,
|
initialNavigationUIState: NavigationUIState,
|
||||||
nostrRepository: press.mantra.compose.repository.NostrRepository,
|
nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
): ViewModelProvider.Factory = viewModelFactory {
|
): ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
NavigationViewModel(
|
NavigationViewModel(
|
||||||
activeWallet,
|
activeIdentity,
|
||||||
initialNavigationUIState = initialNavigationUIState,
|
initialNavigationUIState = initialNavigationUIState,
|
||||||
nostrRepository = nostrRepository,
|
nostrRepository = nostrRepository,
|
||||||
scope = scope
|
scope = scope
|
||||||
@@ -176,34 +175,22 @@ class NavigationViewModel(
|
|||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
delay(2_100.milliseconds) // Looking busy...
|
delay(2_100.milliseconds) // Looking busy...
|
||||||
|
|
||||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
activeIdentityStateFlow.collectLatest { identity ->
|
||||||
logger.d("Active Wallet: $activeWallet")
|
logger.d("Active identity: $identity")
|
||||||
if (activeWallet == null) {
|
if (identity == null) {
|
||||||
|
// Nothing has been started: go and start something.
|
||||||
_navigationUIState.getAndUpdate {
|
_navigationUIState.getAndUpdate {
|
||||||
NavigationUIState.StartupPhoenix
|
NavigationUIState.StartupPhoenix
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
activeWallet.business.let { business ->
|
// The identity carries its own key, so there is no node to wait on and no
|
||||||
if (business == null) {
|
// "couldn't get the nostrKey" dead end. The branch that used to send a
|
||||||
_navigationUIState.getAndUpdate {
|
// nodeless identity back to startup went with it: an identity without a
|
||||||
NavigationUIState.StartupPhoenix
|
// node is now a kind of identity, not a wallet that has not finished
|
||||||
}
|
// starting.
|
||||||
} else {
|
logger.i("Observing: ${identity.nostrPublicKey}")
|
||||||
val activeUserPublicKey = business.walletManager.keyManager.value?.nostrPublicKey()
|
|
||||||
|
|
||||||
if (activeUserPublicKey == null) {
|
loadNostrProfile(identity.nostrPublicKey)
|
||||||
_navigationUIState.getAndUpdate {
|
|
||||||
NavigationUIState.Loading(text = "Couldn't get the nostrKey")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.i("Observing: $activeUserPublicKey")
|
|
||||||
|
|
||||||
loadNostrProfile(activeUserPublicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.i("Navigation UI State is Landing")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
|
||||||
import fr.acinq.phoenix.managers.nsecPassword
|
import fr.acinq.phoenix.managers.nsecPassword
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import press.mantra.compose.database.model.NostrEvent
|
import press.mantra.compose.database.model.NostrEvent
|
||||||
@@ -31,7 +27,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
class NotaryViewModel(
|
class NotaryViewModel(
|
||||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
val nostrRepository: press.mantra.compose.repository.NostrRepository,
|
val nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||||
val chatRepository: press.mantra.compose.repository.ChatRepository,
|
val chatRepository: press.mantra.compose.repository.ChatRepository,
|
||||||
val marmotRepository: MarmotRepository,
|
val marmotRepository: MarmotRepository,
|
||||||
@@ -43,7 +39,7 @@ class NotaryViewModel(
|
|||||||
|
|
||||||
|
|
||||||
fun factory(
|
fun factory(
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
nostrRepository: press.mantra.compose.repository.NostrRepository,
|
nostrRepository: press.mantra.compose.repository.NostrRepository,
|
||||||
chatRepository: press.mantra.compose.repository.ChatRepository,
|
chatRepository: press.mantra.compose.repository.ChatRepository,
|
||||||
marmotRepository: MarmotRepository,
|
marmotRepository: MarmotRepository,
|
||||||
@@ -51,7 +47,7 @@ class NotaryViewModel(
|
|||||||
): ViewModelProvider.Factory = viewModelFactory {
|
): ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
NotaryViewModel(
|
NotaryViewModel(
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
nostrRepository = nostrRepository,
|
nostrRepository = nostrRepository,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
marmotRepository = marmotRepository,
|
marmotRepository = marmotRepository,
|
||||||
@@ -63,14 +59,14 @@ class NotaryViewModel(
|
|||||||
|
|
||||||
private val logger = Logger.withTag(TAG)
|
private val logger = Logger.withTag(TAG)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private fun observeActiveNostrKey() {
|
private fun observeActiveNostrKey() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
activeWalletStateFlow
|
// One flow, not a `flatMapLatest` into the node's key manager: the identity
|
||||||
.flatMapLatest { activeWallet ->
|
// carries its key from the moment it exists, so there is nothing to wait for.
|
||||||
activeWallet?.business?.walletManager?.keyManager ?: flowOf(null)
|
// `distinctUntilChanged` on the key stays -- an identity re-emitted with the same
|
||||||
}
|
// key must not tear down and restart the observers below.
|
||||||
.map { keyManager -> keyManager?.nostrPrivateKey() }
|
activeIdentityStateFlow
|
||||||
|
.map { identity -> identity?.nostrPrivateKey }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collectLatest { nostrPrivateKey ->
|
.collectLatest { nostrPrivateKey ->
|
||||||
if (nostrPrivateKey == null) return@collectLatest
|
if (nostrPrivateKey == null) return@collectLatest
|
||||||
|
|||||||
+10
-10
@@ -7,7 +7,7 @@ import androidx.lifecycle.viewmodel.initializer
|
|||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import fr.acinq.phoenix.PhoenixGlobal
|
import fr.acinq.phoenix.PhoenixGlobal
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.data.DecryptSeedResult
|
import fr.acinq.phoenix.data.DecryptSeedResult
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -39,7 +39,7 @@ import press.mantra.compose.ui.view.state.RecoveryPhraseUIState
|
|||||||
*/
|
*/
|
||||||
class RecoveryPhraseViewModel(
|
class RecoveryPhraseViewModel(
|
||||||
val phoenixGlobal: PhoenixGlobal,
|
val phoenixGlobal: PhoenixGlobal,
|
||||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
private val logger = Logger.withTag(TAG)
|
private val logger = Logger.withTag(TAG)
|
||||||
@@ -60,18 +60,18 @@ class RecoveryPhraseViewModel(
|
|||||||
* instead of drawing an unticked box over a ticked preference.
|
* instead of drawing an unticked box over a ticked preference.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val isBackupDone: StateFlow<Boolean?> = activeWalletStateFlow
|
val isBackupDone: StateFlow<Boolean?> = activeIdentityStateFlow
|
||||||
.flatMapLatest { it?.internalPrefs?.isManualSeedBackupDone ?: flowOf(null) }
|
.flatMapLatest { it?.internalPrefs?.isManualSeedBackupDone ?: flowOf(null) }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val isDisclaimerRead: StateFlow<Boolean?> = activeWalletStateFlow
|
val isDisclaimerRead: StateFlow<Boolean?> = activeIdentityStateFlow
|
||||||
.flatMapLatest { it?.internalPrefs?.isSeedLossDisclaimerRead ?: flowOf(null) }
|
.flatMapLatest { it?.internalPrefs?.isSeedLossDisclaimerRead ?: flowOf(null) }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||||
|
|
||||||
/** True until the user has both written the phrase down and read the disclaimer. */
|
/** True until the user has both written the phrase down and read the disclaimer. */
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val showBackupNotice: StateFlow<Boolean> = activeWalletStateFlow
|
val showBackupNotice: StateFlow<Boolean> = activeIdentityStateFlow
|
||||||
.flatMapLatest { it?.internalPrefs?.showSeedBackupNotice ?: flowOf(false) }
|
.flatMapLatest { it?.internalPrefs?.showSeedBackupNotice ?: flowOf(false) }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class RecoveryPhraseViewModel(
|
|||||||
fun revealRecoveryPhrase() {
|
fun revealRecoveryPhrase() {
|
||||||
if (_recoveryPhraseUIState.value is RecoveryPhraseUIState.Revealing) return
|
if (_recoveryPhraseUIState.value is RecoveryPhraseUIState.Revealing) return
|
||||||
|
|
||||||
val walletId = activeWalletStateFlow.value?.id
|
val walletId = activeIdentityStateFlow.value?.id
|
||||||
if (walletId == null) {
|
if (walletId == null) {
|
||||||
logger.e { "no active wallet, cannot read a recovery phrase" }
|
logger.e { "no active wallet, cannot read a recovery phrase" }
|
||||||
_recoveryPhraseUIState.value = RecoveryPhraseUIState.Error.NoActiveWallet
|
_recoveryPhraseUIState.value = RecoveryPhraseUIState.Error.NoActiveWallet
|
||||||
@@ -128,7 +128,7 @@ class RecoveryPhraseViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setBackupDone(isDone: Boolean) {
|
fun setBackupDone(isDone: Boolean) {
|
||||||
val internalPrefs = activeWalletStateFlow.value?.internalPrefs ?: return
|
val internalPrefs = activeIdentityStateFlow.value?.internalPrefs ?: return
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
|
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
|
||||||
logger.e("could not save the backup flag", throwable)
|
logger.e("could not save the backup flag", throwable)
|
||||||
}) {
|
}) {
|
||||||
@@ -137,7 +137,7 @@ class RecoveryPhraseViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setDisclaimerRead(isRead: Boolean) {
|
fun setDisclaimerRead(isRead: Boolean) {
|
||||||
val internalPrefs = activeWalletStateFlow.value?.internalPrefs ?: return
|
val internalPrefs = activeIdentityStateFlow.value?.internalPrefs ?: return
|
||||||
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
|
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
|
||||||
logger.e("could not save the disclaimer flag", throwable)
|
logger.e("could not save the disclaimer flag", throwable)
|
||||||
}) {
|
}) {
|
||||||
@@ -150,12 +150,12 @@ class RecoveryPhraseViewModel(
|
|||||||
|
|
||||||
fun factory(
|
fun factory(
|
||||||
phoenixGlobal: PhoenixGlobal,
|
phoenixGlobal: PhoenixGlobal,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
): ViewModelProvider.Factory = viewModelFactory {
|
): ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
RecoveryPhraseViewModel(
|
RecoveryPhraseViewModel(
|
||||||
phoenixGlobal = phoenixGlobal,
|
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.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -58,7 +57,7 @@ class SelectChatRoomTypeViewModel(
|
|||||||
val description: String?,
|
val description: String?,
|
||||||
val memberPublicKeys: List<HexKey>,
|
val memberPublicKeys: List<HexKey>,
|
||||||
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState,
|
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState,
|
||||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
val nostrRepository: NostrRepository,
|
val nostrRepository: NostrRepository,
|
||||||
val chatRepository: ChatRepository,
|
val chatRepository: ChatRepository,
|
||||||
val dkgRepository: DkgRepository,
|
val dkgRepository: DkgRepository,
|
||||||
@@ -190,7 +189,7 @@ class SelectChatRoomTypeViewModel(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val nostrPrivateKey = activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||||
if (nostrPrivateKey == null) {
|
if (nostrPrivateKey == null) {
|
||||||
selectChatRoomTypeUIState = SelectChatRoomTypeUIState.Error(
|
selectChatRoomTypeUIState = SelectChatRoomTypeUIState.Error(
|
||||||
"Couldn't read your keys. Please try again."
|
"Couldn't read your keys. Please try again."
|
||||||
@@ -447,7 +446,7 @@ class SelectChatRoomTypeViewModel(
|
|||||||
description: String?,
|
description: String?,
|
||||||
memberPublicKeys: List<HexKey>,
|
memberPublicKeys: List<HexKey>,
|
||||||
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState = SelectChatRoomTypeUIState.Loading,
|
initialSelectChatRoomTypeUIState: SelectChatRoomTypeUIState = SelectChatRoomTypeUIState.Loading,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
nostrRepository: NostrRepository,
|
nostrRepository: NostrRepository,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository
|
dkgRepository: DkgRepository
|
||||||
@@ -459,7 +458,7 @@ class SelectChatRoomTypeViewModel(
|
|||||||
description = description,
|
description = description,
|
||||||
memberPublicKeys = memberPublicKeys,
|
memberPublicKeys = memberPublicKeys,
|
||||||
initialSelectChatRoomTypeUIState = initialSelectChatRoomTypeUIState,
|
initialSelectChatRoomTypeUIState = initialSelectChatRoomTypeUIState,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
nostrRepository = nostrRepository,
|
nostrRepository = nostrRepository,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository
|
dkgRepository = dkgRepository
|
||||||
|
|||||||
+5
-7
@@ -12,8 +12,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -51,7 +50,7 @@ class SelectSubgroupAdminsViewModel(
|
|||||||
val activeUserPublicKey: HexKey,
|
val activeUserPublicKey: HexKey,
|
||||||
val parentChatRoomId: String,
|
val parentChatRoomId: String,
|
||||||
initialUIState: SelectSubgroupAdminsUIState,
|
initialUIState: SelectSubgroupAdminsUIState,
|
||||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
val chatRepository: ChatRepository,
|
val chatRepository: ChatRepository,
|
||||||
val dkgRepository: DkgRepository,
|
val dkgRepository: DkgRepository,
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
@@ -204,8 +203,7 @@ class SelectSubgroupAdminsViewModel(
|
|||||||
|
|
||||||
val loaded = uiState as? SelectSubgroupAdminsUIState.Loaded ?: return
|
val loaded = uiState as? SelectSubgroupAdminsUIState.Loaded ?: return
|
||||||
|
|
||||||
val nostrPrivateKey = activeWalletStateFlow.value
|
val nostrPrivateKey = activeIdentityStateFlow.value?.nostrPrivateKey
|
||||||
?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()
|
|
||||||
if (nostrPrivateKey == null) {
|
if (nostrPrivateKey == null) {
|
||||||
uiState = SelectSubgroupAdminsUIState.Error("Couldn't read your keys. Please try again.")
|
uiState = SelectSubgroupAdminsUIState.Error("Couldn't read your keys. Please try again.")
|
||||||
return
|
return
|
||||||
@@ -293,7 +291,7 @@ class SelectSubgroupAdminsViewModel(
|
|||||||
activeUserPublicKey: HexKey,
|
activeUserPublicKey: HexKey,
|
||||||
parentChatRoomId: String,
|
parentChatRoomId: String,
|
||||||
initialUIState: SelectSubgroupAdminsUIState = SelectSubgroupAdminsUIState.Loading,
|
initialUIState: SelectSubgroupAdminsUIState = SelectSubgroupAdminsUIState.Loading,
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
dkgRepository: DkgRepository
|
dkgRepository: DkgRepository
|
||||||
): ViewModelProvider.Factory = viewModelFactory {
|
): ViewModelProvider.Factory = viewModelFactory {
|
||||||
@@ -302,7 +300,7 @@ class SelectSubgroupAdminsViewModel(
|
|||||||
activeUserPublicKey = activeUserPublicKey,
|
activeUserPublicKey = activeUserPublicKey,
|
||||||
parentChatRoomId = parentChatRoomId,
|
parentChatRoomId = parentChatRoomId,
|
||||||
initialUIState = initialUIState,
|
initialUIState = initialUIState,
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
dkgRepository = dkgRepository
|
dkgRepository = dkgRepository
|
||||||
)
|
)
|
||||||
|
|||||||
+49
-14
@@ -12,13 +12,13 @@ import co.touchlab.kermit.Logger
|
|||||||
import fr.acinq.lightning.logging.error
|
import fr.acinq.lightning.logging.error
|
||||||
import fr.acinq.phoenix.PhoenixBusiness
|
import fr.acinq.phoenix.PhoenixBusiness
|
||||||
import fr.acinq.phoenix.PhoenixGlobal
|
import fr.acinq.phoenix.PhoenixGlobal
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
|
||||||
import fr.acinq.phoenix.data.DecryptSeedResult
|
import fr.acinq.phoenix.data.DecryptSeedResult
|
||||||
import fr.acinq.phoenix.data.ElectrumConfig
|
import fr.acinq.phoenix.data.ElectrumConfig
|
||||||
import fr.acinq.phoenix.data.ListWalletState
|
import fr.acinq.phoenix.data.ListWalletState
|
||||||
import fr.acinq.phoenix.data.UserWallet
|
import fr.acinq.phoenix.data.UserWallet
|
||||||
import fr.acinq.phoenix.data.WalletId
|
import fr.acinq.phoenix.data.WalletId
|
||||||
import fr.acinq.phoenix.managers.DataStoreManager
|
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.GlobalPrefs
|
||||||
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
|
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
@@ -33,6 +33,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import press.mantra.compose.identity.Identity
|
||||||
|
import press.mantra.compose.identity.IdentityKind
|
||||||
|
|
||||||
sealed class WritingSeedState {
|
sealed class WritingSeedState {
|
||||||
data object Init : WritingSeedState()
|
data object Init : WritingSeedState()
|
||||||
@@ -83,8 +85,17 @@ class SovereignWalletViewModel(
|
|||||||
val desiredWalletId = _desiredWalletId.asStateFlow()
|
val desiredWalletId = _desiredWalletId.asStateFlow()
|
||||||
val startWalletImmediately = MutableStateFlow(true)
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val exchangeRates = phoenixGlobal.currencyManager.ratesFlow
|
val exchangeRates = phoenixGlobal.currencyManager.ratesFlow
|
||||||
@@ -115,14 +126,38 @@ class SovereignWalletViewModel(
|
|||||||
listAvailableWallets(onDone = {})
|
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) {
|
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 dataStoreManager = DataStoreManager(business)
|
||||||
|
|
||||||
val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = walletId)
|
setActiveIdentity(
|
||||||
val internalPrefs = dataStoreManager.loadInternalPrefsForWallet( walletId = walletId)
|
Identity(
|
||||||
_activeWalletInUI.value = ActiveWallet(id = walletId, business = business, userPrefs = userPrefs, internalPrefs = internalPrefs)
|
id = walletId,
|
||||||
|
kind = IdentityKind.Mnemonic,
|
||||||
|
nostrPrivateKey = keyManager.nostrPrivateKey(),
|
||||||
|
userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = walletId),
|
||||||
|
internalPrefs = dataStoreManager.loadInternalPrefsForWallet(walletId = walletId),
|
||||||
|
business = business,
|
||||||
|
)
|
||||||
|
)
|
||||||
updateBusinessActiveInUI(walletId)
|
updateBusinessActiveInUI(walletId)
|
||||||
// scheduleAutoLock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun listAvailableWallets(onDone: () -> Unit) {
|
fun listAvailableWallets(onDone: () -> Unit) {
|
||||||
@@ -186,7 +221,7 @@ class SovereignWalletViewModel(
|
|||||||
// // TODO: Run this through an expect...
|
// // TODO: Run this through an expect...
|
||||||
// viewModelScope.launch {
|
// viewModelScope.launch {
|
||||||
// autoLockHandler.removeCallbacksAndMessages(null)
|
// autoLockHandler.removeCallbacksAndMessages(null)
|
||||||
// val activeUserPrefs = activeWalletInUI.first()?.userPrefs ?: return@launch
|
// val activeUserPrefs = activeIdentity.first()?.userPrefs ?: return@launch
|
||||||
//
|
//
|
||||||
// val biometricLockEnabled = activeUserPrefs.getLockBiometricsEnabled.first()
|
// val biometricLockEnabled = activeUserPrefs.getLockBiometricsEnabled.first()
|
||||||
// val customPinLockEnabled = activeUserPrefs.getLockPinEnabled.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) {
|
fun switchToWallet(walletId: WalletId) {
|
||||||
_desiredWalletId.value = 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() {
|
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() {
|
fun resetToSelector() {
|
||||||
_desiredWalletId.value = null
|
_desiredWalletId.value = null
|
||||||
_activeWalletInUI.value = null
|
_activeIdentity.value = null
|
||||||
startWalletImmediately.value = false
|
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.NegCloseCmd
|
||||||
import com.vitorpamplona.quartz.nip77Negentropy.NegMsgCmd
|
import com.vitorpamplona.quartz.nip77Negentropy.NegMsgCmd
|
||||||
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
|
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -59,7 +58,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class SynchronizationViewModel(
|
class SynchronizationViewModel(
|
||||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
val activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
val nostrRepository: NostrRepository,
|
val nostrRepository: NostrRepository,
|
||||||
val chatRepository: ChatRepository,
|
val chatRepository: ChatRepository,
|
||||||
val relayRepository: RelayRepository,
|
val relayRepository: RelayRepository,
|
||||||
@@ -67,7 +66,7 @@ class SynchronizationViewModel(
|
|||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
val relaysSocketManager = RelaysSocketManager(
|
val relaysSocketManager = RelaysSocketManager(
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
nostrSocketClientFactory = DefaultNostrSocketClientFactory,
|
nostrSocketClientFactory = DefaultNostrSocketClientFactory,
|
||||||
cachingImportRepository = CachingImportRepository.NO_OP_CACHING_IMPORT_REPOSITORY,
|
cachingImportRepository = CachingImportRepository.NO_OP_CACHING_IMPORT_REPOSITORY,
|
||||||
relayRepository = relayRepository,
|
relayRepository = relayRepository,
|
||||||
@@ -130,7 +129,7 @@ class SynchronizationViewModel(
|
|||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
fun factory(
|
fun factory(
|
||||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
activeIdentityStateFlow: StateFlow<Identity?>,
|
||||||
nostrRepository: NostrRepository,
|
nostrRepository: NostrRepository,
|
||||||
chatRepository: ChatRepository,
|
chatRepository: ChatRepository,
|
||||||
relayRepository: RelayRepository,
|
relayRepository: RelayRepository,
|
||||||
@@ -138,7 +137,7 @@ class SynchronizationViewModel(
|
|||||||
): ViewModelProvider.Factory = viewModelFactory {
|
): ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
SynchronizationViewModel(
|
SynchronizationViewModel(
|
||||||
activeWalletStateFlow = activeWalletStateFlow,
|
activeIdentityStateFlow = activeIdentityStateFlow,
|
||||||
nostrRepository = nostrRepository,
|
nostrRepository = nostrRepository,
|
||||||
chatRepository = chatRepository,
|
chatRepository = chatRepository,
|
||||||
relayRepository = relayRepository,
|
relayRepository = relayRepository,
|
||||||
@@ -188,9 +187,9 @@ class SynchronizationViewModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
activeIdentityStateFlow.collectLatest { identity ->
|
||||||
logger.d("Synchronization ActiveWallet: $activeWallet")
|
logger.d("Synchronization identity: $identity")
|
||||||
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
|
identity?.nostrPrivateKey?.let { nostrPrivateKey ->
|
||||||
val keyPair = KeyPair(
|
val keyPair = KeyPair(
|
||||||
privKey = nostrPrivateKey.value.toByteArray()
|
privKey = nostrPrivateKey.value.toByteArray()
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
package press.mantra.compose.ui.view.model
|
package press.mantra.compose.ui.view.model
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import fr.acinq.phoenix.data.ActiveWallet
|
import press.mantra.compose.identity.Identity
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -102,7 +102,7 @@ class NavigationRoutingTest {
|
|||||||
private fun bootedOn(vararg accounts: LocalAccount): NavigationViewModel {
|
private fun bootedOn(vararg accounts: LocalAccount): NavigationViewModel {
|
||||||
val scope = CoroutineScope(Job())
|
val scope = CoroutineScope(Job())
|
||||||
return NavigationViewModel(
|
return NavigationViewModel(
|
||||||
activeWalletStateFlow = MutableStateFlow<ActiveWallet?>(null),
|
activeIdentityStateFlow = MutableStateFlow<Identity?>(null),
|
||||||
initialNavigationUIState = stranded,
|
initialNavigationUIState = stranded,
|
||||||
nostrRepository = Device(accounts.toList()),
|
nostrRepository = Device(accounts.toList()),
|
||||||
scope = scope,
|
scope = scope,
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ import kotlin.time.Instant
|
|||||||
*
|
*
|
||||||
* What is asserted here is the DAO's half: that the backlog arrives whole and in a stable
|
* What is asserted here is the DAO's half: that the backlog arrives whole and in a stable
|
||||||
* order, and that publishing some rows neither disturbs nor depends on the others. The loop
|
* order, and that publishing some rows neither disturbs nor depends on the others. The loop
|
||||||
* that walks it lives in NotaryViewModel, which wants an ActiveWallet flow to stand up; the
|
* that walks it lives in NotaryViewModel, which wants an Identity flow to stand up; the
|
||||||
* [drain] below is shaped like it but is this test's own, so it pins the queue rather than the
|
* [drain] below is shaped like it but is this test's own, so it pins the queue rather than the
|
||||||
* collector.
|
* collector.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
package press.mantra.compose.ui.view.model
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.PrivateKey
|
||||||
|
import fr.acinq.phoenix.utils.preferences.InternalPrefs
|
||||||
|
import fr.acinq.phoenix.utils.preferences.UserPrefs
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import press.mantra.compose.database.model.Profile
|
||||||
|
import press.mantra.compose.database.model.UnsignedNostrEvent
|
||||||
|
import press.mantra.compose.database.model.intermdiate.LocalAccount
|
||||||
|
import press.mantra.compose.identity.Identity
|
||||||
|
import press.mantra.compose.identity.IdentityKind
|
||||||
|
import press.mantra.compose.identity.toWalletId
|
||||||
|
import press.mantra.compose.repository.NostrRepository
|
||||||
|
import press.mantra.compose.ui.view.state.NavigationUIState
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An identity with no node behind it is routed like any other.
|
||||||
|
*
|
||||||
|
* `NavigationViewModel.observeProfile` used to reach the active user's pubkey through
|
||||||
|
* the wallet -- `business.walletManager.keyManager.value.nostrPublicKey()` -- and
|
||||||
|
* answered a null `business` by navigating back to startup. For a wallet that is a
|
||||||
|
* loop the startup screen breaks by starting the node; for an identity that will
|
||||||
|
* never have a node it is a loop with no exit. The [Identity] type carries the key
|
||||||
|
* itself, so the branch is gone, and this pins that it stays gone: a `NostrSecret`
|
||||||
|
* identity with a signed, synced account on the device lands on `ProfileLoaded`.
|
||||||
|
*
|
||||||
|
* A jvm test rather than a common one because [Identity] derives its public key on
|
||||||
|
* construction, which takes the secp256k1 natives, and those are on this classpath
|
||||||
|
* (see the other tests in `jvmTest` that build a `PrivateKey`).
|
||||||
|
*
|
||||||
|
* The observer waits 2.1 seconds on `Dispatchers.IO` before its first read, so this
|
||||||
|
* runs under `runBlocking` and a real-time `withTimeout`: `runTest` would advance a
|
||||||
|
* virtual clock past the timeout while the real delay had not elapsed.
|
||||||
|
*/
|
||||||
|
class NavigationIdentityRoutingJvmTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
private val stranded = NavigationUIState.Loading("Introducing... Torch")
|
||||||
|
|
||||||
|
private val privateKey = PrivateKey(ByteVector32("01".repeat(32)))
|
||||||
|
|
||||||
|
private fun prefsStore(name: String) = PreferenceDataStoreFactory.createWithPath {
|
||||||
|
temporaryFolder.newFolder().resolve("$name.preferences_pb").path.toPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nostrSecretIdentity() = Identity(
|
||||||
|
id = privateKey.publicKey().xOnly().toWalletId(),
|
||||||
|
kind = IdentityKind.NostrSecret,
|
||||||
|
nostrPrivateKey = privateKey,
|
||||||
|
userPrefs = UserPrefs(prefsStore("user")),
|
||||||
|
internalPrefs = InternalPrefs(prefsStore("internal")),
|
||||||
|
business = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Everything `observeProfile` reads, and nothing else; every other member throws. */
|
||||||
|
private class Device(
|
||||||
|
private val accounts: List<LocalAccount>
|
||||||
|
) : NostrRepository by NostrRepository.NO_OP_NOSTR_REPOSITORY {
|
||||||
|
override suspend fun getLocalAccounts(): List<LocalAccount> = accounts
|
||||||
|
override suspend fun observeLocalAccount(publicKey: HexKey): Flow<LocalAccount?> =
|
||||||
|
flowOf(accounts.firstOrNull { it.unsignedNostrEvent?.pubKey == publicKey })
|
||||||
|
}
|
||||||
|
|
||||||
|
private val at = Instant.fromEpochSeconds(1_700_000_000)
|
||||||
|
|
||||||
|
private fun syncedAccount(publicKey: HexKey) = LocalAccount(
|
||||||
|
unsignedNostrEvent = UnsignedNostrEvent(
|
||||||
|
id = 1,
|
||||||
|
pubKey = publicKey,
|
||||||
|
kind = 0,
|
||||||
|
tags = emptyArray(),
|
||||||
|
content = "{}",
|
||||||
|
signedAt = at,
|
||||||
|
createdAt = at,
|
||||||
|
updatedAt = at,
|
||||||
|
savedAt = at,
|
||||||
|
),
|
||||||
|
nostrEvent = null,
|
||||||
|
profile = Profile(
|
||||||
|
publicKey = publicKey,
|
||||||
|
nostrEventId = "e".repeat(64),
|
||||||
|
createdAt = at,
|
||||||
|
updatedAt = at,
|
||||||
|
savedAt = at,
|
||||||
|
),
|
||||||
|
broadcastNostrEventRequest = null,
|
||||||
|
broadcastNostrEventReceipt = null,
|
||||||
|
synchronizeNostrEventRequests = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the identity's public key is the x-only key nostr uses`() {
|
||||||
|
val identity = nostrSecretIdentity()
|
||||||
|
|
||||||
|
// The same key quartz derives, so the notary's KeyPair and this agree on who
|
||||||
|
// the user is. Not the 33-byte compressed form the library's own helper returns.
|
||||||
|
assertEquals(
|
||||||
|
KeyPair(privKey = privateKey.value.toByteArray()).pubKey.toHexKey(),
|
||||||
|
identity.nostrPublicKey,
|
||||||
|
)
|
||||||
|
assertEquals(64, identity.nostrPublicKey.length)
|
||||||
|
// Same shape as a wallet's id: forty hex characters of a hash160.
|
||||||
|
assertEquals(40, identity.id.nodeIdHash.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an identity with no node behind it is routed by its account, not back to startup`() {
|
||||||
|
val identity = nostrSecretIdentity()
|
||||||
|
val scope = CoroutineScope(Job())
|
||||||
|
val viewModel = NavigationViewModel(
|
||||||
|
activeIdentityStateFlow = MutableStateFlow<Identity?>(identity),
|
||||||
|
initialNavigationUIState = stranded,
|
||||||
|
nostrRepository = Device(listOf(syncedAccount(identity.nostrPublicKey))),
|
||||||
|
scope = scope,
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val destination = runBlocking {
|
||||||
|
withTimeout(15_000) { viewModel.navigationUIState.first { it != stranded } }
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
NavigationUIState.ProfileLoaded(publicKey = identity.nostrPublicKey),
|
||||||
|
destination,
|
||||||
|
"a nodeless identity was not routed by its account",
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user