Load the encrypted seed
This commit is contained in:
@@ -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 ->
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {}
|
||||
// }
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package ac.cord.auxiliary.compose.ui.composable.navigation.routes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object AuthenticatedProfileRoute {
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,6 @@
|
||||
package ac.cord.auxiliary.compose.ui.composable.navigation.routes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object SovereignWalletStartupRoute
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user