Load the encrypted seed

This commit is contained in:
Kgothatso Ngako
2026-06-17 17:10:53 +03:00
parent a825038c06
commit 51873efb2a
23 changed files with 1473 additions and 652 deletions

View File

@@ -59,7 +59,7 @@ abstract class NostrDao(
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
logger.i("Publish Nostr Event: $nostrEvent ($relayURLs)")
// Update unsignedEvent with signedTime time...
@@ -76,7 +76,7 @@ abstract class NostrDao(
relayURL = relayURLs.first(),
synchronizationRelayURLs = relayURLs,
level = 0,
walletManager = walletManager
activeKeyPair = activeKeyPair
)
relayURLs.forEach { relayURL ->
@@ -96,7 +96,7 @@ abstract class NostrDao(
relayURL: String,
synchronizationRelayURLs: List<String>,
level: Int,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
val storedNostrEvent = database.nostrEventDao().getNostrEventById(nostrEvent.id)
@@ -135,7 +135,7 @@ abstract class NostrDao(
relayURL = relayURL,
synchronizationRelayURLs = synchronizationRelayURLs,
level = level,
walletManager = walletManager
activeKeyPair = activeKeyPair
)
}
@@ -144,7 +144,7 @@ abstract class NostrDao(
relayURL: String,
synchronizationRelayURLs: List<String>,
level: Int,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
val profilePublicKeysToSync = mutableMapOf<String, MutableSet<String>>()
val eventIdsToSync = mutableMapOf<String, MutableSet<String>>()
@@ -544,259 +544,246 @@ abstract class NostrDao(
// }
// }
// }
walletManager.keyManager.value?.let { keyManager ->
keyManager.nostrPrivateKey()
}
if (walletManager.isLoaded()) {
val nostrKey = walletManager.keyManager.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
val activeKeyPair = KeyPair(
privKey = nostrPrivateKey.value.toByteArray()
giftWrapMessage.decryptGiftWrapSeal(
activeKeyPair
).let { giftWrapSeal ->
if (giftWrapSeal == null) {
throw GiftWrapUnsealException("Failed to unseal ${nostrEvent.id} from the giftWrap")
} else {
database.giftWrapSealDao().upsert(
giftWrapSeal
)
giftWrapMessage.decryptGiftWrapSeal(
activeKeyPair
).let { giftWrapSeal ->
if (giftWrapSeal == null) {
throw GiftWrapUnsealException("Failed to unseal ${nostrEvent.id} from the giftWrap")
giftWrapSeal.decryptGiftWrapPayload(
activeKeyPair.privKey!!
).let { decryptedGiftWrapPayload ->
if (decryptedGiftWrapPayload == null) {
throw GiftWrapSealDecryptionException("Failed to decrypt sealed ${giftWrapSeal.id} payload")
} else {
database.giftWrapSealDao().upsert(
giftWrapSeal
if (giftWrapSeal.publicKey.lowercase() != decryptedGiftWrapPayload.publicKey.lowercase()) {
val giftWrapImpersonation = GiftWrapImpersonationException(
"Seal pubkey (${giftWrapSeal.publicKey}) and payload pubkey (${decryptedGiftWrapPayload.publicKey}) needs to be the same for the giftWrap ${nostrEvent.id}"
)
logger.e("Impersonation", giftWrapImpersonation)
throw giftWrapImpersonation // Once this is thrown the database transaction fails and nothing gets saved to the DB...
}
val result = database.giftWrapPayloadDao().upsert(
decryptedGiftWrapPayload
)
giftWrapSeal.decryptGiftWrapPayload(
activeKeyPair.privKey!!
).let { decryptedGiftWrapPayload ->
if (decryptedGiftWrapPayload == null) {
throw GiftWrapSealDecryptionException("Failed to decrypt sealed ${giftWrapSeal.id} payload")
} else {
if (giftWrapSeal.publicKey.lowercase() != decryptedGiftWrapPayload.publicKey.lowercase()) {
val giftWrapImpersonation = GiftWrapImpersonationException(
"Seal pubkey (${giftWrapSeal.publicKey}) and payload pubkey (${decryptedGiftWrapPayload.publicKey}) needs to be the same for the giftWrap ${nostrEvent.id}"
val giftWrapPayload = if (result != -1L) {
decryptedGiftWrapPayload.copy(
id = result
)
} else {
decryptedGiftWrapPayload
}
val chatRoomId =
giftWrapPayload.aggregatedParticipantsPublicKey()
logger.d("ChatRoomId: $chatRoomId")
if (chatRoomId != null) {
// Find or create chatRoom
val localChatRoom = database.chatRoomDao().findChatRoomById(
chatRoomId
)
if (localChatRoom == null) {
val userPublicKey = activeKeyPair.pubKey.toHex()
database.chatRoomDao().upsert(
ChatRoom(
id = chatRoomId,
userPublicKey = userPublicKey,
subject = decryptedGiftWrapPayload.parseSubject(),
createdAt = decryptedGiftWrapPayload.createdAt,
initialGiftWrapPayloadId = giftWrapPayload.id
)
logger.e("Impersonation", giftWrapImpersonation)
throw giftWrapImpersonation // Once this is thrown the database transaction fails and nothing gets saved to the DB...
}
val result = database.giftWrapPayloadDao().upsert(
decryptedGiftWrapPayload
)
val giftWrapPayload = if (result != -1L) {
decryptedGiftWrapPayload.copy(
id = result
// Add participants...
val participants = giftWrapPayload.participantPTags(
NormalizedRelayUrl(relayURL) // TODO: Get relayURL for publicKey profile...
).map {
Participant(
participantPublicKey = it.pubKey,
chatRoomId = chatRoomId,
relayHint = it.relayHint?.url
)
} else {
decryptedGiftWrapPayload
}
val chatRoomId =
giftWrapPayload.aggregatedParticipantsPublicKey()
logger.d("ChatRoomId: $chatRoomId")
// Sync missing participant profiles...
participants.forEach { participant ->
val giftWrapParticipantProfile =
database.profileDao()
.getProfileByPublicKey(participant.participantPublicKey)
if (chatRoomId != null) {
// Find or create chatRoom
val localChatRoom = database.chatRoomDao().findChatRoomById(
chatRoomId
)
if (localChatRoom == null) {
val userPublicKey = activeKeyPair.pubKey.toHex()
database.chatRoomDao().upsert(
ChatRoom(
id = chatRoomId,
userPublicKey = userPublicKey,
subject = decryptedGiftWrapPayload.parseSubject(),
createdAt = decryptedGiftWrapPayload.createdAt,
initialGiftWrapPayloadId = giftWrapPayload.id
if (giftWrapParticipantProfile == null) {
// Save a placeholder
database.profileDao().insertPlaceholderProfile(
Profile(
displayName = "LOADING...",
publicKey = participant.participantPublicKey,
createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
)
)
// Add participants...
val participants = giftWrapPayload.participantPTags(
NormalizedRelayUrl(relayURL) // TODO: Get relayURL for publicKey profile...
).map {
Participant(
participantPublicKey = it.pubKey,
chatRoomId = chatRoomId,
relayHint = it.relayHint?.url
val recommendRelayUrl =
giftWrapMessage.receiverRelayHit
if (recommendRelayUrl != null) {
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
profilePublicKeysToSync[recommendRelayUrl] =
mutableSetOf()
}
profilePublicKeysToSync[recommendRelayUrl]?.add(
participant.participantPublicKey
)
}
// Sync missing participant profiles...
participants.forEach { participant ->
val giftWrapParticipantProfile =
database.profileDao()
.getProfileByPublicKey(participant.participantPublicKey)
if (giftWrapParticipantProfile == null) {
// Save a placeholder
database.profileDao().insertPlaceholderProfile(
Profile(
displayName = "LOADING...",
publicKey = participant.participantPublicKey,
createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
)
)
val recommendRelayUrl =
giftWrapMessage.receiverRelayHit
if (recommendRelayUrl != null) {
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
profilePublicKeysToSync[recommendRelayUrl] =
mutableSetOf()
}
profilePublicKeysToSync[recommendRelayUrl]?.add(
participant.participantPublicKey
)
} else {
if (profilePublicKeysToSync[relayURL] == null) {
profilePublicKeysToSync[relayURL] =
mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(
participant.participantPublicKey
)
}
} else {
if (profilePublicKeysToSync[relayURL] == null) {
profilePublicKeysToSync[relayURL] =
mutableSetOf()
}
}
database.participantDao().upsert(
participants
)
participants.filter { it.participantPublicKey != userPublicKey }
.forEach { participant ->
val publicKey = participant.participantPublicKey
val chatMessageRelayListEvent =
database.nostrEventDao()
.getAuthoredNostrEvents(
kinds = arrayOf(
ChatMessageRelayListEvent.KIND
),
authors = arrayOf(
publicKey
),
since = GENESIS_AT,
limit = 5
).firstOrNull()?.let { nostrEvent ->
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
null
} else {
ChatMessageRelayListEvent(
id = nostrEvent.id,
tags = nostrEvent.tags,
pubKey = nostrEvent.pubKey,
content = nostrEvent.content,
createdAt = nostrEvent.createdAt.epochSeconds,
sig = nostrEvent.sig
)
}
}
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay that were sent by us
val synchronizationFilter =
SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
authors = arrayOf(
userPublicKey
),
tags = mapOf(
Pair(
"p",
listOf(participant.participantPublicKey)
)
),
limit = 50
)
database.negentropySynchronizeRequestDao()
.insert(
chatMessageRelayListEvent.relays()
.map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
// Sync ChatMessageRelayListEvent publicKey...
// TODO: Get relayHint form participant...
if (profilePublicKeysToSync.containsKey(
relayURL
)
) {
profilePublicKeysToSync[relayURL] =
mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(
participant.participantPublicKey
)
}
}
} else {
giftWrapPayload.parseSubject()?.let { subject ->
database.chatRoomDao().upsert(
localChatRoom.chatRoom.copy(
subject = subject,
updatedAt = giftWrapPayload.createdAt
)
profilePublicKeysToSync[relayURL]?.add(
participant.participantPublicKey
)
}
}
}
val chatMessageId = database.chatMessageDao().upsert(
ChatMessage(
giftWrapPayloadId = giftWrapPayload.id,
senderPublicKey = giftWrapPayload.publicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == giftWrapPayload.publicKey,
chatRoomId = chatRoomId,
createdAt = giftWrapPayload.createdAt,
content = giftWrapPayload.content,
database.participantDao().upsert(
participants
)
participants.filter { it.participantPublicKey != userPublicKey }
.forEach { participant ->
val publicKey = participant.participantPublicKey
val chatMessageRelayListEvent =
database.nostrEventDao()
.getAuthoredNostrEvents(
kinds = arrayOf(
ChatMessageRelayListEvent.KIND
),
authors = arrayOf(
publicKey
),
since = GENESIS_AT,
limit = 5
).firstOrNull()?.let { nostrEvent ->
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
null
} else {
ChatMessageRelayListEvent(
id = nostrEvent.id,
tags = nostrEvent.tags,
pubKey = nostrEvent.pubKey,
content = nostrEvent.content,
createdAt = nostrEvent.createdAt.epochSeconds,
sig = nostrEvent.sig
)
}
}
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay that were sent by us
val synchronizationFilter =
SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
authors = arrayOf(
userPublicKey
),
tags = mapOf(
Pair(
"p",
listOf(participant.participantPublicKey)
)
),
limit = 50
)
database.negentropySynchronizeRequestDao()
.insert(
chatMessageRelayListEvent.relays()
.map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
// Sync ChatMessageRelayListEvent publicKey...
// TODO: Get relayHint form participant...
if (profilePublicKeysToSync.containsKey(
relayURL
)
) {
profilePublicKeysToSync[relayURL] =
mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(
participant.participantPublicKey
)
}
}
} else {
giftWrapPayload.parseSubject()?.let { subject ->
database.chatRoomDao().upsert(
localChatRoom.chatRoom.copy(
subject = subject,
updatedAt = giftWrapPayload.createdAt
)
)
val broadcastNostrEventReceiptId =
database.broadcastNostrEventReceiptDao().upsert(
BroadcastNostrEventReceipt(
nostrEventId = nostrEvent.id,
isAccepted = true,
isSync = true,
relayURL = relayURL
)
)
database.chatMessageBroadcastNostrEventReceiptRelationDao()
.upsert(
ChatMessageBroadcastNostrEventReceiptRelation(
broadcastNostrEventReceiptId = broadcastNostrEventReceiptId,
chatMessageId = chatMessageId
)
)
} else {
logger.w("Failed to setup chatRoom for message")
}
}
val chatMessageId = database.chatMessageDao().upsert(
ChatMessage(
giftWrapPayloadId = giftWrapPayload.id,
senderPublicKey = giftWrapPayload.publicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == giftWrapPayload.publicKey,
chatRoomId = chatRoomId,
createdAt = giftWrapPayload.createdAt,
content = giftWrapPayload.content,
)
)
val broadcastNostrEventReceiptId =
database.broadcastNostrEventReceiptDao().upsert(
BroadcastNostrEventReceipt(
nostrEventId = nostrEvent.id,
isAccepted = true,
isSync = true,
relayURL = relayURL
)
)
database.chatMessageBroadcastNostrEventReceiptRelationDao()
.upsert(
ChatMessageBroadcastNostrEventReceiptRelation(
broadcastNostrEventReceiptId = broadcastNostrEventReceiptId,
chatMessageId = chatMessageId
)
)
} else {
logger.w("Failed to setup chatRoom for message")
}
}
}
}
}
}
nostrEvent.toPost()?.let { post ->

View File

@@ -27,6 +27,7 @@ import ac.cord.auxiliary.compose.repository.RelayRepository
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
@@ -56,7 +57,6 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import fr.acinq.phoenix.managers.WalletManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -390,13 +390,13 @@ class DatabaseNostrRepository(
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
database.nostrDao().publishNostrEvent(
unsignedNostrEvent,
nostrEvent,
relayURLs,
walletManager = walletManager
activeKeyPair = activeKeyPair
)
}
@@ -462,7 +462,7 @@ class DatabaseNostrRepository(
nostrEvent: NostrEvent,
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
synchronizationRelayURLs: List<String>,
walletManager: WalletManager,
activeKeyPair: KeyPair,
) {
storeNostrEventMutex.withLock {
logger.d("saveNostrEvent: $nostrEvent")
@@ -474,7 +474,7 @@ class DatabaseNostrRepository(
synchronizationRelayURLs = synchronizationRelayURLs,
relayURL = synchronizeNostrEventRequest.relayURL,
level = synchronizeNostrEventRequest.level,
walletManager = walletManager
activeKeyPair = activeKeyPair
)
database.synchronizeNostrEventRequestDao().upsert(
@@ -494,7 +494,7 @@ class DatabaseNostrRepository(
nostrEvent: NostrEvent,
negentropySynchronizeRequest: NegentropySynchronizeRequest,
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
logger.d("saveNostrEvent: $nostrEvent")
@@ -504,7 +504,7 @@ class DatabaseNostrRepository(
relayURL = negentropySynchronizeRequest.relayURL,
synchronizationRelayURLs = synchronizationRelayURLs,
level = negentropySynchronizeRequest.level,
walletManager = walletManager
activeKeyPair = activeKeyPair
)
database.negentropySynchronizeRequestDao().upsert(

View File

@@ -22,10 +22,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
import fr.acinq.phoenix.data.ActiveWallet
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import fr.acinq.phoenix.managers.nostrPublicKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
@@ -35,10 +37,10 @@ import kotlinx.coroutines.flow.collectLatest
* https://github.com/PrimalHQ/primal-android-app/blob/6db2e6862239335dede4162338d7c4f10ad35031/app/src/main/kotlin/net/primal/android/networking/relays/RelaysSocketManager.kt
*/
class RelaysSocketManager constructor(
private val activeWalletStateFlow: StateFlow<ActiveWallet?>,
private val nostrSocketClientFactory: NostrSocketClientFactory,
private val cachingImportRepository: CachingImportRepository,
private val relayRepository: RelayRepository,
private val walletManager: WalletManager
) {
val logger = Logger.withTag("RelaysSocketManager")
private val scope = CoroutineScope(Dispatchers.IO)
@@ -67,13 +69,19 @@ class RelaysSocketManager constructor(
private fun observeActiveUserId() =
scope.launch {
walletManager.keyManager.collectLatest { keyManager ->
keyManager?.nostrPublicKey()?.value?.toHex()?.let { pubkey ->
observeRelayJobs[pubkey]?.cancel()
observeRelayJobs[pubkey] = observeRelays(pubkey)
activeWalletStateFlow.collectLatest { activeWallet ->
if (activeWallet == null) {
// TODO: Cancel all pending jobs?
}
activeWallet?.business?.walletManager?.keyManager?.collectLatest { keyManager ->
keyManager?.nostrPublicKey()?.let { pubkey ->
observeRelayJobs[pubkey]?.cancel()
observeRelayJobs[pubkey] = observeRelays(pubkey)
}
}
}
}
private fun observeRelays(publicKey: String): Job =

View File

@@ -19,7 +19,7 @@ import ac.cord.auxiliary.compose.database.model.typealiases.SynchronizationFilte
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import fr.acinq.phoenix.managers.WalletManager
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import kotlinx.coroutines.flow.Flow
interface NostrRepository {
@@ -67,7 +67,7 @@ interface NostrRepository {
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String> = emptyList(),
walletManager: WalletManager
activeKeyPair: KeyPair
)
suspend fun broadcastProcessed(broadcastNostrEventRequest: BroadcastNostrEventRequest, status: String = "processing")
@@ -85,14 +85,14 @@ interface NostrRepository {
nostrEvent: NostrEvent,
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
)
suspend fun saveNostrEvent(
nostrEvent: NostrEvent,
negentropySynchronizeRequest: NegentropySynchronizeRequest,
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
)
suspend fun queueSynchronizeNostrEvent(
@@ -206,7 +206,7 @@ interface NostrRepository {
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
}
@@ -234,7 +234,7 @@ interface NostrRepository {
nostrEvent: NostrEvent,
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
TODO("Not yet implemented")
}
@@ -243,7 +243,7 @@ interface NostrRepository {
nostrEvent: NostrEvent,
negentropySynchronizeRequest: NegentropySynchronizeRequest,
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
activeKeyPair: KeyPair
) {
TODO("Not yet implemented")
}

View File

@@ -45,17 +45,14 @@ import fr.acinq.phoenix.managers.WalletManager
@Composable
fun CreateProfileScreen(
initialCreateProfileUIState: CreateProfileUIState = CreateProfileUIState.Declaration,
onNavigateToSocialPreconditionRoute: () -> Unit,
onNavigateToEndThis: () -> Unit,
nostrRepository: NostrRepository,
writeSeed: (List<String>) -> Unit,
walletManager: WalletManager
writeSeed: (List<String>) -> Unit
) {
val createProfileViewModel: CreateProfileViewModel = viewModel (
factory = CreateProfileViewModel.factory(
initialCreateProfileUIState,
nostrRepository,
walletManager = walletManager
nostrRepository
)
)
Scaffold { innerPadding ->
@@ -319,8 +316,7 @@ fun CreateProfileScreen(
contentColor = Color.White
),
onClick = {
onNavigateToSocialPreconditionRoute.invoke()
createProfileViewModel.createAccount(writeSeed)
}
) {
Text(
@@ -366,7 +362,6 @@ fun CreateAccountScreenPreview() {
modifier = Modifier.fillMaxSize()
) {
CreateProfileScreen(
walletManager = WalletManager(Chain.Mainnet),
initialCreateProfileUIState =
// CreateProfileUIState.InputPrompt,
// CreateProfileUIState.Error,
@@ -403,7 +398,6 @@ fun CreateAccountScreenPreview() {
about = "Polyglot: Math... is my middle name, computer scientist. cryptographer, and gold hider."
)
),
onNavigateToSocialPreconditionRoute = {},
onNavigateToEndThis = {},
writeSeed = {},
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY

View File

@@ -7,18 +7,22 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun LoadingScreen(
text: String? = null
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
LoadingDataIndicator()
LoadingDataIndicator(
text = text
)
}
}
}
@@ -31,6 +35,8 @@ private fun LoadingScreenPreview() {
modifier = Modifier.fillMaxSize()
) {
LoadingScreen(
text = "Starting up"
)
}
}

View File

@@ -0,0 +1,363 @@
package ac.cord.auxiliary.compose.ui.composable
import ac.cord.auxiliary.compose.ui.composable.widgets.LoadingDataIndicator
import ac.cord.auxiliary.compose.ui.composable.widgets.wallet.WalletsSelector
import ac.cord.auxiliary.compose.ui.view.model.SovereignWalletStartupViewModel
import ac.cord.auxiliary.compose.ui.view.model.SovereignWalletViewModel
import ac.cord.auxiliary.compose.ui.view.model.StartupViewState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import fr.acinq.phoenix.data.ListWalletState
import fr.acinq.phoenix.data.UserWallet
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.managers.DataStoreManager
import fr.acinq.phoenix.managers.NodeParamsManager
import fr.acinq.phoenix.utils.preferences.UserPrefs
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
import fr.acinq.phoenix.utils.preferences.getByWalletIdOrDefault
import kotlinx.coroutines.flow.first
import kotlin.collections.get
@Composable
fun SovereignWalletStartupScreen(
sovereignWalletViewModel: SovereignWalletViewModel,
onNavigateToWalletIntroPage: () -> Unit,
onNavigateToWalletLandingPage: () -> Unit,
onNavigateToWalletDashboard: () -> Unit,
forceWalletId: WalletId?,
) {
val sovereignWalletStartupViewModel = viewModel<SovereignWalletStartupViewModel>(
factory = SovereignWalletStartupViewModel.factory(
phoenixGlobal = sovereignWalletViewModel.phoenixGlobal,
)
)
val showIntro = sovereignWalletStartupViewModel.getShowIntroFlow().collectAsState(initial = null)
if (showIntro.value == true) {
LaunchedEffect(Unit) { onNavigateToWalletIntroPage.invoke() }
}
Box(
modifier = Modifier
.fillMaxSize()
.imePadding(),
contentAlignment = Alignment.Center
) {
when (sovereignWalletViewModel.listWalletState.value) {
is ListWalletState.Init -> {
LoadingDataIndicator(
text = "Decrypting..."
)
}
is ListWalletState.Error -> {
ImplementationPendingScreen(
"Failed to load wallet data",
)
}
is ListWalletState.Success -> {
val globalPrefs = sovereignWalletViewModel.getGlobalPrefs()
val availableWallets by sovereignWalletViewModel.availableWallets.collectAsState()
val defaultWallet = globalPrefs.getDefaultWallet.collectAsState(null)
val startWalletImmediately by sovereignWalletViewModel.startWalletImmediately.collectAsState()
val availableWalletMetadataPrefs = globalPrefs.getAvailableWalletsMeta.collectAsState(null)
val availableWalletMetadata = availableWalletMetadataPrefs.value
val desiredWalletIdFlow = sovereignWalletViewModel.desiredWalletId.collectAsState()
val desiredWalletId = desiredWalletIdFlow.value
val activeWalletFlow = sovereignWalletViewModel.activeWalletInUI.collectAsState()
val activeWallet = activeWalletFlow.value
when {
availableWallets.isEmpty() -> {
LaunchedEffect(Unit) { onNavigateToWalletLandingPage.invoke() }
LoadingDataIndicator(text = "Initializing...")
}
availableWalletMetadata == null || defaultWallet.value == null -> {
LoadingDataIndicator(text = "Preparing wallet...")
}
activeWallet != null -> {
LoadingDataIndicator(text = "Opening Wallet")
LaunchedEffect(Unit) {
sovereignWalletViewModel.loadSovereignData(activeWallet.id)
onNavigateToWalletDashboard.invoke()
}
}
else -> {
when (val startupState = sovereignWalletStartupViewModel.state.value) {
is StartupViewState.Init -> {
var loadingWallet by remember {
mutableStateOf(
when {
forceWalletId != null -> availableWallets[forceWalletId]
!startWalletImmediately -> null
availableWallets.size == 1 -> availableWallets.entries.firstOrNull()?.value
desiredWalletId != null -> availableWallets[desiredWalletId]
startWalletImmediately -> availableWallets[defaultWallet.value]
else -> null
}
)
}
when (val wallet = loadingWallet) {
null -> {
WalletsSelector(
wallets = availableWallets,
globalPrefs = globalPrefs,
walletsMetadata = availableWalletMetadata,
activeWalletId = null,
onWalletClick = { sovereignWalletViewModel.switchToWallet(it.walletId) ; loadingWallet = it },
canEdit = false,
modifier = Modifier.padding(horizontal = 24.dp),
topContent = {
Spacer(Modifier.height(64.dp))
Text(text = "Select a wallet", style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.height(16.dp))
},
bottomContent = {
Spacer(Modifier.height(128.dp))
}
)
}
else -> {
val metadata = remember { availableWalletMetadata.getByWalletIdOrDefault(wallet.walletId) }
val dataStoreManager = DataStoreManager(
ctx = sovereignWalletViewModel.phoenixGlobal.ctx,
chain = NodeParamsManager.chain
)
LoadWallet(
userWallet = wallet,
metadata = metadata,
userPrefs = dataStoreManager.loadUserPrefsForWallet(wallet.walletId),
promptScreenLockImmediately = startWalletImmediately,
doLoadWallet = { userWallet ->
sovereignWalletStartupViewModel.startupNode(walletId = userWallet.walletId, words = userWallet.words, onStartupSuccess = {
sovereignWalletViewModel.setActiveWallet(walletId = userWallet.walletId, business = it)
onNavigateToWalletDashboard.invoke()
})
loadingWallet = null
},
// only show back-to-selector button if there's more than one wallet
goToWalletSelector = availableWallets.takeIf { it.size > 1 }?.let {
{ loadingWallet = null; sovereignWalletViewModel.startWalletImmediately.value = false }
}
)
}
}
}
is StartupViewState.StartingBusiness -> {
LoadingDataIndicator(text = "Starting wallet")
}
is StartupViewState.BusinessActive -> {
LoadingDataIndicator(text = "Opening wallet")
}
is StartupViewState.Error -> {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Startup Error"
)
when (startupState) {
is StartupViewState.Error.Generic -> {
startupState.cause?.message?.let { message ->
Text(
text = message,
)
}
}
}
Spacer(Modifier.height(16.dp))
HorizontalDivider(
modifier = Modifier.width(50.dp)
)
Spacer(Modifier.height(24.dp))
// BorderButton(
// text = stringResource(R.string.startup_error_try_again),
// icon = R.drawable.ic_reset,
// onClick = onTryAgainClick
// )
Spacer(Modifier.height(8.dp))
// StartErrorShareLogsButton()
}
}
}
}
}
}
}
}
}
@Composable
private fun BoxScope.LoadWallet(
userWallet: UserWallet,
metadata: UserWalletMetadata,
userPrefs: UserPrefs,
promptScreenLockImmediately: Boolean,
doLoadWallet: (UserWallet) -> Unit,
goToWalletSelector: (() -> Unit)?
) {
val isScreenLockRequired = produceState<Boolean?>(initialValue = null, key1 = userWallet) {
val biometricLockEnabled = userPrefs.getLockBiometricsEnabled.first()
val customPinLockEnabled = userPrefs.getLockPinEnabled.first()
value = biometricLockEnabled || customPinLockEnabled
}
when (isScreenLockRequired.value) {
null -> {
LoadingDataIndicator(text = "Loading preferences...")
}
true -> {
LoadingDataIndicator(text = "Unlock to continue")
ScreenLockPrompt(
walletId = userWallet.walletId,
walletName = metadata.nameOrDefault(),
promptScreenLockImmediately = promptScreenLockImmediately,
onUnlock = { doLoadWallet(userWallet) },
onLock = { },
userPrefs = userPrefs,
goToWalletSelector = goToWalletSelector,
)
}
false -> {
LoadingDataIndicator(text = "Starting wallet...")
LaunchedEffect(Unit) {
doLoadWallet(userWallet)
}
}
}
}
@Composable
private fun BoxScope.ScreenLockPrompt(
walletId: WalletId,
walletName: String,
userPrefs: UserPrefs,
promptScreenLockImmediately: Boolean,
onLock: () -> Unit,
onUnlock: () -> Unit,
goToWalletSelector: (() -> Unit)?,
) {
Text(
text = "Lock prompt coming soon.",
)
// val scope = rememberCoroutineScope()
//
//
// val isBiometricLockEnabledState = userPrefs.getLockBiometricsEnabled.collectAsState(initial = null)
// val isBiometricLockEnabled = isBiometricLockEnabledState.value
// val isCustomPinLockEnabledState = userPrefs.getLockPinEnabled.collectAsState(initial = null)
// val isCustomPinLockEnabled = isCustomPinLockEnabledState.value
//
// val promptBiometricLock = {
// val promptInfo = BiometricPrompt.PromptInfo.Builder().apply {
// setTitle(context.getString(R.string.lockprompt_title))
// setAllowedAuthenticators(BiometricsHelper.authCreds)
// }.build()
// BiometricsHelper.getPrompt(
// activity = context.findActivity(),
// onSuccess = {
// scope.launch { userPrefs.saveLockPinCodeSuccess() }
// onUnlock()
// },
// onFailure = { onLock() },
// onCancel = { }
// ).authenticate(promptInfo)
// }
//
// var showPinLockDialog by rememberSaveable { mutableStateOf(false) }
// if (showPinLockDialog) {
// CheckScreenLockPinFlow(
// walletId = walletId,
// onCancel = { showPinLockDialog = false },
// onPinValid = { onUnlock() }
// )
// }
//
// if (goToWalletSelector != null) {
//// TODO: BackHandler(enabled = !showPinLockDialog) { goToWalletSelector() }
// TransparentFilledButton(
//// icon = R.drawable.ic_arrow_back,
// onClick = goToWalletSelector,
// modifier = Modifier.align(Alignment.TopStart),
// padding = PaddingValues(24.dp),
// )
// }
//
// Column(
// modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
// horizontalAlignment = Alignment.CenterHorizontally,
// verticalArrangement = Arrangement.spacedBy(16.dp)
// ) {
// if (isBiometricLockEnabled == true) {
// Button(
// text = "System lock",
//// icon = R.drawable.ic_fingerprint,
// onClick = promptBiometricLock,
// modifier = Modifier.fillMaxWidth(),
// backgroundColor = MaterialTheme.colors.surface,
// shape = CircleShape,
// padding = PaddingValues(16.dp),
// )
// }
// if (isCustomPinLockEnabled == true) {
// Button(
// text = "PIN Code",
//// icon = R.drawable.ic_pin,
// onClick = { showPinLockDialog = true },
// modifier = Modifier.fillMaxWidth(),
// backgroundColor = MaterialTheme.colors.surface,
// shape = CircleShape,
// padding = PaddingValues(16.dp),
// )
// }
// Spacer(modifier = Modifier.height(WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()))
// }
//
// when {
// isBiometricLockEnabled == true && isCustomPinLockEnabled == false -> {
// LaunchedEffect(Unit) {
// promptBiometricLock()
// }
// }
//
// isBiometricLockEnabled == false && isCustomPinLockEnabled == true -> {
// LaunchedEffect(Unit) {
// showPinLockDialog = true
// }
// }
//
// promptScreenLockImmediately -> {
// LaunchedEffect(key1 = isBiometricLockEnabled, key2 = isCustomPinLockEnabled, ) {
// if (isBiometricLockEnabled == true) {
// promptBiometricLock()
// } else if (isCustomPinLockEnabled == true) {
// showPinLockDialog = true
// }
// }
// }
// else -> {}
// }
}

View File

@@ -16,6 +16,7 @@ import ac.cord.auxiliary.compose.ui.composable.SearchResultScreen
import ac.cord.auxiliary.compose.ui.composable.SearchScreen
import ac.cord.auxiliary.compose.ui.composable.SignInToProfileScreen
import ac.cord.auxiliary.compose.ui.composable.SocialPreconditionScreen
import ac.cord.auxiliary.compose.ui.composable.SovereignWalletStartupScreen
import ac.cord.auxiliary.compose.ui.composable.UnannouncedProfileScreen
import ac.cord.auxiliary.compose.ui.composable.UnindexedProfileScreen
import ac.cord.auxiliary.compose.ui.composable.UnqueuedProfileScreen
@@ -40,6 +41,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.SocialPreconditionRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.SovereignWalletStartupRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.UnannouncedProfileRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.UnindexedProfileRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.UnqueuedProfileRoute
@@ -49,6 +51,7 @@ import ac.cord.auxiliary.compose.ui.composable.navigation.routes.UnsyncedProfile
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.WriteNewNoteRoute
import ac.cord.auxiliary.compose.ui.view.model.NavigationViewModel
import ac.cord.auxiliary.compose.ui.view.model.NotaryViewModel
import ac.cord.auxiliary.compose.ui.view.model.SovereignWalletViewModel
import ac.cord.auxiliary.compose.ui.view.model.SynchronizationViewModel
import ac.cord.auxiliary.compose.ui.view.state.NavigationUIState
import ac.cord.auxiliary.compose.ui.view.state.NostrEventDetailUIState
@@ -56,6 +59,7 @@ import ac.cord.auxiliary.compose.ui.view.state.SearchUIState
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.LocalLifecycleOwner
@@ -76,7 +80,7 @@ fun AuxNavHost(
navController: NavHostController
) {
val logger = Logger.withTag("AuxNavHost")
// TODO
val exceptionHandler =
CoroutineExceptionHandler { _, throwable ->
logger.e("Caught exception: ${throwable.message}", throwable)
@@ -102,30 +106,38 @@ fun AuxNavHost(
database = auxDatabaseManager.auxDatabase
)
val sovereignWalletViewModel: SovereignWalletViewModel = viewModel(factory = SovereignWalletViewModel.factory(
phoenixGlobal = phoenixGlobal
))
val navigationViewModel: NavigationViewModel = viewModel (
factory = NavigationViewModel.factory(
initialNavigationUIState = NavigationUIState.Loading,
activeWallet = sovereignWalletViewModel.activeWalletInUI,
initialNavigationUIState = NavigationUIState.Loading(),
nostrRepository = databaseNostrRepository,
phoenixGlobal = phoenixGlobal,
scope = applicationIOScope
)
)
// val notaryViewModel: NotaryViewModel = viewModel(
// factory = NotaryViewModel.factory(
// nostrRepository = databaseNostrRepository,
// chatRepository = databaseChatRepository,
// scope = applicationIOScope
// )
// )
// // TODO: Produce a notary UI Element...
// val synchronizationViewModel: SynchronizationViewModel = viewModel(
// factory = SynchronizationViewModel.factory(
// nostrRepository = databaseNostrRepository,
// chatRepository = databaseChatRepository,
// relayRepository = databaseNostrRepository,
// scope = applicationIOScope
// )
// )
val notaryViewModel: NotaryViewModel = viewModel(
factory = NotaryViewModel.factory(
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository,
scope = applicationIOScope
)
)
// TODO: Produce a notary UI Element...
val synchronizationViewModel: SynchronizationViewModel = viewModel(
factory = SynchronizationViewModel.factory(
activeWalletStateFlow = sovereignWalletViewModel.activeWalletInUI,
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository,
relayRepository = databaseNostrRepository,
scope = applicationIOScope
)
)
// TODO: Produce a synchronization UI element...
LaunchedEffect(lifecycleOwner) {
@@ -138,9 +150,18 @@ fun AuxNavHost(
popUpTo(0)
}
}
is NavigationUIState.StartupPhoenix -> {
navController.navigate(
route = SovereignWalletStartupRoute
) {
popUpTo(0)
}
}
is NavigationUIState.Loading -> {
navController.navigate(
route = LoadingRoute
route = LoadingRoute(
text = state.text
)
) {
popUpTo(0)
}
@@ -207,7 +228,9 @@ fun AuxNavHost(
}
is NavigationUIState.ProfileLoaded -> {
navController.navigate(
route = SocialPreconditionRoute
route = SocialPreconditionRoute(
activeUserPubkey = state.publicKey
)
) {
popUpTo(0)
}
@@ -225,10 +248,37 @@ fun AuxNavHost(
NavHost(
navController = navController,
startDestination = LoadingRoute
startDestination = LoadingRoute()
) {
composable<LoadingRoute> {
LoadingScreen()
composable<SovereignWalletStartupRoute> {
SovereignWalletStartupScreen(
sovereignWalletViewModel = sovereignWalletViewModel,
onNavigateToWalletLandingPage = {
navController.navigate(
route = LandingRoute
)
},
onNavigateToWalletIntroPage = {
navController.navigate(
route = LandingRoute
)
},
onNavigateToWalletDashboard = {
navController.navigate(
route = LoadingRoute(
text = "Opening Aux"
)
)
},
forceWalletId = null,
)
}
composable<LoadingRoute> { backStackEntry ->
val route = backStackEntry.toRoute<LoadingRoute>()
LoadingScreen(
text = route.text
)
}
composable<LandingRoute> {
LandingScreen(
@@ -250,37 +300,32 @@ fun AuxNavHost(
)
}
composable<CreateProfileRoute> {
val walletManager = navigationViewModel.activeWalletInUI.value?.business?.walletManager
if (walletManager != null) {
CreateProfileScreen(
onNavigateToSocialPreconditionRoute = {
navController.navigate(
route = SocialPreconditionRoute
)
},
onNavigateToEndThis = {
navController.navigate(
route = BlankRoute
) {
popUpTo(0)
}
},
nostrRepository = databaseNostrRepository,
walletManager = walletManager,
writeSeed = { words ->
navigationViewModel.writeSeed(
words,
isRestoringWallet = false,
onSeedWritten = { walletId ->
}
)
CreateProfileScreen(
onNavigateToEndThis = {
navController.navigate(
route = BlankRoute
) {
popUpTo(0)
}
)
} else {
ImplementationPendingScreen("Something went wrong")
}
},
nostrRepository = databaseNostrRepository,
writeSeed = { words ->
sovereignWalletViewModel.writeSeed(
words,
isRestoringWallet = false,
onSeedWritten = { walletId ->
sovereignWalletViewModel.loadSovereignData(walletId)
sovereignWalletViewModel.listAvailableWallets {
sovereignWalletViewModel.switchToWallet(walletId)
navController.navigate(
route = SovereignWalletStartupRoute
)
}
}
)
}
)
}
composable<WriteNewNoteRoute> { backStackEntry ->
@@ -340,11 +385,14 @@ fun AuxNavHost(
UnannouncedProfileScreen()
}
composable<SocialPreconditionRoute> {
composable<SocialPreconditionRoute> { backStackEntry ->
val route = backStackEntry.toRoute<SocialPreconditionRoute>()
SocialPreconditionScreen(
onNavigateToSkipForNow = {
navController.navigate(
route = FeedRoute
route = FeedRoute(
activeUserPublicKey = route.activeUserPubkey
)
)
},
onNavigateToInviteFriend = {

View File

@@ -0,0 +1,7 @@
package ac.cord.auxiliary.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
data object AuthenticatedProfileRoute {
}

View File

@@ -3,4 +3,6 @@ package ac.cord.auxiliary.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
object LoadingRoute: Route()
data class LoadingRoute(
val text: String? = null
): Route()

View File

@@ -3,4 +3,6 @@ package ac.cord.auxiliary.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
object SocialPreconditionRoute: Route()
data class SocialPreconditionRoute(
val activeUserPubkey: String
): Route()

View File

@@ -0,0 +1,6 @@
package ac.cord.auxiliary.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
data object SovereignWalletStartupRoute

View File

@@ -5,10 +5,12 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -17,6 +19,7 @@ fun LoadingDataIndicator(
modifier: Modifier = Modifier.fillMaxWidth(),
color: Color = MaterialTheme.colorScheme.secondary,
fillScreen: Boolean = true,
text: String? = null
) {
Column(
modifier = modifier,
@@ -32,6 +35,18 @@ fun LoadingDataIndicator(
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)
text?.let {
Spacer(
modifier = Modifier.height(30.dp)
)
Text(
text = text,
modifier = Modifier.fillMaxWidth().padding(10.dp),
textAlign = TextAlign.Center
)
}
if (fillScreen) {
Spacer(modifier = Modifier.weight(3f))
}
@@ -45,7 +60,9 @@ fun LoadingDataIndicator(
private fun TransferHistoryScreenPreview() {
AuxTheme {
Surface(modifier = Modifier.fillMaxSize()) {
LoadingDataIndicator()
LoadingDataIndicator(
text = "Starting up"
)
}
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2025 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ac.cord.auxiliary.compose.ui.composable.widgets.wallet
import ac.cord.auxiliary.compose.ui.composable.widgets.LoadingDataIndicator
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.machankura.compose.ui.composable.widgets.buttons.Clickable
import fr.acinq.phoenix.data.UserWallet
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
import fr.acinq.phoenix.utils.preferences.getByWalletIdOrDefault
@Composable
fun WalletsSelector(
modifier: Modifier = Modifier,
globalPrefs: GlobalPrefs,
wallets: Map<WalletId, UserWallet>,
walletsMetadata: Map<WalletId, UserWalletMetadata>,
activeWalletId: WalletId?,
canEdit: Boolean,
onWalletClick: (UserWallet) -> Unit,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
topContent: @Composable (() -> Unit)? = null,
bottomContent: @Composable (() -> Unit)? = null,
) {
val currentWallet = remember(wallets) { wallets.entries.firstOrNull { it.key == activeWalletId }?.value }
val otherWalletsList = remember(wallets, walletsMetadata) { wallets.entries.filterNot { it.key == activeWalletId || walletsMetadata[it.key]?.isHidden == true }.toList() }
LazyColumn(modifier = modifier, verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment) {
topContent?.let {
item { it.invoke() }
}
if (currentWallet != null) {
item {
AvailableWalletView(
userWallet = currentWallet,
globalPrefs = globalPrefs,
metadata = walletsMetadata.getByWalletIdOrDefault(currentWallet.walletId),
isCurrent = true,
canEdit = canEdit,
onClick = { onWalletClick(currentWallet) }
)
Spacer(Modifier.height(12.dp))
HorizontalDivider()
Spacer(Modifier.height(8.dp))
}
}
items(items = otherWalletsList) { (walletId, userWallet) ->
AvailableWalletView(
userWallet = userWallet,
globalPrefs = globalPrefs,
metadata = walletsMetadata.getByWalletIdOrDefault(walletId),
isCurrent = false,
canEdit = canEdit,
onClick = { onWalletClick(userWallet) }
)
Spacer(Modifier.height(8.dp))
}
bottomContent?.let {
item { it.invoke() }
}
}
}
@Composable
private fun AvailableWalletView(
modifier: Modifier = Modifier,
globalPrefs: GlobalPrefs,
userWallet: UserWallet,
metadata: UserWalletMetadata,
isCurrent: Boolean,
canEdit: Boolean,
onClick: (WalletId) -> Unit,
) {
var showWalletEditDialog by remember { mutableStateOf(false) }
if (showWalletEditDialog) {
LoadingDataIndicator(
text = "Edit function coming soon"
)
// EditWalletDialog(
// onDismiss = { showWalletEditDialog = false },
// walletId = userWallet.walletId,
// globalPrefs = globalPrefs,
// metadata = metadata,
// )
}
Clickable(
modifier = modifier,
onClick = {
if (isCurrent) {
if (canEdit) showWalletEditDialog = true else return@Clickable
} else {
onClick(userWallet.walletId)
}
},
shape = RoundedCornerShape(16.dp),
backgroundColor = MaterialTheme.colorScheme.surface,
) {
Row(modifier = Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 12.dp)
.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
WalletAvatar(avatar = metadata.avatar, backgroundColor = Color.Transparent, internalPadding = PaddingValues(4.dp))
Spacer(Modifier.width(12.dp))
Column {
Text(text = metadata.nameOrDefault(), modifier = Modifier, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium)
Spacer(Modifier.height(2.dp))
Text(text = userWallet.nodeId, modifier = Modifier, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.displayMedium.copy(fontFamily = FontFamily.Monospace, fontSize = 12.sp))
}
}
if (isCurrent && canEdit) {
Spacer(Modifier.width(8.dp))
// TODO: PhoenixIcon(R.drawable.ic_edit, tint = MaterialTheme.colorScheme.primary)
Spacer(Modifier.width(16.dp))
}
}
}
}

View File

@@ -31,22 +31,19 @@ import kotlinx.coroutines.launch
class CreateProfileViewModel(
val initialCreateProfileUIState: CreateProfileUIState,
val createProfileFormState: CreateProfileFormState = CreateProfileFormState(),
val nostrRepository: NostrRepository,
val walletManager: WalletManager
val nostrRepository: NostrRepository
): ViewModel() {
companion object {
private const val TAG = "CreateAccountViewModel"
fun factory(
initialCreateProfileUIState: CreateProfileUIState,
nostrRepository: NostrRepository,
walletManager: WalletManager
nostrRepository: NostrRepository
): ViewModelProvider.Factory = viewModelFactory {
initializer {
CreateProfileViewModel(
initialCreateProfileUIState,
nostrRepository = nostrRepository,
walletManager = walletManager
nostrRepository = nostrRepository
)
}
}
@@ -106,8 +103,10 @@ class CreateProfileViewModel(
remoteSwapInExtendedPublicKey = NodeParamsManager.remoteSwapInXpub
)
val pubkey = localKeyManager.nostrPublicKey()
logger.d("NostrKey: $pubkey")
nostrRepository.createNewProfile(
localKeyManager.nostrPublicKey().toHex(),
pubkey,
name = createProfileFormState.nameField.textFieldState.text.toString(),
biography = createProfileFormState.biographyField.textFieldState.text.toString()
)

View File

@@ -1,94 +1,29 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.extensions.getGlobalPrefs
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.widgets.wallet.WalletAvatars
import ac.cord.auxiliary.compose.ui.view.state.NavigationUIState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import fr.acinq.lightning.logging.error
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.PhoenixGlobal
import fr.acinq.phoenix.data.ActiveWallet
import fr.acinq.phoenix.data.DecryptSeedResult
import fr.acinq.phoenix.data.ElectrumConfig
import fr.acinq.phoenix.data.ListWalletState
import fr.acinq.phoenix.data.UserWallet
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.managers.DataStoreManager
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import fr.acinq.phoenix.managers.nostrPublicKey
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.launch
sealed class WritingSeedState {
data object Init : WritingSeedState()
data class Writing(val mnemonics: List<String>) : WritingSeedState()
data class WrittenToDisk(val walletId: WalletId) : WritingSeedState()
sealed class Error : WritingSeedState() {
data class Generic(val cause: Throwable) : Error()
data object CannotLoadSeedMap: Error()
data object SeedAlreadyExists: Error()
}
}
expect fun updateBusinessActiveInUI(walletId: WalletId)
expect fun loadAndDecryptSeed(phoenixGlobal: PhoenixGlobal): DecryptSeedResult
expect fun getAvailableWalletsMeta(phoenixGlobal: PhoenixGlobal): Flow<Map<WalletId, UserWalletMetadata>>
expect suspend fun saveAvailableWalletMeta(phoenixGlobal: PhoenixGlobal, metadata: UserWalletMetadata)
expect suspend fun saveAvailableWalletMeta(
phoenixGlobal: PhoenixGlobal,
walletId: WalletId,
name: String?,
avatar: String,
isHidden: Boolean
)
expect fun platformWriteSeed(
log: Logger,
phoenixGlobal: PhoenixGlobal,
globalPrefs: GlobalPrefs,
writingState: WritingSeedState,
viewModelScope: CoroutineScope,
mnemonics: List<String>,
onWritingSeedError: (WritingSeedState.Error) -> Unit,
onWritingSeedStateWriting: (WritingSeedState.Writing) -> Unit,
isRestoringWallet: Boolean,
isTorEnabled: Boolean,
customElectrumServer: ElectrumConfig.Custom?,
onSeedWritten: (WalletId) -> Unit
)
class NavigationViewModel(
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
initialNavigationUIState: NavigationUIState,
val phoenixGlobal: PhoenixGlobal,
val nostrRepository: NostrRepository,
val scope: CoroutineScope,
): ViewModel() {
@@ -97,14 +32,14 @@ class NavigationViewModel(
private const val TAG = "NavigationViewModel"
fun factory(
activeWallet: StateFlow<ActiveWallet?>,
initialNavigationUIState: NavigationUIState,
nostrRepository: NostrRepository,
phoenixGlobal: PhoenixGlobal,
scope: CoroutineScope,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
NavigationViewModel(
phoenixGlobal = phoenixGlobal,
activeWallet,
initialNavigationUIState = initialNavigationUIState,
nostrRepository = nostrRepository,
scope = scope
@@ -120,18 +55,6 @@ class NavigationViewModel(
)
val navigationUIState = _navigationUIState.asStateFlow()
val listWalletState = mutableStateOf<ListWalletState>(ListWalletState.Init)
private val _availableWallets = MutableStateFlow<Map<WalletId, UserWallet>>(emptyMap())
val availableWallets = _availableWallets.asStateFlow()
private val _desiredWalletId = MutableStateFlow<WalletId?>(null)
val desiredWalletId = _desiredWalletId.asStateFlow()
val startWalletImmediately = MutableStateFlow(true)
private val _activeWalletInUI = MutableStateFlow<ActiveWallet?>(null)
val activeWalletInUI = _activeWalletInUI.asStateFlow()
init {
observeProfile()
}
@@ -141,241 +64,93 @@ class NavigationViewModel(
scope.launch(Dispatchers.IO) {
delay(2_100) // Looking busy...
logger.i("Navigation UI State is Landing")
activeWalletInUI.collectLatest { activeWallet ->
if (activeWallet == null) {
// TODO: No active wallet
activeWalletStateFlow.collectLatest { activeWallet ->
logger.d("Active Wallet: $activeWallet")
if (activeWallet == null) {
_navigationUIState.getAndUpdate {
NavigationUIState.StartupPhoenix
}
} else {
if (activeWallet.business != null) {
activeWallet.business.walletManager.keyManager.collectLatest { keyManager ->
keyManager?.nostrPublicKey()?.let { nostrPublicKey ->
val publicKey = nostrPublicKey.toHex()
if (activeWallet.business == null) {
_navigationUIState.getAndUpdate {
NavigationUIState.StartupPhoenix
}
} else {
val activeUserPublicKey = activeWallet.business.walletManager.keyManager.value?.nostrPublicKey()
logger.i("Observing: $publicKey")
if (activeUserPublicKey == null) {
_navigationUIState.getAndUpdate {
NavigationUIState.Loading(text = "Couldn't get the nostrKey")
}
} else {
nostrRepository.observeProfile(
publicKey = publicKey
).distinctUntilChanged().collect { localProfile ->
logger.i("Local Profile: $localProfile")
_navigationUIState.getAndUpdate {
if (localProfile == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.unsignedNostrEvent == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.broadcastNostrEventReceipt != null) {
logger.i("We have a broadcast receipt: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.profile != null) {
logger.i("We have successfully synced a profile: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.broadcastNostrEventRequest != null) {
logger.i("We have a broadcast request: ${localProfile.broadcastNostrEventRequest}")
NavigationUIState.UnannouncedProfile(
broadcastNostrEventRequest = localProfile.broadcastNostrEventRequest
)
} else if (localProfile.profile != null) {
logger.i("We have a profile that needs to queued for broadcast: ${localProfile.profile}")
NavigationUIState.UnqueuedProfile(
profile = localProfile.profile
)
} else if (localProfile.nostrEvent != null) {
logger.i("Nostr event with the profile needs to be indexed: ${localProfile.nostrEvent}")
NavigationUIState.UnindexedProfile(
nostrEvent = localProfile.nostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should request to sync profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnqueuedProfileSynchronization(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should be syncing the profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsyncedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
} else {
logger.i("We have an unsigned profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsignedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
}
logger.i("Observing: $activeUserPublicKey")
nostrRepository.observeProfile(
publicKey = activeUserPublicKey
).distinctUntilChanged().collect { localProfile ->
logger.i("Local Profile: $localProfile")
_navigationUIState.getAndUpdate {
if (localProfile == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.unsignedNostrEvent == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.broadcastNostrEventReceipt != null) {
logger.i("We have a broadcast receipt: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.profile != null) {
logger.i("We have successfully synced a profile: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.broadcastNostrEventRequest != null) {
logger.i("We have a broadcast request: ${localProfile.broadcastNostrEventRequest}")
NavigationUIState.UnannouncedProfile(
broadcastNostrEventRequest = localProfile.broadcastNostrEventRequest
)
} else if (localProfile.profile != null) {
logger.i("We have a profile that needs to queued for broadcast: ${localProfile.profile}")
NavigationUIState.UnqueuedProfile(
profile = localProfile.profile
)
} else if (localProfile.nostrEvent != null) {
logger.i("Nostr event with the profile needs to be indexed: ${localProfile.nostrEvent}")
NavigationUIState.UnindexedProfile(
nostrEvent = localProfile.nostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should request to sync profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnqueuedProfileSynchronization(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should be syncing the profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsyncedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
} else {
logger.i("We have an unsigned profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsignedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
}
}
}
}
} else {
// TODO: We have an error somewhere...
}
}
}
}
}
fun setActiveWallet(walletId: WalletId, business: PhoenixBusiness) {
val dataStoreManager = DataStoreManager(business)
val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = walletId)
val internalPrefs = dataStoreManager.loadInternalPrefsForWallet( walletId = walletId)
_activeWalletInUI.value = ActiveWallet(id = walletId, business = business, userPrefs = userPrefs, internalPrefs = internalPrefs)
updateBusinessActiveInUI(walletId)
// scheduleAutoLock()
}
fun listAvailableWallets(onDone: () -> Unit) {
viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e ->
// logger.error("error when initialising startup-view: ", e)
listWalletState.value = ListWalletState.Error.Generic(e)
}) {
when (val result =
loadAndDecryptSeed(phoenixGlobal)) {
is DecryptSeedResult.Failure.SerializationError -> {
logger.error {"cannot deserialize seed file: "}
listWalletState.value = ListWalletState.Error.Serialization
}
is DecryptSeedResult.Failure.DecryptionError -> {
logger.e("cannot decrypt seed file: ", throwable = result.cause)
listWalletState.value = ListWalletState.Error.DecryptionError.GeneralException(result.cause)
}
is DecryptSeedResult.Failure.KeyStoreFailure -> {
logger.e("key store failure: ", throwable = result.cause)
listWalletState.value = ListWalletState.Error.DecryptionError.KeystoreFailure(result.cause)
}
is DecryptSeedResult.Failure.SeedFileUnreadable -> {
logger.e("aborting, unreadable seed file")
listWalletState.value = ListWalletState.Error.Generic(null)
}
is DecryptSeedResult.Failure.SeedInvalid -> {
logger.e("aborting, seed is invalid")
listWalletState.value = ListWalletState.Error.Generic(null)
}
is DecryptSeedResult.Failure.SeedFileNotFound -> {
listWalletState.value = ListWalletState.Success
_availableWallets.value = emptyMap()
}
is DecryptSeedResult.Success -> {
val metadataMap = getAvailableWalletsMeta(
phoenixGlobal
).first()
result.userWalletsMap.forEach { (walletId, _) ->
if (metadataMap[walletId] == null) {
saveAvailableWalletMeta(
phoenixGlobal = phoenixGlobal,
walletId = walletId,
name = null,
avatar = WalletAvatars.list.random(),
isHidden = false
)
}
}
_availableWallets.value = result.userWalletsMap
listWalletState.value = ListWalletState.Success
viewModelScope.launch(Dispatchers.Main) {
onDone()
}
logger.i("Navigation UI State is Landing")
}
}
}
}
// fun scheduleAutoLock() {
// // TODO: Run this through an expect...
// viewModelScope.launch {
// autoLockHandler.removeCallbacksAndMessages(null)
// val activeUserPrefs = activeWalletInUI.first()?.userPrefs ?: return@launch
//
// val biometricLockEnabled = activeUserPrefs.getLockBiometricsEnabled.first()
// val customPinLockEnabled = activeUserPrefs.getLockPinEnabled.first()
// val autoLockDelay = activeUserPrefs.getAutoLockDelay.first()
//
// if ((biometricLockEnabled || customPinLockEnabled) && autoLockDelay != Duration.INFINITE) {
// autoLockHandler.postDelayed(autoLockRunnable, autoLockDelay.inWholeMilliseconds)
// }
// }
// }
/** Clears the active wallet and signals the startup screen to load the given [walletId]. */
fun switchToWallet(walletId: WalletId) {
_desiredWalletId.value = walletId
_activeWalletInUI.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. */
fun clearActiveWallet() {
_activeWalletInUI.value = null
}
/** Resets the active wallet and [desiredWalletId]. The UI will redirect to the startup screen with the wallets selector prompt. */
fun resetToSelector() {
_desiredWalletId.value = null
_activeWalletInUI.value = null
startWalletImmediately.value = false
}
override fun onCleared() {
super.onCleared()
logger.i("AppViewModel cleared")
}
fun getPhoenixGlobalPrefs(): GlobalPrefs {
return getGlobalPrefs(
phoenixGlobal
)
}
// wallet initialisation options -- to be saved to user prefs once the wallet has been created and we know its walletId
var isTorEnabled = mutableStateOf(false)
var customElectrumServer = mutableStateOf<ElectrumConfig.Custom?>(null)
/** Monitors the writing of a seed on disk ; used by the restore view and the create view thru [writeSeed]. */
var writingState by mutableStateOf<WritingSeedState>(
WritingSeedState.Init
)
private set
/**
* Attempts to write a seed on disk and updates the view model state. If a seed already
* exists on disk, this method will put the [writingState] in error.
*/
fun writeSeed(
mnemonics: List<String>,
isRestoringWallet: Boolean,
onSeedWritten: (WalletId) -> Unit
) {
platformWriteSeed(
log = logger,
phoenixGlobal = phoenixGlobal,
globalPrefs = getPhoenixGlobalPrefs(),
writingState = writingState,
viewModelScope = viewModelScope,
mnemonics = mnemonics,
onWritingSeedError = { writingSeedStateError ->
writingState = writingSeedStateError
},
onWritingSeedStateWriting = { writing ->
writingState = writing
},
isRestoringWallet = isRestoringWallet,
isTorEnabled = isTorEnabled.value,
customElectrumServer = customElectrumServer.value,
onSeedWritten = { walletId ->
onSeedWritten(walletId)
}
)
}
}

View File

@@ -14,20 +14,23 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import fr.acinq.phoenix.data.ActiveWallet
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlin.time.Instant
class NotaryViewModel(
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository,
val scope: CoroutineScope,
val walletManager: WalletManager,
): ViewModel() {
companion object {
@@ -35,17 +38,17 @@ class NotaryViewModel(
fun factory(
activeWalletStateFlow: StateFlow<ActiveWallet?>,
nostrRepository: NostrRepository,
chatRepository: ChatRepository,
walletManager: WalletManager,
scope: CoroutineScope,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
NotaryViewModel(
activeWalletStateFlow = activeWalletStateFlow,
nostrRepository = nostrRepository,
chatRepository = chatRepository,
scope = scope,
walletManager = walletManager
)
}
}
@@ -54,13 +57,19 @@ class NotaryViewModel(
private val logger = Logger.withTag(TAG)
init {
walletManager.keyManager.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
val keyPair = KeyPair(
privKey = nostrPrivateKey.value.toByteArray()
)
scope.launch {
activeWalletStateFlow.collectLatest { activeWallet ->
logger.d("activeWallet: $activeWallet")
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
observeUnsignedNostrEvents(keyPair)
observeUnsealedGiftWrapPayloads(keyPair)
val keyPair = KeyPair(
privKey = nostrPrivateKey.value.toByteArray()
)
observeUnsignedNostrEvents(keyPair)
observeUnsealedGiftWrapPayloads(keyPair)
}
}
}
}
@@ -72,7 +81,7 @@ class NotaryViewModel(
keyPair
)
scope.launch(Dispatchers.IO) {
logger.i { "observeUnsignedNostrEvents" }
logger.i { "observeUnsignedNostrEvents: ${keyPair.pubKey.toHexKey()}" }
nostrRepository.observeUnsignedNostrEvents(
publicKey = keyPair.pubKey.toHexKey()
).distinctUntilChanged().collect { unsignedNostrEventOrNull ->
@@ -100,7 +109,7 @@ class NotaryViewModel(
unsignedNostrEventId = unsignedNostrEvent.id
),
relayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url },
walletManager = walletManager
activeKeyPair = keyPair
)
}
}
@@ -116,7 +125,7 @@ class NotaryViewModel(
keyPair
)
scope.launch(Dispatchers.IO) {
logger.i { "observeUnsealedGiftWrapPayloads" }
logger.i { "observeUnsealedGiftWrapPayloads: ${keyPair.pubKey.toHexKey()}" }
chatRepository.observeUnsealedGiftWrapPayloads(
publicKey = keyPair.pubKey.toHexKey()
).distinctUntilChanged().collect { giftWrapPayloadOrNull ->

View File

@@ -0,0 +1,92 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.extensions.getShowIntroFlow
import ac.cord.auxiliary.compose.extensions.platformStartupLogic
import ac.cord.auxiliary.compose.extensions.schedulePlatformLogic
import androidx.compose.runtime.mutableStateOf
import androidx.datastore.preferences.core.Preferences
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.PhoenixGlobal
import fr.acinq.phoenix.data.StartBusinessResult
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
sealed class StartupViewState {
data object Init : StartupViewState()
data class StartingBusiness(val walletId: WalletId) : StartupViewState()
data class BusinessActive(val walletId: WalletId): StartupViewState()
sealed class Error: StartupViewState() {
abstract val walletId: WalletId
data class Generic(override val walletId: WalletId, val cause: Throwable?): Error()
}
}
class SovereignWalletStartupViewModel(
val phoenixGlobal: PhoenixGlobal
): ViewModel() {
private val log = Logger.withTag("SovereignWalletStartupViewModel")
val state = mutableStateOf<StartupViewState>(StartupViewState.Init)
fun startupNode(walletId: WalletId, words: List<String>, onStartupSuccess: (PhoenixBusiness) -> Unit) {
if (state.value !is StartupViewState.Init) {
return
}
viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e ->
log.e("error when initialising startup-view: ", throwable = e)
state.value = StartupViewState.Error.Generic(walletId = walletId, cause = e)
}) {
state.value = StartupViewState.StartingBusiness(walletId)
val startResult: StartBusinessResult = platformStartupLogic(
words
)
schedulePlatformLogic(
phoenixGlobal = phoenixGlobal,
)
when (startResult) {
is StartBusinessResult.Success -> {
state.value =StartupViewState.BusinessActive(walletId)
launch(Dispatchers.Main) {
onStartupSuccess(startResult.business)
}
}
is StartBusinessResult.Failure.Generic -> state.value = StartupViewState.Error.Generic(walletId = walletId, cause = startResult.cause)
is StartBusinessResult.Failure.LoadWalletError -> state.value = StartupViewState.Error.Generic(walletId = walletId, cause = null)
}
}
}
fun getShowIntroFlow(): Flow<Boolean> {
return getShowIntroFlow(phoenixGlobal)
}
companion object {
fun factory(
phoenixGlobal: PhoenixGlobal,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
SovereignWalletStartupViewModel(
phoenixGlobal = phoenixGlobal,
)
}
}
}
}

View File

@@ -0,0 +1,293 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.extensions.getGlobalPrefs
import ac.cord.auxiliary.compose.ui.composable.widgets.wallet.WalletAvatars
import ac.cord.auxiliary.compose.ui.view.state.SovereignWalletUIState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import fr.acinq.bitcoin.MnemonicCode
import fr.acinq.lightning.crypto.LocalKeyManager
import fr.acinq.lightning.logging.error
import fr.acinq.lightning.utils.toByteVector
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.PhoenixGlobal
import fr.acinq.phoenix.data.ActiveWallet
import fr.acinq.phoenix.data.DecryptSeedResult
import fr.acinq.phoenix.data.ElectrumConfig
import fr.acinq.phoenix.data.ListWalletState
import fr.acinq.phoenix.data.UserWallet
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.managers.DataStoreManager
import fr.acinq.phoenix.managers.NodeParamsManager
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlin.String
import kotlin.plus
sealed class WritingSeedState {
data object Init : WritingSeedState()
data class Writing(val mnemonics: List<String>) : WritingSeedState()
data class WrittenToDisk(val walletId: WalletId) : WritingSeedState()
sealed class Error : WritingSeedState() {
data class Generic(val cause: Throwable) : Error()
data object CannotLoadSeedMap: Error()
data object SeedAlreadyExists: Error()
}
}
expect fun updateBusinessActiveInUI(walletId: WalletId)
expect fun loadAndDecryptSeed(phoenixGlobal: PhoenixGlobal): DecryptSeedResult
expect fun getAvailableWalletsMeta(phoenixGlobal: PhoenixGlobal): Flow<Map<WalletId, UserWalletMetadata>>
expect suspend fun saveAvailableWalletMeta(phoenixGlobal: PhoenixGlobal, metadata: UserWalletMetadata)
expect suspend fun saveAvailableWalletMeta(
phoenixGlobal: PhoenixGlobal,
walletId: WalletId,
name: String?,
avatar: String,
isHidden: Boolean
)
class SovereignWalletViewModel(
val phoenixGlobal: PhoenixGlobal,
// We might end up only using the machankuraWalletRepository in the future where we send the walletId in each request.
): ViewModel() {
private val log = Logger.withTag("SovereignWalletViewModel")
val listWalletState = mutableStateOf<ListWalletState>(ListWalletState.Init)
private val _availableWallets = MutableStateFlow<Map<WalletId, UserWallet>>(emptyMap())
val availableWallets = _availableWallets.asStateFlow()
private val _desiredWalletId = MutableStateFlow<WalletId?>(null)
val desiredWalletId = _desiredWalletId.asStateFlow()
val startWalletImmediately = MutableStateFlow(true)
private val _activeWalletInUI = MutableStateFlow<ActiveWallet?>(null)
val activeWalletInUI = _activeWalletInUI.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
val exchangeRates = phoenixGlobal.currencyManager.ratesFlow
.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = emptyList())
var walletUIState: SovereignWalletUIState by mutableStateOf(SovereignWalletUIState.Loading)
private set
fun retryLoad() {
walletUIState = SovereignWalletUIState.Loading
}
fun loadSovereignData(walletId: WalletId) {
walletUIState = SovereignWalletUIState.Success(
publicKey = walletId.nodeIdHash,
balanceInSatoshis = 0 // TODO: Get balance...
)
}
init {
listAvailableWallets(onDone = {})
}
fun setActiveWallet(walletId: WalletId, business: PhoenixBusiness) {
val dataStoreManager = DataStoreManager(business)
val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = walletId)
val internalPrefs = dataStoreManager.loadInternalPrefsForWallet( walletId = walletId)
_activeWalletInUI.value = ActiveWallet(id = walletId, business = business, userPrefs = userPrefs, internalPrefs = internalPrefs)
updateBusinessActiveInUI(walletId)
// scheduleAutoLock()
}
fun listAvailableWallets(onDone: () -> Unit) {
viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e ->
// log.error("error when initialising startup-view: ", e)
listWalletState.value = ListWalletState.Error.Generic(e)
}) {
when (val result = loadAndDecryptSeed(phoenixGlobal)) {
is DecryptSeedResult.Failure.SerializationError -> {
log.error {"cannot deserialize seed file: "}
listWalletState.value = ListWalletState.Error.Serialization
}
is DecryptSeedResult.Failure.DecryptionError -> {
log.e("cannot decrypt seed file: ", throwable = result.cause)
listWalletState.value = ListWalletState.Error.DecryptionError.GeneralException(result.cause)
}
is DecryptSeedResult.Failure.KeyStoreFailure -> {
log.e("key store failure: ", throwable = result.cause)
listWalletState.value = ListWalletState.Error.DecryptionError.KeystoreFailure(result.cause)
}
is DecryptSeedResult.Failure.SeedFileUnreadable -> {
log.e("aborting, unreadable seed file")
listWalletState.value = ListWalletState.Error.Generic(null)
}
is DecryptSeedResult.Failure.SeedInvalid -> {
log.e("aborting, seed is invalid")
listWalletState.value = ListWalletState.Error.Generic(null)
}
is DecryptSeedResult.Failure.SeedFileNotFound -> {
listWalletState.value = ListWalletState.Success
_availableWallets.value = emptyMap()
}
is DecryptSeedResult.Success -> {
val metadataMap = getAvailableWalletsMeta(phoenixGlobal).first()
result.userWalletsMap.forEach { (walletId, _) ->
if (metadataMap[walletId] == null) {
saveAvailableWalletMeta(
phoenixGlobal = phoenixGlobal,
walletId = walletId,
name = null,
avatar = WalletAvatars.list.random(),
isHidden = false
)
}
}
_availableWallets.value = result.userWalletsMap
listWalletState.value = ListWalletState.Success
viewModelScope.launch(Dispatchers.Main) {
onDone()
}
}
}
}
}
// fun scheduleAutoLock() {
// // TODO: Run this through an expect...
// viewModelScope.launch {
// autoLockHandler.removeCallbacksAndMessages(null)
// val activeUserPrefs = activeWalletInUI.first()?.userPrefs ?: return@launch
//
// val biometricLockEnabled = activeUserPrefs.getLockBiometricsEnabled.first()
// val customPinLockEnabled = activeUserPrefs.getLockPinEnabled.first()
// val autoLockDelay = activeUserPrefs.getAutoLockDelay.first()
//
// if ((biometricLockEnabled || customPinLockEnabled) && autoLockDelay != Duration.INFINITE) {
// autoLockHandler.postDelayed(autoLockRunnable, autoLockDelay.inWholeMilliseconds)
// }
// }
// }
/** Clears the active wallet and signals the startup screen to load the given [walletId]. */
fun switchToWallet(walletId: WalletId) {
_desiredWalletId.value = walletId
_activeWalletInUI.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. */
fun clearActiveWallet() {
_activeWalletInUI.value = null
}
/** Resets the active wallet and [desiredWalletId]. The UI will redirect to the startup screen with the wallets selector prompt. */
fun resetToSelector() {
_desiredWalletId.value = null
_activeWalletInUI.value = null
startWalletImmediately.value = false
}
override fun onCleared() {
super.onCleared()
log.i("AppViewModel cleared")
}
fun getGlobalPrefs(): GlobalPrefs {
return getGlobalPrefs(
phoenixGlobal
)
}
// wallet initialisation options -- to be saved to user prefs once the wallet has been created and we know its walletId
var isTorEnabled = mutableStateOf(false)
var customElectrumServer = mutableStateOf<ElectrumConfig.Custom?>(null)
/** Monitors the writing of a seed on disk ; used by the restore view and the create view thru [writeSeed]. */
var writingState by mutableStateOf<WritingSeedState>(WritingSeedState.Init)
private set
/**
* Attempts to write a seed on disk and updates the view model state. If a seed already
* exists on disk, this method will put the [writingState] in error.
*/
fun writeSeed(
mnemonics: List<String>,
isRestoringWallet: Boolean,
onSeedWritten: (WalletId) -> Unit
) {
platformWriteSeed(
log = log,
phoenixGlobal = phoenixGlobal,
globalPrefs = getGlobalPrefs(),
writingState = writingState,
viewModelScope = viewModelScope,
mnemonics = mnemonics,
onWritingSeedError = { writingSeedStateError ->
writingState = writingSeedStateError
},
onWritingSeedStateWriting = { writing ->
writingState = writing
},
isRestoringWallet = isRestoringWallet,
isTorEnabled = isTorEnabled.value,
customElectrumServer = customElectrumServer.value,
onSeedWritten = { walletId ->
onSeedWritten(walletId)
}
)
}
companion object {
private const val TAG = "SovereignWalletViewModel"
fun factory(
phoenixGlobal: PhoenixGlobal,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
SovereignWalletViewModel(
phoenixGlobal = phoenixGlobal,
)
}
}
}
}
expect fun platformWriteSeed(
log: Logger,
phoenixGlobal: PhoenixGlobal,
globalPrefs: GlobalPrefs,
writingState: WritingSeedState,
viewModelScope: CoroutineScope,
mnemonics: List<String>,
onWritingSeedError: (WritingSeedState.Error) -> Unit,
onWritingSeedStateWriting: (WritingSeedState.Writing) -> Unit,
isRestoringWallet: Boolean,
isTorEnabled: Boolean,
customElectrumServer: ElectrumConfig.Custom?,
onSeedWritten: (WalletId) -> Unit
)

View File

@@ -12,6 +12,7 @@ import ac.cord.auxiliary.compose.repository.CachingImportRepository
import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.repository.RelayRepository
import androidx.compose.runtime.key
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
@@ -19,17 +20,22 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.negentropy.Negentropy
import com.vitorpamplona.negentropy.storage.StorageVector
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
import fr.acinq.phoenix.data.ActiveWallet
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
@@ -38,18 +44,18 @@ import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration.Companion.milliseconds
class SynchronizationViewModel(
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository,
val relayRepository: RelayRepository,
val walletManager: WalletManager,
val scope: CoroutineScope,
): ViewModel() {
val relaysSocketManager = RelaysSocketManager(
activeWalletStateFlow = activeWalletStateFlow,
nostrSocketClientFactory = NostrSocketClientFactory,
cachingImportRepository = CachingImportRepository.NO_OP_CACHING_IMPORT_REPOSITORY,
relayRepository = relayRepository,
walletManager = walletManager
)
companion object {
@@ -58,18 +64,18 @@ class SynchronizationViewModel(
private val mutex = Mutex()
fun factory(
activeWalletStateFlow: StateFlow<ActiveWallet?>,
nostrRepository: NostrRepository,
chatRepository: ChatRepository,
relayRepository: RelayRepository,
walletManager: WalletManager,
scope: CoroutineScope,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
SynchronizationViewModel(
activeWalletStateFlow = activeWalletStateFlow,
nostrRepository = nostrRepository,
chatRepository = chatRepository,
relayRepository = relayRepository,
walletManager = walletManager,
scope = scope
)
}
@@ -79,12 +85,25 @@ class SynchronizationViewModel(
private val logger = Logger.withTag(TAG)
init {
observePendingBroadcastNostrEventRequests()
observePendingSyncNostrEventRequests()
observePendingNegentropySynchronizeRequests()
scope.launch {
activeWalletStateFlow.collectLatest { activeWallet ->
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
val keyPair = KeyPair(
privKey = nostrPrivateKey.value.toByteArray()
)
observePendingBroadcastNostrEventRequests(keyPair)
observePendingSyncNostrEventRequests(keyPair)
observePendingNegentropySynchronizeRequests(keyPair)
}
}
}
}
private fun observePendingSyncNostrEventRequests() {
private fun observePendingSyncNostrEventRequests(
keyPair: KeyPair
) {
logger.i { "observePendingSyncNostrEventRequests" }
scope.launch(Dispatchers.IO) {
@@ -125,7 +144,7 @@ class SynchronizationViewModel(
nostrEvent = it,
synchronizeNostrEventRequest,
synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL),
walletManager = walletManager
activeKeyPair = keyPair
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
@@ -139,7 +158,7 @@ class SynchronizationViewModel(
nostrEvent = nostrEvent,
synchronizeNostrEventRequest,
synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL),
walletManager = walletManager
activeKeyPair = keyPair
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
@@ -170,7 +189,9 @@ class SynchronizationViewModel(
}
}
private fun observePendingNegentropySynchronizeRequests() {
private fun observePendingNegentropySynchronizeRequests(
keyPair: KeyPair
) {
logger.i { "observePendingNegentropySynchronizeRequests" }
scope.launch(Dispatchers.IO) {
@@ -238,7 +259,7 @@ class SynchronizationViewModel(
nostrEvent = it,
negentropySynchronizeRequest,
synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL),
walletManager = walletManager
activeKeyPair = keyPair
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
@@ -252,7 +273,7 @@ class SynchronizationViewModel(
nostrEvent = nostrEvent,
negentropySynchronizeRequest,
synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL),
walletManager = walletManager
activeKeyPair = keyPair
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
@@ -356,7 +377,9 @@ class SynchronizationViewModel(
}
@OptIn(FlowPreview::class)
private fun observePendingBroadcastNostrEventRequests() {
private fun observePendingBroadcastNostrEventRequests(
keyPair: KeyPair
) {
logger.i { "observePendingBroadcastNostrEventRequests" }
scope.launch(Dispatchers.IO) {

View File

@@ -8,8 +8,11 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
abstract class NavigationUIState {
data object Error: NavigationUIState()
data object Loading: NavigationUIState()
data class Loading(
val text: String? = null
): NavigationUIState()
data object StartupPhoenix: NavigationUIState()
data object Landing: NavigationUIState()
data class UnsignedProfile(

View File

@@ -0,0 +1,17 @@
package ac.cord.auxiliary.compose.ui.view.state
import fr.acinq.phoenix.PhoenixBusiness
sealed interface SovereignWalletUIState {
data object Uninitialized: SovereignWalletUIState
data class Success(
val publicKey: String,
val balanceInSatoshis: Long,
): SovereignWalletUIState
data object Error: SovereignWalletUIState
data object Loading: SovereignWalletUIState
}

View File

@@ -16,6 +16,9 @@
package fr.acinq.phoenix.managers
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.*
import fr.acinq.lightning.crypto.Bip84OnChainKeys
import fr.acinq.lightning.crypto.LocalKeyManager
@@ -99,8 +102,10 @@ fun LocalKeyManager.nostrPrivateKey(): PrivateKey {
return derivePrivateKey(path).privateKey
}
fun LocalKeyManager.nostrPublicKey(): PublicKey {
return nostrPrivateKey().publicKey()
fun LocalKeyManager.nostrPublicKey(): HexKey {
return KeyPair(
privKey = nostrPrivateKey().value.toByteArray()
).pubKey.toHexKey()
}
fun LocalKeyManager.cloudKeyHash(): String {