diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index cf834428..3946a0f2 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -35,7 +35,7 @@ kotlin { } } - jvm() +// jvm() sourceSets { androidMain.dependencies { @@ -160,7 +160,7 @@ dependencies { add("kspIosSimulatorArm64", libs.androidx.room3.compiler) // add("kspIosX64", libs.androidx.room.compiler) add("kspIosArm64", libs.androidx.room3.compiler) - add("kspJvm", libs.androidx.room3.compiler) +// add("kspJvm", libs.androidx.room3.compiler) // Add any other platform target you use in your project, for example kspDesktop } diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt index 2947484c..4f40b511 100644 --- a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.activity.enableEdgeToEdge import androidx.navigation.compose.rememberNavController import com.machankura.compose.ui.composable.widgets.nfc.NfcState import com.machankura.compose.ui.composable.widgets.nfc.NfcStateRepository +import fr.acinq.phoenix.PhoenixGlobal import fr.acinq.phoenix.android.services.HceService class MainActivity : ComponentActivity() { @@ -31,7 +32,13 @@ class MainActivity : ComponentActivity() { platformContext = PlatformContext( applicationContext = applicationContext ) + ), + phoenixGlobal = PhoenixGlobal( + ctx = fr.acinq.phoenix.utils.PlatformContext( + applicationContext = applicationContext + ) ) + ) } } diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.android.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.android.kt new file mode 100644 index 00000000..3f5e2d7f --- /dev/null +++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.android.kt @@ -0,0 +1,33 @@ +package ac.cord.auxiliary.compose.extensions + +import ac.cord.auxiliary.android.InomboloApplication +import fr.acinq.phoenix.PhoenixGlobal +import fr.acinq.phoenix.android.BusinessManager +import fr.acinq.phoenix.android.services.ChannelsWatcher +import fr.acinq.phoenix.android.services.ContactsPhotoCleaner +import fr.acinq.phoenix.data.StartBusinessResult +import fr.acinq.phoenix.utils.preferences.GlobalPrefs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + + +actual suspend fun platformStartupLogic(words: List): StartBusinessResult { + return withContext(Dispatchers.Default) { + BusinessManager.startNewBusiness(words, isHeadless = false) + } +} + +actual fun schedulePlatformLogic(phoenixGlobal: PhoenixGlobal) { + ChannelsWatcher.schedule(phoenixGlobal.ctx.applicationContext) + ContactsPhotoCleaner.schedule(phoenixGlobal.ctx.applicationContext) +} +actual fun getShowIntroFlow(phoenixGlobal: PhoenixGlobal): Flow { + val machankuraApplication = phoenixGlobal.ctx.applicationContext as InomboloApplication + return machankuraApplication.globalPrefs.getShowIntro +} + +actual fun getGlobalPrefs(phoenixGlobal: PhoenixGlobal): GlobalPrefs { + val machankuraApplication = phoenixGlobal.ctx.applicationContext as InomboloApplication + return machankuraApplication.globalPrefs +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.android.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.android.kt new file mode 100644 index 00000000..91d43e1b --- /dev/null +++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.android.kt @@ -0,0 +1,140 @@ +package ac.cord.auxiliary.compose.ui.view.model + +import ac.cord.auxiliary.android.InomboloApplication +import ac.cord.auxiliary.compose.AppVersion +import co.touchlab.kermit.Logger +import fr.acinq.bitcoin.MnemonicCode +import fr.acinq.lightning.crypto.LocalKeyManager +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.phoenix.PhoenixGlobal +import fr.acinq.phoenix.android.BusinessManager +import fr.acinq.phoenix.data.DecryptSeedResult +import fr.acinq.phoenix.data.ElectrumConfig +import fr.acinq.phoenix.data.WalletId +import fr.acinq.phoenix.managers.DataStoreManager +import fr.acinq.phoenix.managers.NodeParamsManager +import fr.acinq.phoenix.managers.SeedManager +import fr.acinq.phoenix.security.EncryptedSeed +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.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +actual fun updateBusinessActiveInUI(walletId: WalletId) { + BusinessManager.updateBusinessActiveInUI(walletId) +} + +actual fun loadAndDecryptSeed(phoenixGlobal: PhoenixGlobal): DecryptSeedResult { + return SeedManager.loadAndDecrypt( + phoenixGlobal + ) +} + +actual fun getAvailableWalletsMeta(phoenixGlobal: PhoenixGlobal): Flow> { + val machankuraApplication = phoenixGlobal.ctx.applicationContext as InomboloApplication + return machankuraApplication.globalPrefs.getAvailableWalletsMeta +} + +actual suspend fun saveAvailableWalletMeta(phoenixGlobal: PhoenixGlobal, metadata: UserWalletMetadata) { + val machankuraApplication = phoenixGlobal.ctx.applicationContext as InomboloApplication + machankuraApplication.globalPrefs.saveAvailableWalletMeta(metadata) +} + +actual suspend fun saveAvailableWalletMeta( + phoenixGlobal: PhoenixGlobal, + walletId: WalletId, + name: String?, + avatar: String, + isHidden: Boolean +) { + val machankuraApplication = phoenixGlobal.ctx.applicationContext as InomboloApplication + machankuraApplication.globalPrefs.saveAvailableWalletMeta( + walletId = walletId, + name = name, + avatar = avatar, + isHidden = isHidden + ) +} + + +actual fun platformWriteSeed( + log: Logger, + phoenixGlobal: PhoenixGlobal, + globalPrefs: GlobalPrefs, + writingState: WritingSeedState, + viewModelScope: CoroutineScope, + mnemonics: List, + onWritingSeedError: (WritingSeedState.Error) -> Unit, + onWritingSeedStateWriting: (WritingSeedState.Writing) -> Unit, + isRestoringWallet: Boolean, + isTorEnabled: Boolean, + customElectrumServer: ElectrumConfig.Custom?, + onSeedWritten: (WalletId) -> Unit +) { + if (writingState !is WritingSeedState.Init) return + viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> + log.e("failed to write mnemonics to disk: ${e.message}") + onWritingSeedError.invoke( + WritingSeedState.Error.Generic(e) + ) + }) { + log.d("writing mnemonics to disk...") + + onWritingSeedStateWriting.invoke( + WritingSeedState.Writing(mnemonics) + ) + val existingSeeds = SeedManager.loadAndDecryptOrNull(phoenixGlobal)?.map { + it.key to it.value.words + }?.toMap() + + val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector() + val keyManager = LocalKeyManager(seed, NodeParamsManager.chain, NodeParamsManager.remoteSwapInXpub) + val newWalletId = WalletId(keyManager.nodeKeys.nodeKey.publicKey) + + when { + existingSeeds == null -> { + log.e("could not load the existing seed map, aborting...") + onWritingSeedError.invoke( + WritingSeedState.Error.CannotLoadSeedMap + ) + return@launch + } + existingSeeds.containsKey(newWalletId) -> { + log.i("attempting to import a seed that already exists, aborting...") + onWritingSeedError.invoke( + WritingSeedState.Error.SeedAlreadyExists + ) + return@launch + } + else -> { + val newSeedMap = existingSeeds + (newWalletId to mnemonics) + val encrypted = EncryptedSeed.V2.encrypt(newSeedMap) + SeedManager.writeSeedToDisk(phoenixGlobal, encrypted, overwrite = true) + onSeedWritten.invoke(newWalletId) + if (isRestoringWallet) { + log.i("successfully restored wallet=$newWalletId") + } else { + log.i("successfully created wallet=$newWalletId") + } + } + } + + globalPrefs.saveLastUsedAppCode(AppVersion.versionCode) + val dataStoreManager = DataStoreManager( + phoenixGlobal.ctx, + chain = NodeParamsManager.chain, + ) + val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = newWalletId) + userPrefs.saveIsTorEnabled(isTorEnabled) + userPrefs.saveElectrumServer(customElectrumServer) + + viewModelScope.launch(Dispatchers.Main) { + delay(1000) + onSeedWritten(newWalletId) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/AuxApp.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/AuxApp.kt index 516d4edb..83d1c416 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/AuxApp.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/AuxApp.kt @@ -7,11 +7,13 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.navigation.NavHostController +import fr.acinq.phoenix.PhoenixGlobal @Composable fun AuxApp( navController: NavHostController, - auxGlobal: AuxGlobal + auxGlobal: AuxGlobal, + phoenixGlobal: PhoenixGlobal ) { AuxTheme { Surface( @@ -19,6 +21,7 @@ fun AuxApp( ) { AuxNavHost( auxGlobal = auxGlobal, + phoenixGlobal = phoenixGlobal, navController = navController ) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt index ed2e8bf6..242ddae3 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt @@ -20,12 +20,10 @@ import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter import ac.cord.auxiliary.compose.exceptions.GiftWrapImpersonationException import ac.cord.auxiliary.compose.exceptions.GiftWrapSealDecryptionException import ac.cord.auxiliary.compose.exceptions.GiftWrapUnsealException -import ac.cord.auxiliary.compose.managers.SeedManager -import ac.cord.auxiliary.compose.managers.toHex +import ac.cord.auxiliary.compose.extensions.toHex import androidx.room3.Dao import androidx.room3.Transaction import co.touchlab.kermit.Logger -import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -34,13 +32,14 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import kotlin.math.log +import fr.acinq.phoenix.managers.WalletManager +import fr.acinq.phoenix.managers.nostrPrivateKey import kotlin.time.Clock import kotlin.time.Instant @Dao abstract class NostrDao( - private val database: AuxDatabase + private val database: AuxDatabase, ) { val logger = Logger.withTag("NostrDao") @@ -59,7 +58,8 @@ abstract class NostrDao( open suspend fun publishNostrEvent( unsignedNostrEvent: UnsignedNostrEvent, nostrEvent: NostrEvent, - relayURLs: List + relayURLs: List, + walletManager: WalletManager ) { logger.i("Publish Nostr Event: $nostrEvent ($relayURLs)") // Update unsignedEvent with signedTime time... @@ -75,7 +75,8 @@ abstract class NostrDao( nostrEvent = nostrEvent, relayURL = relayURLs.first(), synchronizationRelayURLs = relayURLs, - level = 0 + level = 0, + walletManager = walletManager ) relayURLs.forEach { relayURL -> @@ -94,7 +95,8 @@ abstract class NostrDao( nostrEvent: NostrEvent, relayURL: String, synchronizationRelayURLs: List, - level: Int + level: Int, + walletManager: WalletManager ) { val storedNostrEvent = database.nostrEventDao().getNostrEventById(nostrEvent.id) @@ -132,7 +134,8 @@ abstract class NostrDao( nostrEvent = nostrEvent, relayURL = relayURL, synchronizationRelayURLs = synchronizationRelayURLs, - level = level + level = level, + walletManager = walletManager ) } @@ -140,7 +143,8 @@ abstract class NostrDao( nostrEvent: NostrEvent, relayURL: String, synchronizationRelayURLs: List, - level: Int + level: Int, + walletManager: WalletManager ) { val profilePublicKeysToSync = mutableMapOf>() val eventIdsToSync = mutableMapOf>() @@ -541,217 +545,251 @@ abstract class NostrDao( // } // } - SeedManager.activeKeyPair().let { activeKeyPair -> - giftWrapMessage.decryptGiftWrapSeal( - activeKeyPair - ).let { giftWrapSeal -> - if (giftWrapSeal == null) { - throw GiftWrapUnsealException("Failed to unseal ${nostrEvent.id} from the giftWrap") - } else { - database.giftWrapSealDao().upsert( - giftWrapSeal - ) + 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() + ) - 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}" - ) - 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 - ) + giftWrapMessage.decryptGiftWrapSeal( + activeKeyPair + ).let { giftWrapSeal -> + if (giftWrapSeal == null) { + throw GiftWrapUnsealException("Failed to unseal ${nostrEvent.id} from the giftWrap") + } else { + database.giftWrapSealDao().upsert( + giftWrapSeal + ) - val giftWrapPayload = if (result != -1L) { - decryptedGiftWrapPayload.copy( - id = result - ) + giftWrapSeal.decryptGiftWrapPayload( + activeKeyPair.privKey!! + ).let { decryptedGiftWrapPayload -> + if (decryptedGiftWrapPayload == null) { + throw GiftWrapSealDecryptionException("Failed to decrypt sealed ${giftWrapSeal.id} payload") } else { - decryptedGiftWrapPayload - } - - val chatRoomId = giftWrapPayload.aggregatedParticipantsPublicKey() - logger.d("ChatRoomId: $chatRoomId") - - if (chatRoomId != null) { - // Find or create chatRoom - val localChatRoom = database.chatRoomDao().findChatRoomById( - chatRoomId + 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 ) - if (localChatRoom == null) { - val userPublicKey = SeedManager.activeKeyPair().pubKey.toHex() - database.chatRoomDao().upsert( - ChatRoom( - id = chatRoomId, - userPublicKey = userPublicKey, - subject = decryptedGiftWrapPayload.parseSubject(), - createdAt = decryptedGiftWrapPayload.createdAt, - initialGiftWrapPayloadId = giftWrapPayload.id - ) + 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 - ) - } - - // 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 - ) - } - } - } - - 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 - ) - ) - } + decryptedGiftWrapPayload } - val chatMessageId = database.chatMessageDao().upsert( - ChatMessage( - giftWrapPayloadId = giftWrapPayload.id, - senderPublicKey = giftWrapPayload.publicKey, - isUserMessage = SeedManager.activePublicKey().toHex() == giftWrapPayload.publicKey, - chatRoomId = chatRoomId, - createdAt = giftWrapPayload.createdAt, - content = giftWrapPayload.content, - ) - ) + val chatRoomId = + giftWrapPayload.aggregatedParticipantsPublicKey() + logger.d("ChatRoomId: $chatRoomId") - val broadcastNostrEventReceiptId = database.broadcastNostrEventReceiptDao().upsert( - BroadcastNostrEventReceipt( - nostrEventId = nostrEvent.id, - isAccepted = true, - isSync = true, - relayURL = relayURL + if (chatRoomId != null) { + // Find or create chatRoom + val localChatRoom = database.chatRoomDao().findChatRoomById( + chatRoomId ) - ) - database.chatMessageBroadcastNostrEventReceiptRelationDao().upsert( - ChatMessageBroadcastNostrEventReceiptRelation( - broadcastNostrEventReceiptId = broadcastNostrEventReceiptId, - chatMessageId = chatMessageId + 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 + ) + ) + + // 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 + ) + } + + // 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 + ) + } + } + } + + 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 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, + ) ) - ) - } else { - logger.w("Failed to setup chatRoom for message") + + 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") + } } } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt index 6354557a..4229c279 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt @@ -12,10 +12,10 @@ import ac.cord.auxiliary.compose.database.model.GiftWrapSeal import ac.cord.auxiliary.compose.database.model.NostrEvent import ac.cord.auxiliary.compose.database.model.Participant import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom -import ac.cord.auxiliary.compose.managers.SeedManager import androidx.room3.Dao import androidx.room3.Transaction import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @@ -32,13 +32,12 @@ abstract class NostrNip17Dao( val logger = Logger.withTag(TAG) @Transaction - open suspend fun getOrCreateChatRoom(publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom? { + open suspend fun getOrCreateChatRoom(activeUserPublicKey: HexKey, publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom? { logger.d("getOrCreateChatRoom: $publicKey") - val activePublicKey = SeedManager.activePublicKey().toHexKey() val hexKeys = setOf( - activePublicKey, + activeUserPublicKey, publicKey ) @@ -55,7 +54,7 @@ abstract class NostrNip17Dao( // Create new chatRoom val chatRoom = ChatRoom( id = chatRoomId, - userPublicKey = activePublicKey, + userPublicKey = activeUserPublicKey, subject = defaultSubject, ) database.chatRoomDao().upsert(chatRoom) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/Profile.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/Profile.kt index 413f6f19..6d1b8358 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/Profile.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/Profile.kt @@ -131,6 +131,7 @@ data class Profile( @Composable fun RenderAsListItem( + activeUserPublicKey: String, onNavigateToEvent: (Route) -> Unit ) { Card( @@ -141,6 +142,7 @@ data class Profile( onClick = { onNavigateToEvent.invoke( NostrEventDetailRoute( + activeUserPublicKey = activeUserPublicKey, nostrEventId ) ) @@ -191,7 +193,9 @@ data class Profile( @Preview @Composable -fun ProfileRenderAsListItemPreview() { +fun ProfileRenderAsListItemPreview( + activeUserPublicKey: String +) { val profile = Profile( publicKey = "pubKey", displayName = "John Doe", @@ -204,6 +208,7 @@ fun ProfileRenderAsListItemPreview() { modifier = Modifier.padding(20.dp) ) { profile.RenderAsListItem( + activeUserPublicKey = "", onNavigateToEvent = {} ) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt index f6472e74..d425649d 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt @@ -7,18 +7,15 @@ import ac.cord.auxiliary.compose.database.model.ChatRoom import ac.cord.auxiliary.compose.database.model.GiftWrapPayload import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatMessage import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom -import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.repository.ChatRepository import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind -import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent @@ -53,12 +50,14 @@ class DatabaseChatRepository( override suspend fun getOrCreateChatRoom( + activeUserPublicKey: HexKey, publicKey: String, relayHint: String?, defaultSubject: String? ): LocalChatRoom? = try { return database.nostrNip17Dao().getOrCreateChatRoom( - publicKey, + activeUserPublicKey = activeUserPublicKey, + publicKey = publicKey, relayHint = relayHint, defaultSubject = defaultSubject ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt index d7bce4d7..4654b2e7 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt @@ -16,7 +16,6 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount -import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing @@ -57,6 +56,7 @@ 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 @@ -389,12 +389,14 @@ class DatabaseNostrRepository( override suspend fun publishNostrEvent( unsignedNostrEvent: UnsignedNostrEvent, nostrEvent: NostrEvent, - relayURLs: List + relayURLs: List, + walletManager: WalletManager ) { database.nostrDao().publishNostrEvent( unsignedNostrEvent, nostrEvent, - relayURLs + relayURLs, + walletManager = walletManager ) } @@ -459,7 +461,8 @@ class DatabaseNostrRepository( override suspend fun saveNostrEvent( nostrEvent: NostrEvent, synchronizeNostrEventRequest: SynchronizeNostrEventRequest, - synchronizationRelayURLs: List + synchronizationRelayURLs: List, + walletManager: WalletManager, ) { storeNostrEventMutex.withLock { logger.d("saveNostrEvent: $nostrEvent") @@ -470,7 +473,8 @@ class DatabaseNostrRepository( ), synchronizationRelayURLs = synchronizationRelayURLs, relayURL = synchronizeNostrEventRequest.relayURL, - level = synchronizeNostrEventRequest.level + level = synchronizeNostrEventRequest.level, + walletManager = walletManager ) database.synchronizeNostrEventRequestDao().upsert( @@ -489,7 +493,8 @@ class DatabaseNostrRepository( override suspend fun saveNostrEvent( nostrEvent: NostrEvent, negentropySynchronizeRequest: NegentropySynchronizeRequest, - synchronizationRelayURLs: List + synchronizationRelayURLs: List, + walletManager: WalletManager ) { logger.d("saveNostrEvent: $nostrEvent") @@ -498,7 +503,8 @@ class DatabaseNostrRepository( nostrEvent, relayURL = negentropySynchronizeRequest.relayURL, synchronizationRelayURLs = synchronizationRelayURLs, - level = negentropySynchronizeRequest.level + level = negentropySynchronizeRequest.level, + walletManager = walletManager ) database.negentropySynchronizeRequestDao().upsert( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/extensions/Credentials.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/extensions/Credentials.kt new file mode 100644 index 00000000..2bbf2335 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/extensions/Credentials.kt @@ -0,0 +1,71 @@ +package ac.cord.auxiliary.compose.extensions + +import ac.cord.auxiliary.compose.exceptions.InvalidNostrPrivateKeyException +import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 +import fr.acinq.bitcoin.PrivateKey +import fr.acinq.secp256k1.Hex +import io.ktor.utils.io.core.toByteArray + + +fun String.assureValidNsec() = if (startsWith("nsec")) this else this.hexToNsecHrp() +fun String.assureValidNpub() = if (startsWith("npub")) this else this.hexToNpubHrp() +fun String.assureValidPubKeyHex() = if (startsWith("npub")) this.bech32ToHexOrThrow() else this + + +fun String.hexToNoteHrp() = + Bech32.encodeBytes( + hrp = "note", + data = Hex.decode(this), + encoding = Bech32.Encoding.Bech32, + ) + +fun String.hexToNpubHrp() = + Bech32.encodeBytes( + hrp = "npub", + data = Hex.decode(this), + encoding = Bech32.Encoding.Bech32, + ) + +fun String.hexToNsecHrp() = + Bech32.encodeBytes( + hrp = "nsec", + data = Hex.decode(this), + encoding = Bech32.Encoding.Bech32, + ) + +fun String.urlToLnUrlHrp() = + Bech32.encodeBytes( + hrp = "lnurl", + data = this.toByteArray(), + encoding = Bech32.Encoding.Bech32, + ) + + +fun String.bech32ToHexOrThrow() = Bech32.decodeBytes(bech32 = this).second.toHex() + +fun String.bech32ToHexOrNull() = runCatching { this.bech32ToHexOrThrow() }.getOrNull() + +fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32) + +@OptIn(ExperimentalStdlibApi::class) +fun ByteArray.toHex() = Hex.encode(this) + +@Throws(IllegalArgumentException::class) +fun String.bechToBytesOrThrow(hrp: String? = null): ByteArray { + val decodedForm = Bech32.decodeBytes(this) + hrp?.also { require(it == decodedForm.first) } + return decodedForm.second +} + +fun String.extractKeyPairFromPrivateKeyOrThrow(): Pair { + return try { + val nsec = this.assureValidNsec() + val decoded = Bech32.decodeBytes(nsec) + val pubkey = PrivateKey(decoded.second).publicKey().value.toByteArray() + nsec to pubkey.toNpub() + } catch (error: IllegalArgumentException) { + Logger.withTag("String.extractKeyPairFromPrivateKeyOrThrow").w(error) { error.message ?: "" } + throw InvalidNostrPrivateKeyException() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.kt new file mode 100644 index 00000000..705288c3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.kt @@ -0,0 +1,15 @@ +package ac.cord.auxiliary.compose.extensions + +import fr.acinq.phoenix.PhoenixGlobal +import fr.acinq.phoenix.data.StartBusinessResult +import fr.acinq.phoenix.utils.preferences.GlobalPrefs +import kotlinx.coroutines.flow.Flow + + +expect suspend fun platformStartupLogic(words: List): StartBusinessResult + +expect fun schedulePlatformLogic(phoenixGlobal: PhoenixGlobal) + +expect fun getShowIntroFlow(phoenixGlobal: PhoenixGlobal): Flow + +expect fun getGlobalPrefs(phoenixGlobal: PhoenixGlobal): GlobalPrefs diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/DatabaseManager.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/AuxDatabaseManager.kt similarity index 96% rename from composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/DatabaseManager.kt rename to composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/AuxDatabaseManager.kt index 75ff6492..a70d8747 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/DatabaseManager.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/AuxDatabaseManager.kt @@ -4,7 +4,7 @@ import ac.cord.auxiliary.compose.AuxGlobal import ac.cord.auxiliary.compose.database.builder.PlatformDatabaseBuilder import ac.cord.auxiliary.compose.database.builder.getRoomDatabase -class DatabaseManager( +class AuxDatabaseManager( auxGlobal: AuxGlobal ) { val auxDatabase by lazy { diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/CredentialsManager.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/CredentialsManager.kt deleted file mode 100644 index 089f220b..00000000 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/CredentialsManager.kt +++ /dev/null @@ -1,171 +0,0 @@ -package ac.cord.auxiliary.compose.managers - -import ac.cord.auxiliary.compose.exceptions.InvalidNostrPrivateKeyException -import androidx.datastore.core.DataStore -import co.touchlab.kermit.Logger -import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 -import com.vitorpamplona.quartz.nip19Bech32.toNsec -import fr.acinq.bitcoin.PrivateKey -import fr.acinq.lightning.Lightning -import fr.acinq.secp256k1.Hex -import io.ktor.utils.io.core.toByteArray -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.Serializable - -/*** - * TODO: Make this a Singleton - */ -class CredentialsManager( - private val persistence: DataStore>, -) { - - private val scope = CoroutineScope(Dispatchers.IO) - - val credentials = persistence.data - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = runBlocking { persistence.data.first() }, - ) - - private suspend fun addCredential(credential: Credential) = persistence.updateData { it + credential } - - suspend fun clearCredentials() = persistence.updateData { emptySet() } - - fun isExternalSignerCredential(npub: String) = - checkCredentialType(npub = npub, credentialType = CredentialType.ExternalSigner) - - fun isNpubCredential(npub: String) = checkCredentialType(npub = npub, credentialType = CredentialType.PublicKey) - - suspend fun getOrCreateInternalSignerCredentials() = - credentials.value.find { it.type == CredentialType.InternalSigner } - ?: Lightning.randomKey().let { privateKey -> - Credential( - nsec = privateKey.value.toByteArray().toNsec(), - npub = privateKey.publicKey().value.toByteArray().toNpub(), - type = CredentialType.InternalSigner - ) - } - - private fun checkCredentialType(npub: String, credentialType: CredentialType) = - credentials.value.find { it.npub == npub }?.type == credentialType - - suspend fun saveNsec(nostrKey: String): String { - val (nsec, pubkey) = nostrKey.extractKeyPairFromPrivateKeyOrThrow() - addCredential(Credential(nsec = nsec, npub = pubkey, type = CredentialType.PrivateKey)) - return pubkey.bech32ToHexOrThrow() - } - - suspend fun saveExternalSignerNpub(npub: String): String { - val (hexKey, bech32Key) = if (npub.startsWith("npub")) { - npub.bech32ToHexOrThrow() to npub - } else { - npub to npub.hexToNpubHrp() - } - - addCredential(Credential(nsec = null, npub = bech32Key, type = CredentialType.ExternalSigner)) - return hexKey - } - - suspend fun saveNpub(npub: String): String { - addCredential(Credential(nsec = null, npub = npub, type = CredentialType.PublicKey)) - return npub.bech32ToHexOrThrow() - } - - suspend fun removeCredentialByNsec(nsec: String) = - persistence.updateData { - it.filterNot { cred -> cred.nsec == nsec }.toSet() - } - - suspend fun removeCredentialByNpub(npub: String) = - persistence.updateData { - it.filterNot { cred -> cred.npub == npub }.toSet() - } - - fun findOrThrow(npub: String): Credential = - credentials.value.find { it.npub == npub } - ?: throw IllegalArgumentException("Credential not found for $npub.") - - @Serializable - data class Credential( - val nsec: String?, - val npub: String, - val type: CredentialType = CredentialType.PrivateKey, - ) - - enum class CredentialType { - InternalSigner, - ExternalSigner, - PrivateKey, - PublicKey, - } - -} - -fun String.assureValidNsec() = if (startsWith("nsec")) this else this.hexToNsecHrp() -fun String.assureValidNpub() = if (startsWith("npub")) this else this.hexToNpubHrp() -fun String.assureValidPubKeyHex() = if (startsWith("npub")) this.bech32ToHexOrThrow() else this - - -fun String.hexToNoteHrp() = - Bech32.encodeBytes( - hrp = "note", - data = Hex.decode(this), - encoding = Bech32.Encoding.Bech32, - ) - -fun String.hexToNpubHrp() = - Bech32.encodeBytes( - hrp = "npub", - data = Hex.decode(this), - encoding = Bech32.Encoding.Bech32, - ) - -fun String.hexToNsecHrp() = - Bech32.encodeBytes( - hrp = "nsec", - data = Hex.decode(this), - encoding = Bech32.Encoding.Bech32, - ) - -fun String.urlToLnUrlHrp() = - Bech32.encodeBytes( - hrp = "lnurl", - data = this.toByteArray(), - encoding = Bech32.Encoding.Bech32, - ) - - -fun String.bech32ToHexOrThrow() = Bech32.decodeBytes(bech32 = this).second.toHex() - -fun String.bech32ToHexOrNull() = runCatching { this.bech32ToHexOrThrow() }.getOrNull() - -fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32) - -@OptIn(ExperimentalStdlibApi::class) -fun ByteArray.toHex() = Hex.encode(this) - -@Throws(IllegalArgumentException::class) -fun String.bechToBytesOrThrow(hrp: String? = null): ByteArray { - val decodedForm = Bech32.decodeBytes(this) - hrp?.also { require(it == decodedForm.first) } - return decodedForm.second -} - -fun String.extractKeyPairFromPrivateKeyOrThrow(): Pair { - return try { - val nsec = this.assureValidNsec() - val decoded = Bech32.decodeBytes(nsec) - val pubkey = PrivateKey(decoded.second).publicKey().value.toByteArray() - nsec to pubkey.toNpub() - } catch (error: IllegalArgumentException) { - Logger.withTag("String.extractKeyPairFromPrivateKeyOrThrow").w(error) { error.message ?: "" } - throw InvalidNostrPrivateKeyException() - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/SeedManager.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/SeedManager.kt deleted file mode 100644 index 88c0f658..00000000 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/managers/SeedManager.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ac.cord.auxiliary.compose.managers - -import co.touchlab.kermit.Logger -import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip19Bech32.toNpub -import kotlin.random.Random - -object SeedManager { - val logger = Logger.withTag("SeedManager") - - private val tempDevelopmentKeyPair = KeyPair( - privKey = Random(21_012_256L).nextBytes(32) - ) - - init { - logger.d("npub: ${tempDevelopmentKeyPair.pubKey.toNpub()}") - } - - fun activeKeyPair(): KeyPair { - // TODO: Actually set and get a pair... - - return tempDevelopmentKeyPair - } - - fun activePublicKey(): ByteArray { - return activeKeyPair().pubKey - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/network/relays/RelaysSocketManager.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/network/relays/RelaysSocketManager.kt index 25179fa4..abc49260 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/network/relays/RelaysSocketManager.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/network/relays/RelaysSocketManager.kt @@ -16,14 +16,17 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import ac.cord.auxiliary.compose.exceptions.NostrPublishException -import ac.cord.auxiliary.compose.managers.SeedManager -import ac.cord.auxiliary.compose.managers.toHex import ac.cord.auxiliary.compose.network.sockets.NostrIncomingMessage +import com.vitorpamplona.quartz.nip01Core.core.HexKey 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.managers.WalletManager +import fr.acinq.phoenix.managers.nostrPrivateKey +import fr.acinq.phoenix.managers.nostrPublicKey import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest /** @@ -34,7 +37,8 @@ import kotlinx.coroutines.flow.Flow class RelaysSocketManager constructor( private val nostrSocketClientFactory: NostrSocketClientFactory, private val cachingImportRepository: CachingImportRepository, - private val relayRepository: RelayRepository + private val relayRepository: RelayRepository, + private val walletManager: WalletManager ) { val logger = Logger.withTag("RelaysSocketManager") private val scope = CoroutineScope(Dispatchers.IO) @@ -59,24 +63,17 @@ class RelaysSocketManager constructor( observeActiveUserId() } - private val observeRelayJobs = mutableMapOf() + private val observeRelayJobs = mutableMapOf() private fun observeActiveUserId() = scope.launch { - SeedManager.activePublicKey().toHex()?.let { publicKey -> - observeRelayJobs[publicKey]?.cancel() - - observeRelayJobs[publicKey] = observeRelays(publicKey) + walletManager.keyManager.collectLatest { keyManager -> + keyManager?.nostrPublicKey()?.value?.toHex()?.let { pubkey -> + observeRelayJobs[pubkey]?.cancel() + observeRelayJobs[pubkey] = observeRelays(pubkey) + } } -// credentialsManager.credentials.collect { credentials -> -// credentials.forEach { credential -> -// credential.npub.bech32ToHexOrNull()?.let { publicKey -> -// observeRelayJobs[publicKey]?.cancel() -// -// observeRelayJobs[publicKey] = observeRelays(publicKey) -// } -// } -// } + } private fun observeRelays(publicKey: String): Job = diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt index b57e4a07..122aa1c4 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt @@ -23,7 +23,11 @@ interface ChatRepository { suspend fun observeChatMessageListByChatRoomId(chatRoomId: String): Flow> - suspend fun getOrCreateChatRoom(publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom? + suspend fun getOrCreateChatRoom( + activeUserPublicKey: HexKey, + publicKey: String, relayHint: String?, + defaultSubject: String? = null + ): LocalChatRoom? suspend fun getChatMessageRelayForPublicKey(publicKey: HexKey): ChatMessageRelayListEvent? @@ -63,6 +67,7 @@ interface ChatRepository { } override suspend fun getOrCreateChatRoom( + activeUserPublicKey: HexKey, publicKey: String, relayHint: String?, defaultSubject: String? diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrNotaryRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrNotaryRepository.kt index 718dd211..662db25d 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrNotaryRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrNotaryRepository.kt @@ -6,12 +6,13 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.exceptions.SignatureException import ac.cord.auxiliary.compose.exceptions.SigningKeyNotFoundException import ac.cord.auxiliary.compose.exceptions.SigningRejectedException -import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.network.UserAgent import ac.cord.auxiliary.compose.network.asClientTag import ac.cord.auxiliary.compose.network.dto.RelayDTO import com.vitorpamplona.quartz.nip19Bech32.toNsec import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent +import fr.acinq.phoenix.managers.WalletManager +import fr.acinq.phoenix.managers.nostrPrivateKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -29,6 +30,7 @@ import kotlinx.coroutines.sync.withLock */ class NostrNotaryRepository( private val nostrRepository: NostrRepository, + private val walletManager: WalletManager, ) { private val scope = CoroutineScope(Dispatchers.Main) @@ -70,11 +72,9 @@ class NostrNotaryRepository( setResponse(SignResult.Rejected(SigningRejectedException())) } - private fun findNsecOrThrow(pubkey: String): String = + private fun findNsecOrThrow(activeUserPublicKey: String): String = runCatching { -// val npub = Hex.decode(pubkey).toNpub() -// credentialsStore.findOrThrow(npub = npub).nsec - SeedManager.activeKeyPair().privKey?.toNsec() + walletManager.keyManager.value?.nostrPrivateKey()?.value?.toByteArray()?.toNsec() }.getOrNull() ?: throw SigningKeyNotFoundException() private fun signNostrEvent(publicKey: String, event: UnsignedNostrEvent): NostrEvent? { diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt index b9359324..b5db7456 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt @@ -12,7 +12,6 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount -import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing @@ -20,6 +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 kotlinx.coroutines.flow.Flow interface NostrRepository { @@ -66,7 +66,8 @@ interface NostrRepository { suspend fun publishNostrEvent( unsignedNostrEvent: UnsignedNostrEvent, nostrEvent: NostrEvent, - relayURLs: List = emptyList() + relayURLs: List = emptyList(), + walletManager: WalletManager ) suspend fun broadcastProcessed(broadcastNostrEventRequest: BroadcastNostrEventRequest, status: String = "processing") @@ -83,13 +84,15 @@ interface NostrRepository { suspend fun saveNostrEvent( nostrEvent: NostrEvent, synchronizeNostrEventRequest: SynchronizeNostrEventRequest, - synchronizationRelayURLs: List + synchronizationRelayURLs: List, + walletManager: WalletManager ) suspend fun saveNostrEvent( nostrEvent: NostrEvent, negentropySynchronizeRequest: NegentropySynchronizeRequest, - synchronizationRelayURLs: List + synchronizationRelayURLs: List, + walletManager: WalletManager ) suspend fun queueSynchronizeNostrEvent( @@ -202,7 +205,8 @@ interface NostrRepository { override suspend fun publishNostrEvent( unsignedNostrEvent: UnsignedNostrEvent, nostrEvent: NostrEvent, - relayURLs: List + relayURLs: List, + walletManager: WalletManager ) { } @@ -229,7 +233,8 @@ interface NostrRepository { override suspend fun saveNostrEvent( nostrEvent: NostrEvent, synchronizeNostrEventRequest: SynchronizeNostrEventRequest, - synchronizationRelayURLs: List + synchronizationRelayURLs: List, + walletManager: WalletManager ) { TODO("Not yet implemented") } @@ -237,7 +242,8 @@ interface NostrRepository { override suspend fun saveNostrEvent( nostrEvent: NostrEvent, negentropySynchronizeRequest: NegentropySynchronizeRequest, - synchronizationRelayURLs: List + synchronizationRelayURLs: List, + walletManager: WalletManager ) { TODO("Not yet implemented") } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt index 26e5898e..f9530131 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt @@ -45,10 +45,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.quartz.nip01Core.core.HexKey @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable fun ChatRoomDetailScreen( + activeUserPublicKey: HexKey, publicKey: String, relayHint: String?, initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading, @@ -61,7 +63,8 @@ fun ChatRoomDetailScreen( relayHint = relayHint, initialChatRoomDetailUIState = initialChatRoomDetailUIState, nostrRepository = nostrRepository, - chatRepository = chatRepository + chatRepository = chatRepository, + activeUserPublicKey = activeUserPublicKey ) ) @@ -270,6 +273,7 @@ private fun ChatRoomDetailScreenPreview() { modifier = Modifier.fillMaxSize() ) { ChatRoomDetailScreen( + activeUserPublicKey = "", publicKey = "publicKey", relayHint = null, initialChatRoomDetailUIState = ChatRoomDetailUIState.Loaded( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/CreateProfileScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/CreateProfileScreen.kt index 256257b7..176b33ac 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/CreateProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/CreateProfileScreen.kt @@ -38,6 +38,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import fr.acinq.bitcoin.Chain +import fr.acinq.phoenix.managers.WalletManager @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -45,12 +47,15 @@ fun CreateProfileScreen( initialCreateProfileUIState: CreateProfileUIState = CreateProfileUIState.Declaration, onNavigateToSocialPreconditionRoute: () -> Unit, onNavigateToEndThis: () -> Unit, - nostrRepository: NostrRepository + nostrRepository: NostrRepository, + writeSeed: (List) -> Unit, + walletManager: WalletManager ) { val createProfileViewModel: CreateProfileViewModel = viewModel ( factory = CreateProfileViewModel.factory( initialCreateProfileUIState, - nostrRepository + nostrRepository, + walletManager = walletManager ) ) Scaffold { innerPadding -> @@ -270,7 +275,7 @@ fun CreateProfileScreen( } else { Button( onClick = { - createProfileViewModel.createAccount() + createProfileViewModel.createAccount(writeSeed) } ) { Text( @@ -361,6 +366,7 @@ fun CreateAccountScreenPreview() { modifier = Modifier.fillMaxSize() ) { CreateProfileScreen( + walletManager = WalletManager(Chain.Mainnet), initialCreateProfileUIState = // CreateProfileUIState.InputPrompt, // CreateProfileUIState.Error, @@ -399,7 +405,9 @@ fun CreateAccountScreenPreview() { ), onNavigateToSocialPreconditionRoute = {}, onNavigateToEndThis = {}, - nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY) + writeSeed = {}, + nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY + ) } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt index 9f2689ce..eaada0d2 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt @@ -50,12 +50,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun HomeScreen( + activeUserPublicKey: HexKey, initialHomeScreenUIState: HomeScreenUIState = HomeScreenUIState.Loading, onNavigateToEvent: (Route) -> Unit, onNavigateToDirectMessageDetail: (Route) -> Unit, @@ -68,6 +70,7 @@ fun HomeScreen( val homeScreenViewModel: HomeViewModel = viewModel( factory = HomeViewModel.factory( + activeUserPublicKey = activeUserPublicKey, initialHomeScreenUIState = initialHomeScreenUIState, nostrRepository = nostrRepository, pagerState = rememberPagerState { HomeScreenType.entries.size } @@ -115,6 +118,7 @@ fun HomeScreen( onClick = { onNavigateToEvent.invoke( NostrEventDetailRoute( + activeUserPublicKey = activeUserPublicKey, nostrEventId = homeScreenUIState.profileWithFollowing.nostrEvent.id ) ) @@ -205,6 +209,7 @@ fun HomeScreen( key(feedListViewModel.feedListUIState) { feedListViewModel.RenderFeed( + activeUserPublicKey = activeUserPublicKey, onNavigateToEvent = onNavigateToEvent ) } @@ -262,6 +267,7 @@ private fun HomeScreenPreview() { modifier = Modifier.padding(20.dp) ) { HomeScreen( + activeUserPublicKey = "", initialHomeScreenUIState = HomeScreenUIState.Loaded( profileWithFollowing = LocalProfileWithFollowing( nostrEvent = NostrEvent( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/NostrEventDetailScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/NostrEventDetailScreen.kt index 845bce53..116d56e4 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/NostrEventDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/NostrEventDetailScreen.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @Composable fun NostrEventDetailScreen( initialNostrEventDetailUIState: NostrEventDetailUIState, + activeUserPublicKey: HexKey, nostrEventId: HexKey, nostrRepository: NostrRepository, onNavigateBack: () -> Unit, @@ -48,7 +49,8 @@ fun NostrEventDetailScreen( factory = NostrEventDetailViewModel.factory( nostrEventId = nostrEventId, initialNostrEventDetailUIState = initialNostrEventDetailUIState, - nostrRepository = nostrRepository + nostrRepository = nostrRepository, + activeUserPublicKey = activeUserPublicKey ), ) @@ -67,6 +69,7 @@ fun NostrEventDetailScreen( when(feedListUIState.localNostrEvent.nostrEvent.kind) { MetadataEvent.KIND -> { MetadataEventDetail( + activeUserPublicKey = activeUserPublicKey, localNostrEvent = feedListUIState.localNostrEvent, onNavigateBack = onNavigateBack, onNavigateToEvent = onNavigateToEvent, @@ -164,6 +167,7 @@ It has survived not only five centuries, but also the leap into electronic types ) ), nostrEventId = "", + activeUserPublicKey = "", nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, onNavigateBack = {}, onNavigateToEvent = {}, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchResultScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchResultScreen.kt index 5fc78dfc..25d00763 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchResultScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchResultScreen.kt @@ -63,6 +63,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SearchResultScreen( + activeUserPublicKey: String, searchQuery: String, nostrRepository: NostrRepository, searchRepository: SearchRepository, @@ -265,6 +266,7 @@ fun SearchResultScreen( onNavigateToEvent = { nostrEventId -> onNavigateToEvent.invoke( NostrEventDetailRoute( + activeUserPublicKey = activeUserPublicKey, nostrEventId = nostrEventId ) ) @@ -315,6 +317,7 @@ private fun SearchResultScreenPreview() { modifier = Modifier.padding(20.dp) ) { SearchResultScreen( + activeUserPublicKey = "", searchQuery = "What happened...", nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, searchRepository = SearchRepository.NO_OP_SEARCH_REPOSITORY, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchScreen.kt index 3cc96960..4b83ac86 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/SearchScreen.kt @@ -69,6 +69,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SearchScreen( + activeUserPublicKey: String, initialSearchUIState: SearchUIState, nostrRepository: NostrRepository, searchRepository: SearchRepository, @@ -165,6 +166,7 @@ fun SearchScreen( onClick = { onNavigateToSearchResult.invoke( SearchResultRoute( + activeUserPublicKey = activeUserPublicKey, textFieldState.text.toString() ) ) @@ -180,6 +182,7 @@ fun SearchScreen( .clickable { onNavigateToSearchResult.invoke( SearchResultRoute( + activeUserPublicKey = activeUserPublicKey, textFieldState.text.toString() ) ) @@ -202,6 +205,7 @@ fun SearchScreen( onClick = { onNavigateToSearchResult.invoke( SearchResultRoute( + activeUserPublicKey = activeUserPublicKey, query = "#${textFieldState.text.toString().replace("#","")}" ) ) @@ -217,6 +221,7 @@ fun SearchScreen( .clickable { onNavigateToSearchResult.invoke( SearchResultRoute( + activeUserPublicKey = activeUserPublicKey, "#${textFieldState.text.toString().replaceFirst("#","")}" ) ) @@ -251,7 +256,10 @@ fun SearchScreen( modifier = Modifier .clickable { onNavigateToProfile.invoke( - NostrEventDetailRoute(profile.nostrEventId) + NostrEventDetailRoute( + activeUserPublicKey = activeUserPublicKey, + profile.nostrEventId + ) ) } .fillMaxWidth(), @@ -292,6 +300,7 @@ fun SearchScreen( onClick = { onNavigateToProfile.invoke( NostrEventDetailRoute( + activeUserPublicKey = activeUserPublicKey, profile.nostrEventId ) ) @@ -353,6 +362,7 @@ fun SearchScreen( .clickable { onNavigateToSearchResult.invoke( SearchResultRoute( + activeUserPublicKey = activeUserPublicKey, query = recentSearch.query ) ) @@ -404,6 +414,7 @@ private fun SearchScreenPreview() { modifier = Modifier.padding(20.dp) ) { SearchScreen( + activeUserPublicKey = "", initialSearchUIState = SearchUIState.Prompt, nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, searchRepository = SearchRepository.NO_OP_SEARCH_REPOSITORY, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/WriteNewNoteScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/WriteNewNoteScreen.kt index 56802be5..9dac19f0 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/WriteNewNoteScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/WriteNewNoteScreen.kt @@ -63,6 +63,7 @@ import kotlin.time.Clock @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class) @Composable fun WriteNewNoteScreen( + activeUserPublicKey: HexKey, initialWriteNewNoteUIState: WriteNewNoteUIState = WriteNewNoteUIState.Loading, replyToNostrEventId: HexKey?, quotedNostrEventId: HexKey?, @@ -72,6 +73,7 @@ fun WriteNewNoteScreen( ) { val writeNewNoteViewModel: WriteNewNoteViewModel = viewModel ( factory = WriteNewNoteViewModel.factory( + activeUserPublicKey = activeUserPublicKey, replyToNostrEventId = replyToNostrEventId, quotedNostrEventId = quotedNostrEventId, initialWriteNewNoteUIState, @@ -417,6 +419,7 @@ fun WriteNewNoteScreen( Button( onClick = { writeNewNoteViewModel.createNewNote( + activeUserPublicKey = activeUserPublicKey, onNostrEventPublished = onNostrEventPublished, inReplyToNostrEvent = writeNewNoteUIState.inReplyToNostrEvent, quotedNostrEvent = writeNewNoteUIState.quotedNostrEvent @@ -461,6 +464,7 @@ private fun WriteNewNoteScreenPreview() { modifier = Modifier.fillMaxSize() ) { WriteNewNoteScreen( + activeUserPublicKey = "", replyToNostrEventId = null, quotedNostrEventId = null, initialWriteNewNoteUIState = diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt index 4928e13a..e9304649 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt @@ -4,7 +4,7 @@ import ac.cord.auxiliary.compose.AuxGlobal import ac.cord.auxiliary.compose.database.repository.DatabaseChatRepository import ac.cord.auxiliary.compose.database.repository.DatabaseNostrRepository import ac.cord.auxiliary.compose.database.repository.DatabaseSearchRepository -import ac.cord.auxiliary.compose.managers.DatabaseManager +import ac.cord.auxiliary.compose.managers.AuxDatabaseManager import ac.cord.auxiliary.compose.ui.composable.CreateProfileScreen import ac.cord.auxiliary.compose.ui.composable.ChatRoomDetailScreen import ac.cord.auxiliary.compose.ui.composable.HomeScreen @@ -61,6 +61,7 @@ import androidx.compose.ui.graphics.Color import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.viewModel import co.touchlab.kermit.Logger +import fr.acinq.phoenix.PhoenixGlobal import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -71,6 +72,7 @@ import kotlinx.coroutines.launch @Composable fun AuxNavHost( auxGlobal: AuxGlobal, + phoenixGlobal: PhoenixGlobal, navController: NavHostController ) { val logger = Logger.withTag("AuxNavHost") @@ -85,44 +87,45 @@ fun AuxNavHost( val lifecycleOwner = LocalLifecycleOwner.current - val databaseManager = DatabaseManager(auxGlobal) + val auxDatabaseManager = AuxDatabaseManager(auxGlobal) val databaseNostrRepository = DatabaseNostrRepository( - database = databaseManager.auxDatabase, + database = auxDatabaseManager.auxDatabase, applicationIOScope ) val databaseChatRepository = DatabaseChatRepository( - database = databaseManager.auxDatabase, + database = auxDatabaseManager.auxDatabase, applicationIOScope ) val searchRepository = DatabaseSearchRepository( - database = databaseManager.auxDatabase + database = auxDatabaseManager.auxDatabase ) val navigationViewModel: NavigationViewModel = viewModel ( factory = NavigationViewModel.factory( 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( +// 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 +// ) +// ) // TODO: Produce a synchronization UI element... LaunchedEffect(lifecycleOwner) { @@ -247,26 +250,44 @@ fun AuxNavHost( ) } composable { - CreateProfileScreen( - onNavigateToSocialPreconditionRoute = { - navController.navigate( - route = SocialPreconditionRoute - ) - }, - onNavigateToEndThis = { - navController.navigate( - route = BlankRoute - ) { - popUpTo(0) + 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 -> + + } + ) } - }, - nostrRepository = databaseNostrRepository - ) + ) + } else { + ImplementationPendingScreen("Something went wrong") + } + } composable { backStackEntry -> val route = backStackEntry.toRoute() WriteNewNoteScreen( replyToNostrEventId = route.inReplyToEventId, + activeUserPublicKey = route.activeUserPublicKey, quotedNostrEventId = route.quotedEventId, onNostrEventPublished = { applicationMainScope.launch { @@ -276,6 +297,7 @@ fun AuxNavHost( onNavigateToNostrEvent = { hexKey -> navController.navigate( route = NostrEventDetailRoute( + activeUserPublicKey = route.activeUserPublicKey, nostrEventId = hexKey ) ) @@ -337,8 +359,10 @@ fun AuxNavHost( } ) } - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() HomeScreen( + activeUserPublicKey = route.activeUserPublicKey, onNavigateToEvent = { eventRoute -> navController.navigate( route = eventRoute @@ -346,7 +370,9 @@ fun AuxNavHost( }, onNavigateToWriteNewNote = { navController.navigate( - route = WriteNewNoteRoute() + route = WriteNewNoteRoute( + activeUserPublicKey = route.activeUserPublicKey + ) ) }, onNavigateToSearch = { @@ -375,14 +401,18 @@ fun AuxNavHost( val route = backStackEntry.toRoute() ChatRoomDetailScreen( + activeUserPublicKey = route.activeUserPublicKey, publicKey = route.directMessageIdentifier, relayHint = route.relayHint, nostrRepository = databaseNostrRepository, chatRepository = databaseChatRepository ) } - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() + SearchScreen( + activeUserPublicKey = route.activeUserPublicKey, initialSearchUIState = SearchUIState.Prompt, nostrRepository = databaseNostrRepository, searchRepository = searchRepository, @@ -402,6 +432,7 @@ fun AuxNavHost( val route = backStackEntry.toRoute() SearchResultScreen( + activeUserPublicKey = route.activeUserPublicKey, searchQuery = route.query, nostrRepository = databaseNostrRepository, searchRepository = searchRepository, @@ -419,6 +450,7 @@ fun AuxNavHost( val route = backStackEntry.toRoute() NostrEventDetailScreen( + activeUserPublicKey = route.activeUserPublicKey, initialNostrEventDetailUIState = NostrEventDetailUIState.Loading, nostrEventId = route.nostrEventId, nostrRepository = databaseNostrRepository, @@ -433,6 +465,7 @@ fun AuxNavHost( onNavigateToWriteAReply = { nostrEventId -> navController.navigate( route = WriteNewNoteRoute( + activeUserPublicKey = route.activeUserPublicKey, inReplyToEventId = nostrEventId ) ) @@ -445,6 +478,7 @@ fun AuxNavHost( onNavigateToQuoteNostrEvent = { nostrEventId -> navController.navigate( route = WriteNewNoteRoute( + activeUserPublicKey = route.activeUserPublicKey, quotedEventId = nostrEventId ) ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt index bde28b8c..025b3cc0 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class ChatRoomDetailRoute( + val activeUserPublicKey: String, val directMessageIdentifier: String, // TODO: have this as a publicKey val relayHint: String? ): Route() { diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/FeedRoute.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/FeedRoute.kt index db2b034c..01fc3bb3 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/FeedRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/FeedRoute.kt @@ -3,4 +3,6 @@ package ac.cord.auxiliary.compose.ui.composable.navigation.routes import kotlinx.serialization.Serializable @Serializable -object FeedRoute: Route() \ No newline at end of file +data class FeedRoute( + val activeUserPublicKey: String +): Route() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt index 2d339b35..758edf8b 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class NostrEventDetailRoute( + val activeUserPublicKey: String, val nostrEventId: String, ): Route() { } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchResultRoute.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchResultRoute.kt index 434fbd08..3e959c8b 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchResultRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchResultRoute.kt @@ -4,5 +4,6 @@ import kotlinx.serialization.Serializable @Serializable data class SearchResultRoute( + val activeUserPublicKey: String, val query: String ): Route() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchRoute.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchRoute.kt index ce5b61c3..53959cf2 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/SearchRoute.kt @@ -3,4 +3,6 @@ package ac.cord.auxiliary.compose.ui.composable.navigation.routes import kotlinx.serialization.Serializable @Serializable -object SearchRoute: Route() \ No newline at end of file +data class SearchRoute( + val activeUserPublicKey: String, +): Route() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/WriteNewNoteRoute.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/WriteNewNoteRoute.kt index 605ba3d3..da05fd87 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/WriteNewNoteRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/WriteNewNoteRoute.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class WriteNewNoteRoute( + val activeUserPublicKey: String, val inReplyToEventId: String? = null, val quotedEventId: String? = null, ): Route() { diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt index 3abb1e5a..6e6532cd 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import kotlinx.coroutines.launch @@ -67,6 +68,7 @@ import kotlinx.coroutines.launch @Composable fun MetadataEventDetail( localNostrEvent: LocalNostrEvent, + activeUserPublicKey: HexKey, onNavigateBack: () -> Unit, onNavigateToEvent: (Route) -> Unit, onNavigateToEditProfile: () -> Unit, @@ -80,6 +82,7 @@ fun MetadataEventDetail( val metadataEventDetailViewModel: MetadataEventDetailViewModel = viewModel( factory = MetadataEventDetailViewModel.factory( + activeUserPublicKey = activeUserPublicKey, publicKey = localNostrEvent.nostrEvent.pubKey, nostrRepository = nostrRepository, pagerState = rememberPagerState { MetadataEventDetailType.entries.size } @@ -227,6 +230,7 @@ fun MetadataEventDetail( onClick = { onNavigateToChatRoom.invoke( ChatRoomDetailRoute( + activeUserPublicKey = activeUserPublicKey, directMessageIdentifier = localNostrEvent.nostrEvent.pubKey, relayHint = localNostrEvent.nostrEvent.relayUrl, ) @@ -305,6 +309,7 @@ fun MetadataEventDetail( key(feedListViewModel.feedListUIState) { feedListViewModel.RenderFeed( + activeUserPublicKey = activeUserPublicKey, onNavigateToEvent = onNavigateToEvent ) } @@ -328,6 +333,7 @@ fun MetadataEventDetail( key(followingListViewModel.followingListUIState) { followingListViewModel.RenderFeed( + activeUserPublicKey = activeUserPublicKey, onNavigateToEvent = onNavigateToEvent ) } @@ -350,6 +356,7 @@ fun MetadataEventDetail( key(followersListViewModel.followersListUIState) { followersListViewModel.RenderFeed( + activeUserPublicKey = activeUserPublicKey, onNavigateToEvent = onNavigateToEvent ) } @@ -380,6 +387,7 @@ private fun MetadataEventEventDetailPreview() { modifier = Modifier.padding(20.dp) ) { MetadataEventDetail( + activeUserPublicKey = "", localNostrEvent = LocalNostrEvent( nostrEvent = NostrEvent( id = "eventId", diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailViewModel.kt index fc13c4f6..95bbf988 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailViewModel.kt @@ -13,6 +13,7 @@ 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.HexKey import fr.acinq.bitcoin.Crypto import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -20,6 +21,7 @@ import kotlinx.coroutines.launch class ChatRoomDetailViewModel( val directMessageId: String, + val activeUserPublicKey: HexKey, val relayHint: String?, initialChatRoomDetailUIState: ChatRoomDetailUIState, val nostrRepository: NostrRepository, @@ -56,6 +58,7 @@ class ChatRoomDetailViewModel( viewModelScope.launch(Dispatchers.IO) { // We need to get or create a chat with the user on this publicKey... val localChatRoom = chatRepository.getOrCreateChatRoom( + activeUserPublicKey = activeUserPublicKey, publicKey = directMessageId, relayHint = relayHint ) @@ -76,6 +79,7 @@ class ChatRoomDetailViewModel( private const val TAG = "ChatRoomDetailViewModel" fun factory( + activeUserPublicKey: HexKey, publicKey: String, relayHint: String?, initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading, @@ -84,6 +88,7 @@ class ChatRoomDetailViewModel( ): ViewModelProvider.Factory = viewModelFactory { initializer { ChatRoomDetailViewModel( + activeUserPublicKey = activeUserPublicKey, directMessageId = publicKey, relayHint = relayHint, initialChatRoomDetailUIState = initialChatRoomDetailUIState, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt index 96866dac..9d2f3453 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt @@ -144,6 +144,7 @@ class ChatRoomListViewModel( onClick = { onNavigateToDirectMessageDetail.invoke( ChatRoomDetailRoute( + activeUserPublicKey = publicKey, directMessageIdentifier = localChatRoom.chatRoom.id, relayHint = localChatRoom.localParticipants.firstOrNull { it.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey }?.participant?.relayHint ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/CreateProfileViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/CreateProfileViewModel.kt index ebcd9f98..8e37d4e1 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/CreateProfileViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/CreateProfileViewModel.kt @@ -1,6 +1,5 @@ package ac.cord.auxiliary.compose.ui.view.model -import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.ui.view.state.CreateProfileUIState import androidx.compose.runtime.MutableState @@ -13,6 +12,18 @@ import ac.cord.auxiliary.compose.ui.view.state.form.CreateProfileFormState import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import fr.acinq.bitcoin.Chain +import fr.acinq.bitcoin.MnemonicCode +import fr.acinq.bitcoin.byteVector +import fr.acinq.lightning.Lightning +import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.LocalKeyManager +import fr.acinq.phoenix.managers.NodeParamsManager +import fr.acinq.phoenix.managers.WalletManager +import fr.acinq.phoenix.managers.nostrPrivateKey +import fr.acinq.phoenix.managers.nostrPublicKey +import fr.acinq.phoenix.utils.MnemonicLanguage +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.launch @@ -20,19 +31,22 @@ import kotlinx.coroutines.launch class CreateProfileViewModel( val initialCreateProfileUIState: CreateProfileUIState, val createProfileFormState: CreateProfileFormState = CreateProfileFormState(), - val nostrRepository: NostrRepository + val nostrRepository: NostrRepository, + val walletManager: WalletManager ): ViewModel() { companion object { private const val TAG = "CreateAccountViewModel" fun factory( initialCreateProfileUIState: CreateProfileUIState, - nostrRepository: NostrRepository + nostrRepository: NostrRepository, + walletManager: WalletManager ): ViewModelProvider.Factory = viewModelFactory { initializer { CreateProfileViewModel( initialCreateProfileUIState, - nostrRepository = nostrRepository + nostrRepository = nostrRepository, + walletManager = walletManager ) } } @@ -64,18 +78,36 @@ class CreateProfileViewModel( return true } + public fun createAccount( + writeSeed: (List) -> Unit // onNavigateToUnsignedProfile: (UnsignedProfileRoute) -> Unit, // onNavigateToUnannouncedProfile: (UnannouncedProfileRoute) -> Unit ) { logger.d { "createAccount" } isActionPending.value = true - viewModelScope.launch(Dispatchers.IO) { - val publicKey = SeedManager.activePublicKey().toHexKey() + viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> + logger.e("error when creating new wallet: ", e) + throw e + }) { + // TODO: Generate a new profile... + logger.d("generating new wallet...") + val entropy = Lightning.randomBytes(16) + val mnemonics = MnemonicCode.toMnemonics( + entropy = entropy, + wordlist = MnemonicLanguage.English.wordlist() + ) + writeSeed(mnemonics) + + val localKeyManager = LocalKeyManager( + seed = MnemonicCode.toSeed(mnemonics, "").byteVector(), + chain = Chain.Mainnet, + remoteSwapInExtendedPublicKey = NodeParamsManager.remoteSwapInXpub + ) nostrRepository.createNewProfile( - publicKey, + localKeyManager.nostrPublicKey().toHex(), name = createProfileFormState.nameField.textFieldState.text.toString(), biography = createProfileFormState.biographyField.textFieldState.text.toString() ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt index c2e3b14a..5e032633 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt @@ -113,6 +113,7 @@ class FeedListViewModel( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun RenderFeed( + activeUserPublicKey: String, onNavigateToEvent: (Route) -> Unit ) { Column( @@ -160,6 +161,7 @@ class FeedListViewModel( onNavigateToEvent = { nostrEventId -> onNavigateToEvent.invoke( NostrEventDetailRoute( + activeUserPublicKey = activeUserPublicKey, nostrEventId = nostrEventId ) ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt index e73e4994..bd3159e5 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt @@ -105,6 +105,7 @@ class FollowersListViewModel( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun RenderFeed( + activeUserPublicKey: String, onNavigateToEvent: (Route) -> Unit ) { Column( @@ -149,6 +150,7 @@ class FollowersListViewModel( key = { profile -> profile.publicKey } ) { follower -> follower.RenderAsListItem( + activeUserPublicKey = activeUserPublicKey, onNavigateToEvent = onNavigateToEvent ) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt index b2e038c0..8e42b639 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt @@ -104,6 +104,7 @@ class FollowingListViewModel( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun RenderFeed( + activeUserPublicKey: String, onNavigateToEvent: (Route) -> Unit ) { Column( @@ -148,6 +149,7 @@ class FollowingListViewModel( key = { profile -> profile.publicKey } ) { profile -> profile.RenderAsListItem( + activeUserPublicKey = activeUserPublicKey, onNavigateToEvent = onNavigateToEvent ) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt index 13f1d890..ae2869f4 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt @@ -2,7 +2,6 @@ package ac.cord.auxiliary.compose.ui.view.model import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter -import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.ui.view.state.HomeScreenUIState import androidx.compose.foundation.pager.PagerState @@ -15,6 +14,7 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent @@ -38,6 +38,7 @@ enum class HomeScreenType { } class HomeViewModel( + val activeUserPublicKey: HexKey, val initialHomeScreenUIState: HomeScreenUIState, val nostrRepository: NostrRepository, val pagerState: PagerState @@ -56,10 +57,8 @@ class HomeViewModel( fun observeActiveUserProfileWithFollowing() { logger.d("observeActiveUserProfileWithFollowing") viewModelScope.launch(Dispatchers.IO) { - val activePublicKey = SeedManager.activePublicKey().toHexKey() - nostrRepository.observeProfileWithFollowing( - activePublicKey + activeUserPublicKey ).collect { profileWithFollowing -> homeScreenUIState = if (profileWithFollowing != null) { HomeScreenUIState.Loaded( @@ -76,7 +75,6 @@ class HomeViewModel( homeScreenType: HomeScreenType, profileWithFollowing: LocalProfileWithFollowing, ): SynchronizationFilter { - val publicKey = SeedManager.activePublicKey().toHexKey() val twelveHoursAgo = Clock.System.now().minus(12.hours) val now = Clock.System.now() @@ -108,7 +106,7 @@ class HomeViewModel( FileServersEvent.KIND ), tags = mapOf( - Pair("p", listOf(publicKey)) + Pair("p", listOf(activeUserPublicKey)) ), since = twelveHoursAgo, until = now, @@ -121,7 +119,7 @@ class HomeViewModel( GiftWrapEvent.KIND, ), tags = mapOf( - Pair("p", listOf(publicKey)) + Pair("p", listOf(activeUserPublicKey)) ), limit = 50 ) @@ -133,12 +131,14 @@ class HomeViewModel( const val TAG = "HomeViewModel" fun factory( + activeUserPublicKey: HexKey, initialHomeScreenUIState: HomeScreenUIState, nostrRepository: NostrRepository, pagerState: PagerState ): ViewModelProvider.Factory = viewModelFactory { initializer { HomeViewModel( + activeUserPublicKey = activeUserPublicKey, initialHomeScreenUIState = initialHomeScreenUIState, nostrRepository = nostrRepository, pagerState = pagerState, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/InReplyToViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/InReplyToViewModel.kt index e9273bbd..dba9bc59 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/InReplyToViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/InReplyToViewModel.kt @@ -118,6 +118,7 @@ class InReplyToViewModel( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun RenderFeed( + activeUserPublicKey: String, onNavigateToEvent: (Route) -> Unit ) { Column( @@ -165,6 +166,7 @@ class InReplyToViewModel( onNavigateToEvent = { nostrEventId -> onNavigateToEvent.invoke( NostrEventDetailRoute( + activeUserPublicKey = activeUserPublicKey, nostrEventId = nostrEventId ) ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt index df70b6bb..364db056 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt @@ -3,7 +3,6 @@ package ac.cord.auxiliary.compose.ui.view.model import ac.cord.auxiliary.compose.database.model.Connection import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter -import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.ui.view.state.MetadataEventDetailUIState import androidx.compose.foundation.pager.PagerState @@ -46,7 +45,8 @@ enum class MetadataEventDetailType { class MetadataEventDetailViewModel( initialMetadataEventDetailUIState: MetadataEventDetailUIState = MetadataEventDetailUIState.Loading, - val publicKey: HexKey, + val activeUserPublicKey: HexKey, + val eventPublicKey: HexKey, val nostrRepository: NostrRepository, val pagerState: PagerState ): ViewModel() { @@ -65,18 +65,17 @@ class MetadataEventDetailViewModel( fun observeMetadataEventRelationWithActiveUser() { logger.d("observeLocalNostrFeed") viewModelScope.launch(Dispatchers.IO) { - val activePublicKey = SeedManager.activePublicKey().toHexKey() nostrRepository.observeRelation( - publicKey, - activePublicKey = activePublicKey + eventPublicKey, + activePublicKey = activeUserPublicKey ).collect { relations -> logger.d("We found relations: $relations") metadataEventDetailUIState = MetadataEventDetailUIState.Loaded( - isActiveUser = isActiveUser(activePublicKey), - followingActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == publicKey && connection.destinationPublicKey == activePublicKey }, - followedByActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == activePublicKey && connection.destinationPublicKey == publicKey } + isActiveUser = isActiveUser(activeUserPublicKey), + followingActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == eventPublicKey && connection.destinationPublicKey == activeUserPublicKey }, + followedByActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == activeUserPublicKey && connection.destinationPublicKey == eventPublicKey } ) if (isActionPending.value) { @@ -88,14 +87,12 @@ class MetadataEventDetailViewModel( } fun isActiveUser(activePublicKey: HexKey): Boolean { - return activePublicKey == publicKey + return activePublicKey == eventPublicKey } fun isActiveUser(): Boolean { - val activePublicKey = SeedManager.activePublicKey().toHexKey() - return isActiveUser( - activePublicKey = activePublicKey + activePublicKey = activeUserPublicKey ) } @@ -105,7 +102,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Posts -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( TextNoteEvent.KIND, @@ -117,7 +114,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Replies -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( TextNoteEvent.KIND, @@ -130,7 +127,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Articles -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( LongTextNoteEvent.KIND, @@ -141,7 +138,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Followers -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( ContactListEvent.KIND, @@ -153,7 +150,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Following -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( ContactListEvent.KIND, @@ -164,7 +161,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Zaps -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( LnZapEvent.KIND, @@ -176,7 +173,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Photos -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( PictureEvent.KIND, @@ -187,7 +184,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Shorts -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( VideoShortEvent.KIND, @@ -198,7 +195,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Videos -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( VideoNormalEvent.KIND, @@ -209,7 +206,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Bookmarks -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( BookmarkListEvent.KIND, @@ -220,7 +217,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Reports -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( ReportEvent.KIND, @@ -231,7 +228,7 @@ class MetadataEventDetailViewModel( MetadataEventDetailType.Relays -> { SynchronizationFilter( authors = arrayOf( - publicKey + eventPublicKey ), kinds = arrayOf( RelayFeedsListEvent.KIND, @@ -244,32 +241,30 @@ class MetadataEventDetailViewModel( fun follow() { isActionPending.value = true - val activePublicKey = SeedManager.activePublicKey().toHexKey() - viewModelScope.launch(Dispatchers.IO) { val contactListEvent = nostrRepository.getNostrEvent( - publicKey = activePublicKey, + publicKey = activeUserPublicKey, kind = ContactListEvent.KIND ) logger.d("ContactListEvent: $contactListEvent") val followUsers: TagArray = if (contactListEvent != null) { - if (contactListEvent.tags.isTaggedUser(publicKey)) { + if (contactListEvent.tags.isTaggedUser(eventPublicKey)) { // User already being followed - logger.d("User is already following $publicKey") + logger.d("User is already following $eventPublicKey") return@launch } contactListEvent.tags.plus( ContactTag( - pubKey = publicKey + pubKey = eventPublicKey ).toTagArray() ) } else { arrayOf( - ContactTag(activePublicKey, null, null).toTagArray(), - ContactTag(publicKey, null, null).toTagArray(), + ContactTag(activeUserPublicKey, null, null).toTagArray(), + ContactTag(eventPublicKey, null, null).toTagArray(), ) } val contactListTagList = listOf(AltTag.assemble(ContactListEvent.ALT)) + @@ -277,7 +272,7 @@ class MetadataEventDetailViewModel( nostrRepository.saveUnsignedNostrEvent( UnsignedNostrEvent( - pubKey = activePublicKey, + pubKey = activeUserPublicKey, kind = ContactListEvent.KIND, tags = contactListTagList.toTypedArray(), content = RelaySet.assemble( @@ -291,24 +286,22 @@ class MetadataEventDetailViewModel( fun unfollow(connection: Connection) { isActionPending.value = true - val activePublicKey = SeedManager.activePublicKey().toHexKey() - viewModelScope.launch(Dispatchers.IO) { val contactListEvent = nostrRepository.getNostrEvent( - publicKey = activePublicKey, + publicKey = activeUserPublicKey, kind = ContactListEvent.KIND ) if (contactListEvent != null) { - if (!contactListEvent.tags.isTaggedUser(publicKey)) { - logger.d("User is not being followed so no need to unfollow: $publicKey") + if (!contactListEvent.tags.isTaggedUser(eventPublicKey)) { + logger.d("User is not being followed so no need to unfollow: $eventPublicKey") } else { nostrRepository.saveUnsignedNostrEvent( UnsignedNostrEvent( - pubKey = activePublicKey, + pubKey = activeUserPublicKey, kind = ContactListEvent.KIND, - tags = contactListEvent.tags.filter { it.size > 1 && it[1] != publicKey }.toTypedArray(), + tags = contactListEvent.tags.filter { it.size > 1 && it[1] != eventPublicKey }.toTypedArray(), content = RelaySet.assemble( emptyMap() ) @@ -326,15 +319,17 @@ class MetadataEventDetailViewModel( const val TAG = "MetadataEventDetailViewModel" fun factory( + activeUserPublicKey: HexKey, publicKey: HexKey, nostrRepository: NostrRepository, pagerState: PagerState ): ViewModelProvider.Factory = viewModelFactory { initializer { MetadataEventDetailViewModel( + activeUserPublicKey = activeUserPublicKey, nostrRepository = nostrRepository, pagerState = pagerState, - publicKey = publicKey + eventPublicKey = publicKey, ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt index 7eafdbdc..f0de7f2d 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt @@ -1,26 +1,94 @@ package ac.cord.auxiliary.compose.ui.view.model -import ac.cord.auxiliary.compose.managers.SeedManager +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.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) : 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> + +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, + onWritingSeedError: (WritingSeedState.Error) -> Unit, + onWritingSeedStateWriting: (WritingSeedState.Writing) -> Unit, + isRestoringWallet: Boolean, + isTorEnabled: Boolean, + customElectrumServer: ElectrumConfig.Custom?, + onSeedWritten: (WalletId) -> Unit +) + + class NavigationViewModel( initialNavigationUIState: NavigationUIState, + val phoenixGlobal: PhoenixGlobal, val nostrRepository: NostrRepository, val scope: CoroutineScope, ): ViewModel() { @@ -31,10 +99,12 @@ class NavigationViewModel( fun factory( initialNavigationUIState: NavigationUIState, nostrRepository: NostrRepository, + phoenixGlobal: PhoenixGlobal, scope: CoroutineScope, ): ViewModelProvider.Factory = viewModelFactory { initializer { NavigationViewModel( + phoenixGlobal = phoenixGlobal, initialNavigationUIState = initialNavigationUIState, nostrRepository = nostrRepository, scope = scope @@ -50,72 +120,262 @@ class NavigationViewModel( ) val navigationUIState = _navigationUIState.asStateFlow() + val listWalletState = mutableStateOf(ListWalletState.Init) + + private val _availableWallets = MutableStateFlow>(emptyMap()) + val availableWallets = _availableWallets.asStateFlow() + + private val _desiredWalletId = MutableStateFlow(null) + val desiredWalletId = _desiredWalletId.asStateFlow() + val startWalletImmediately = MutableStateFlow(true) + + private val _activeWalletInUI = MutableStateFlow(null) + val activeWalletInUI = _activeWalletInUI.asStateFlow() + init { observeProfile() } + private fun observeProfile() { logger.i("observeProfile") scope.launch(Dispatchers.IO) { delay(2_100) // Looking busy... logger.i("Navigation UI State is Landing") - val publicKey = SeedManager.activePublicKey().toHexKey() - logger.i("Observing: $publicKey") - 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 - ) + + activeWalletInUI.collectLatest { activeWallet -> + if (activeWallet == null) { + // TODO: No active wallet + } else { + if (activeWallet.business != null) { + activeWallet.business.walletManager.keyManager.collectLatest { keyManager -> + keyManager?.nostrPublicKey()?.let { nostrPublicKey -> + val publicKey = nostrPublicKey.toHex() + + logger.i("Observing: $publicKey") + + 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 + ) + } + } + } + } + } } else { - logger.i("We have an unsigned profile: ${localProfile.unsignedNostrEvent}") - NavigationUIState.UnsignedProfile( - unsignedNostrEvent = localProfile.unsignedNostrEvent - ) + // 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() } } } } } +// 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(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.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, + 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) + } + ) + } + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NostrEventDetailViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NostrEventDetailViewModel.kt index b5436a99..aebc209a 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NostrEventDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NostrEventDetailViewModel.kt @@ -2,7 +2,6 @@ package ac.cord.auxiliary.compose.ui.view.model import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter -import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.nostr.Relays import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.ui.view.state.NostrEventDetailUIState @@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class NostrEventDetailViewModel( + val activeUserPublicKey: HexKey, val nostrEventId: HexKey, initialNostrEventDetailUIState: NostrEventDetailUIState, val nostrRepository: NostrRepository @@ -54,7 +54,12 @@ class NostrEventDetailViewModel( LnZapEvent.KIND, ), tags = mapOf( - Pair("p", listOf(SeedManager.activePublicKey().toHexKey())) + Pair( + "p", + listOf( + activeUserPublicKey + ) + ) ), limit = 50 ) @@ -85,6 +90,7 @@ class NostrEventDetailViewModel( fun factory( nostrEventId: HexKey, + activeUserPublicKey: HexKey, initialNostrEventDetailUIState: NostrEventDetailUIState = NostrEventDetailUIState.Loading, nostrRepository: NostrRepository, ): ViewModelProvider.Factory = viewModelFactory { @@ -92,7 +98,8 @@ class NostrEventDetailViewModel( NostrEventDetailViewModel( nostrEventId = nostrEventId, initialNostrEventDetailUIState = initialNostrEventDetailUIState, - nostrRepository = nostrRepository + nostrRepository = nostrRepository, + activeUserPublicKey = activeUserPublicKey ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NotaryViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NotaryViewModel.kt index 02ddd7c0..ec69d43a 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NotaryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NotaryViewModel.kt @@ -1,65 +1,33 @@ package ac.cord.auxiliary.compose.ui.view.model -import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest -import ac.cord.auxiliary.compose.database.model.GiftWrapSeal import ac.cord.auxiliary.compose.database.model.NostrEvent -import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest -import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter -import ac.cord.auxiliary.compose.managers.SeedManager -import ac.cord.auxiliary.compose.network.dto.toRelayDTO -import ac.cord.auxiliary.compose.network.relays.RelayPool.Companion.PUBLISH_TIMEOUT -import ac.cord.auxiliary.compose.network.relays.RelaysSocketManager -import ac.cord.auxiliary.compose.network.sockets.NostrIncomingMessage -import ac.cord.auxiliary.compose.network.sockets.NostrSocketClientFactory import ac.cord.auxiliary.compose.nostr.Relays -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 ac.cord.auxiliary.compose.ui.view.state.NavigationUIState import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.initializer 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.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey -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.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers -import com.vitorpamplona.quartz.nip17Dm.NIP17Factory -import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent -import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent -import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd -import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd -import com.vitorpamplona.quartz.utils.TimeUtils +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.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.getAndUpdate -import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Instant class NotaryViewModel( val nostrRepository: NostrRepository, val chatRepository: ChatRepository, val scope: CoroutineScope, + val walletManager: WalletManager, ): ViewModel() { companion object { @@ -69,13 +37,15 @@ class NotaryViewModel( fun factory( nostrRepository: NostrRepository, chatRepository: ChatRepository, + walletManager: WalletManager, scope: CoroutineScope, ): ViewModelProvider.Factory = viewModelFactory { initializer { NotaryViewModel( nostrRepository = nostrRepository, chatRepository = chatRepository, - scope = scope + scope = scope, + walletManager = walletManager ) } } @@ -84,59 +54,71 @@ class NotaryViewModel( private val logger = Logger.withTag(TAG) init { - observeUnsignedNostrEvents() - observeUnsealedGiftWrapPayloads() + walletManager.keyManager.value?.nostrPrivateKey()?.let { nostrPrivateKey -> + val keyPair = KeyPair( + privKey = nostrPrivateKey.value.toByteArray() + ) + + observeUnsignedNostrEvents(keyPair) + observeUnsealedGiftWrapPayloads(keyPair) + } } - private fun observeUnsignedNostrEvents() { - val tempSigner = NostrSignerSync( - SeedManager.activeKeyPair() - ) - scope.launch(Dispatchers.IO) { - logger.i { "observeUnsignedNostrEvents" } - nostrRepository.observeUnsignedNostrEvents( - publicKey = SeedManager.activePublicKey().toHexKey() - ).distinctUntilChanged().collect { unsignedNostrEventOrNull -> - scope.launch(Dispatchers.IO) { - unsignedNostrEventOrNull?.let { unsignedNostrEvent -> - logger.d("Unsigned: ${unsignedNostrEvent.kind}") - val event = tempSigner.signNormal( - createdAt = unsignedNostrEvent.createdAt.epochSeconds, - kind = unsignedNostrEvent.kind, - tags = unsignedNostrEvent.tags, - content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content - ) - logger.d("Signed: ${event.toJson()}") + private fun observeUnsignedNostrEvents( + keyPair: KeyPair + ) { + val tempSigner = NostrSignerSync( + keyPair + ) + scope.launch(Dispatchers.IO) { + logger.i { "observeUnsignedNostrEvents" } + nostrRepository.observeUnsignedNostrEvents( + publicKey = keyPair.pubKey.toHexKey() + ).distinctUntilChanged().collect { unsignedNostrEventOrNull -> + scope.launch(Dispatchers.IO) { + unsignedNostrEventOrNull?.let { unsignedNostrEvent -> + logger.d("Unsigned: ${unsignedNostrEvent.kind}") + val event = tempSigner.signNormal( + createdAt = unsignedNostrEvent.createdAt.epochSeconds, + kind = unsignedNostrEvent.kind, + tags = unsignedNostrEvent.tags, + content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content + ) + logger.d("Signed: ${event.toJson()}") - nostrRepository.publishNostrEvent( - unsignedNostrEvent, - NostrEvent( - id = event.id, - pubKey = event.pubKey, - kind = event.kind, - tags = event.tags, - content = event.content, - createdAt = Instant.fromEpochSeconds(event.createdAt), - sig = event.sig, - unsignedNostrEventId = unsignedNostrEvent.id - ), - relayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } - ) + nostrRepository.publishNostrEvent( + unsignedNostrEvent, + NostrEvent( + id = event.id, + pubKey = event.pubKey, + kind = event.kind, + tags = event.tags, + content = event.content, + createdAt = Instant.fromEpochSeconds(event.createdAt), + sig = event.sig, + unsignedNostrEventId = unsignedNostrEvent.id + ), + relayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }, + walletManager = walletManager + ) + } } } } } - } - private fun observeUnsealedGiftWrapPayloads() { + + private fun observeUnsealedGiftWrapPayloads( + keyPair: KeyPair + ) { val tempSigner = NostrSignerSync( - SeedManager.activeKeyPair() + keyPair ) scope.launch(Dispatchers.IO) { logger.i { "observeUnsealedGiftWrapPayloads" } chatRepository.observeUnsealedGiftWrapPayloads( - publicKey = SeedManager.activePublicKey().toHexKey() + publicKey = keyPair.pubKey.toHexKey() ).distinctUntilChanged().collect { giftWrapPayloadOrNull -> scope.launch(Dispatchers.IO) { giftWrapPayloadOrNull?.let { giftWrapPayload -> @@ -152,5 +134,4 @@ class NotaryViewModel( } } - } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/SynchronizationViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/SynchronizationViewModel.kt index 5ee1b79c..36b6037a 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/SynchronizationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/SynchronizationViewModel.kt @@ -24,6 +24,7 @@ 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.managers.WalletManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -40,6 +41,7 @@ class SynchronizationViewModel( val nostrRepository: NostrRepository, val chatRepository: ChatRepository, val relayRepository: RelayRepository, + val walletManager: WalletManager, val scope: CoroutineScope, ): ViewModel() { @@ -47,10 +49,11 @@ class SynchronizationViewModel( nostrSocketClientFactory = NostrSocketClientFactory, cachingImportRepository = CachingImportRepository.NO_OP_CACHING_IMPORT_REPOSITORY, relayRepository = relayRepository, + walletManager = walletManager ) companion object { - private const val TAG = "NavigationViewModel" + private const val TAG = "SynchronizationViewModel" private val mutex = Mutex() @@ -58,6 +61,7 @@ class SynchronizationViewModel( nostrRepository: NostrRepository, chatRepository: ChatRepository, relayRepository: RelayRepository, + walletManager: WalletManager, scope: CoroutineScope, ): ViewModelProvider.Factory = viewModelFactory { initializer { @@ -65,6 +69,7 @@ class SynchronizationViewModel( nostrRepository = nostrRepository, chatRepository = chatRepository, relayRepository = relayRepository, + walletManager = walletManager, scope = scope ) } @@ -119,7 +124,9 @@ class SynchronizationViewModel( nostrRepository.saveNostrEvent( nostrEvent = it, synchronizeNostrEventRequest, - synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } + synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL), + walletManager = walletManager + // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } ) } } @@ -131,7 +138,9 @@ class SynchronizationViewModel( nostrRepository.saveNostrEvent( nostrEvent = nostrEvent, synchronizeNostrEventRequest, - synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } + synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL), + walletManager = walletManager + // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } ) } } @@ -228,7 +237,9 @@ class SynchronizationViewModel( nostrRepository.saveNostrEvent( nostrEvent = it, negentropySynchronizeRequest, - synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } + synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL), + walletManager = walletManager + // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } ) } } @@ -240,7 +251,9 @@ class SynchronizationViewModel( nostrRepository.saveNostrEvent( nostrEvent = nostrEvent, negentropySynchronizeRequest, - synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } + synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL), + walletManager = walletManager + // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url } ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/WriteNewNoteViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/WriteNewNoteViewModel.kt index d9deef68..8870a38a 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/WriteNewNoteViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/WriteNewNoteViewModel.kt @@ -3,7 +3,6 @@ package ac.cord.auxiliary.compose.ui.view.model import ac.cord.auxiliary.compose.database.model.Profile import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing -import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentParser import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentSegment @@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch class WriteNewNoteViewModel( + val activeUserPublicKey: HexKey, val replyToNostrEventId: HexKey?, val quotedNostrEventId: HexKey?, @@ -41,6 +41,7 @@ class WriteNewNoteViewModel( private const val TAG = "WriteNewNoteViewModel" fun factory( + activeUserPublicKey: HexKey, replyToNostrEventId: HexKey?, quotedNostrEventId: HexKey?, initialWriteNewNoteUIState: WriteNewNoteUIState, @@ -48,6 +49,7 @@ class WriteNewNoteViewModel( ): ViewModelProvider.Factory = viewModelFactory { initializer { WriteNewNoteViewModel( + activeUserPublicKey = activeUserPublicKey, replyToNostrEventId = replyToNostrEventId, quotedNostrEventId = quotedNostrEventId, initialWriteNewNoteUIState = initialWriteNewNoteUIState, @@ -120,7 +122,7 @@ class WriteNewNoteViewModel( ).firstOrNull() } - val activePublicKey = SeedManager.activePublicKey().toHexKey() + val activePublicKey = activeUserPublicKey // TODO: Get note being replied too... // TODO: Get users being mentioned... @@ -170,6 +172,7 @@ class WriteNewNoteViewModel( } fun createNewNote( + activeUserPublicKey: HexKey, onNostrEventPublished: () -> Unit, inReplyToNostrEvent: LocalNostrEvent? = null, quotedNostrEvent: LocalNostrEvent? = null, @@ -183,7 +186,7 @@ class WriteNewNoteViewModel( ).filterIsInstance().map { it.pubkey } nostrRepository.createNewTextNote( - publicKey = SeedManager.activePublicKey().toHexKey(), + publicKey = activeUserPublicKey, mentionedPublicKeys = mentionedPublicKeys, textInput = writeNewNoteFormState.textField.textFieldState.text.toString(), inReplyToNostrEvent = inReplyToNostrEvent, diff --git a/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/SeedManager.kt b/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/SeedManager.kt index 9b423a0c..c10e9e32 100644 --- a/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/SeedManager.kt +++ b/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/SeedManager.kt @@ -113,7 +113,7 @@ object SeedManager { * Returns an empty map if the seed file does not exist yet. * Returns null if there was a problem when loading or decrypting the seed file. */ - suspend fun loadAndDecryptOrNull(phoenixGlobal: PhoenixGlobal): Map? = when (val res = loadAndDecrypt(phoenixGlobal)) { + fun loadAndDecryptOrNull(phoenixGlobal: PhoenixGlobal): Map? = when (val res = loadAndDecrypt(phoenixGlobal)) { is DecryptSeedResult.Success -> res.userWalletsMap is DecryptSeedResult.Failure.SeedFileNotFound -> emptyMap() is DecryptSeedResult.Failure -> null diff --git a/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/WalletManager.kt b/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/WalletManager.kt index 7e08950f..2949111b 100644 --- a/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/WalletManager.kt +++ b/composeApp/src/commonMain/kotlin/fr/acinq/phoenix/managers/WalletManager.kt @@ -18,7 +18,6 @@ package fr.acinq.phoenix.managers import fr.acinq.bitcoin.* import fr.acinq.lightning.crypto.Bip84OnChainKeys -import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.crypto.LocalKeyManager import fr.acinq.lightning.crypto.div import kotlinx.coroutines.CoroutineScope @@ -94,6 +93,16 @@ fun LocalKeyManager.cloudKey(): ByteVector32 { return derivePrivateKey(path).privateKey.value } +/** Key used to encrypt/decrypt blobs we store in the cloud. */ +fun LocalKeyManager.nostrPrivateKey(): PrivateKey { + val path = KeyPath(if (isMainnet()) "m/44'/1237'/0'/0/0" else "m/44'/1237'/1'/0/0") + return derivePrivateKey(path).privateKey +} + +fun LocalKeyManager.nostrPublicKey(): PublicKey { + return nostrPrivateKey().publicKey() +} + fun LocalKeyManager.cloudKeyHash(): String { return Crypto.hash160(cloudKey()).byteVector().toHex() } diff --git a/composeApp/src/iosMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.ios.kt b/composeApp/src/iosMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.ios.kt new file mode 100644 index 00000000..1629cf13 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/ac/cord/auxiliary/compose/extensions/Phoenix.ios.kt @@ -0,0 +1,39 @@ +package ac.cord.auxiliary.compose.extensions + +import co.touchlab.kermit.Logger +import fr.acinq.bitcoin.Chain +import fr.acinq.phoenix.PhoenixGlobal +import fr.acinq.phoenix.data.StartBusinessResult +import fr.acinq.phoenix.ios.BusinessManager +import fr.acinq.phoenix.managers.DataStoreManager +import fr.acinq.phoenix.utils.preferences.GlobalPrefs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + + +actual suspend fun platformStartupLogic( + words: List +): StartBusinessResult { + return withContext(Dispatchers.Main) { + BusinessManager.startNewBusiness(words, isHeadless = false) + } +} + +actual fun schedulePlatformLogic(phoenixGlobal: PhoenixGlobal) { + Logger.withTag("schedulePlatformLogic").e { "schedulePlatformLogic not implemented" } +} + +actual fun getShowIntroFlow(phoenixGlobal: PhoenixGlobal): Flow { + return DataStoreManager( + phoenixGlobal.ctx, + Chain.Mainnet + ).loadGlobalPrefsForWallet().getShowIntro +} + +actual fun getGlobalPrefs(phoenixGlobal: PhoenixGlobal): GlobalPrefs { + return DataStoreManager( + phoenixGlobal.ctx, + Chain.Mainnet + ).loadGlobalPrefsForWallet() +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.ios.kt b/composeApp/src/iosMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.ios.kt new file mode 100644 index 00000000..da93306d --- /dev/null +++ b/composeApp/src/iosMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.ios.kt @@ -0,0 +1,151 @@ +package ac.cord.auxiliary.compose.ui.view.model + +import ac.cord.auxiliary.compose.AppVersion +import co.touchlab.kermit.Logger +import fr.acinq.bitcoin.Chain +import fr.acinq.bitcoin.MnemonicCode +import fr.acinq.lightning.crypto.LocalKeyManager +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.phoenix.PhoenixGlobal +import fr.acinq.phoenix.data.DecryptSeedResult +import fr.acinq.phoenix.data.ElectrumConfig +import fr.acinq.phoenix.data.WalletId +import fr.acinq.phoenix.ios.BusinessManager +import fr.acinq.phoenix.managers.DataStoreManager +import fr.acinq.phoenix.managers.NodeParamsManager +import fr.acinq.phoenix.managers.SeedManager +import fr.acinq.phoenix.security.EncryptedSeed +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.launch +import kotlin.collections.plus + + +actual fun updateBusinessActiveInUI(walletId: WalletId) { + BusinessManager.updateBusinessActiveInUI(walletId) +} + +actual fun loadAndDecryptSeed(phoenixGlobal: PhoenixGlobal): DecryptSeedResult { + return SeedManager.loadAndDecrypt( + phoenixGlobal + ) +} + +actual fun getAvailableWalletsMeta(phoenixGlobal: PhoenixGlobal): Flow> { + return DataStoreManager( + phoenixGlobal.ctx, + chain = Chain.Mainnet + ).loadGlobalPrefsForWallet().getAvailableWalletsMeta +} + +actual suspend fun saveAvailableWalletMeta( + phoenixGlobal: PhoenixGlobal, + metadata: UserWalletMetadata +) { + DataStoreManager( + phoenixGlobal.ctx, + chain = Chain.Mainnet + ).loadGlobalPrefsForWallet().saveAvailableWalletMeta(metadata) +} + +actual suspend fun saveAvailableWalletMeta( + phoenixGlobal: PhoenixGlobal, + walletId: WalletId, + name: String?, + avatar: String, + isHidden: Boolean +) { + DataStoreManager( + phoenixGlobal.ctx, + chain = Chain.Mainnet + ).loadGlobalPrefsForWallet().saveAvailableWalletMeta( + walletId = walletId, + name = name, + avatar = avatar, + isHidden = isHidden + ) +} + +actual fun platformWriteSeed( + log: Logger, + phoenixGlobal: PhoenixGlobal, + globalPrefs: GlobalPrefs, + writingState: WritingSeedState, + viewModelScope: CoroutineScope, + mnemonics: List, + onWritingSeedError: (WritingSeedState.Error) -> Unit, + onWritingSeedStateWriting: (WritingSeedState.Writing) -> Unit, + isRestoringWallet: Boolean, + isTorEnabled: Boolean, + customElectrumServer: ElectrumConfig.Custom?, + onSeedWritten: (WalletId) -> Unit +) { + if (writingState !is WritingSeedState.Init) return + viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> + log.e("failed to write mnemonics to disk: ${e.message}") + onWritingSeedError.invoke( + WritingSeedState.Error.Generic(e) + ) + }) { + log.d("writing mnemonics to disk...") + + onWritingSeedStateWriting.invoke( + WritingSeedState.Writing(mnemonics) + ) + val existingSeeds = SeedManager.loadAndDecryptOrNull(phoenixGlobal)?.map { + it.key to it.value.words + }?.toMap() + + val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector() + val keyManager = LocalKeyManager(seed, NodeParamsManager.chain, NodeParamsManager.remoteSwapInXpub) + val newWalletId = WalletId(keyManager.nodeKeys.nodeKey.publicKey) + + when { + existingSeeds == null -> { + log.e("could not load the existing seed map, aborting...") + onWritingSeedError.invoke( + WritingSeedState.Error.CannotLoadSeedMap + ) + return@launch + } + existingSeeds.containsKey(newWalletId) -> { + log.i("attempting to import a seed that already exists, aborting...") + onWritingSeedError.invoke( + WritingSeedState.Error.SeedAlreadyExists + ) + return@launch + } + else -> { + val newSeedMap = existingSeeds + (newWalletId to mnemonics) + val encrypted = EncryptedSeed.V2.encrypt(newSeedMap) + SeedManager.writeSeedToDisk(phoenixGlobal, encrypted, overwrite = true) + onSeedWritten.invoke(newWalletId) + if (isRestoringWallet) { + log.i("successfully restored wallet=$newWalletId") + } else { + log.i("successfully created wallet=$newWalletId") + } + } + } + + globalPrefs.saveLastUsedAppCode(AppVersion.versionCode) + val dataStoreManager = DataStoreManager( + phoenixGlobal.ctx, + chain = NodeParamsManager.chain, + ) + val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = newWalletId) + userPrefs.saveIsTorEnabled(isTorEnabled) + userPrefs.saveElectrumServer(customElectrumServer) + + viewModelScope.launch(Dispatchers.Main) { + delay(1000) + onSeedWritten(newWalletId) + } + } +} \ No newline at end of file