diff --git a/composeApp/src/androidMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.android.kt b/composeApp/src/androidMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.android.kt index 13c67861..f1220cf8 100644 --- a/composeApp/src/androidMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.android.kt +++ b/composeApp/src/androidMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.android.kt @@ -114,7 +114,6 @@ actual fun platformWriteSeed( 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 { diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/UnsignedNostrEvent.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/UnsignedNostrEvent.kt index 99d3026b..cd57b8ba 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/UnsignedNostrEvent.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/UnsignedNostrEvent.kt @@ -43,9 +43,13 @@ data class UnsignedNostrEvent( other as UnsignedNostrEvent if (id != other.id) return false + if (pubKey != other.pubKey) return false if (kind != other.kind) return false if (!tags.contentDeepEquals(other.tags)) return false + if (privateTags == null != (other.privateTags == null)) return false + if (privateTags != null && !privateTags.contentDeepEquals(other.privateTags)) return false if (content != other.content) return false + if (signedAt != other.signedAt) return false if (createdAt != other.createdAt) return false if (updatedAt != other.updatedAt) return false if (savedAt != other.savedAt) return false @@ -57,9 +61,12 @@ data class UnsignedNostrEvent( override fun hashCode(): Int { var result = id.hashCode() + result = 31 * result + pubKey.hashCode() result = 31 * result + kind result = 31 * result + tags.contentDeepHashCode() + result = 31 * result + (privateTags?.contentDeepHashCode() ?: 0) result = 31 * result + content.hashCode() + result = 31 * result + (signedAt?.hashCode() ?: 0) result = 31 * result + createdAt.hashCode() result = 31 * result + updatedAt.hashCode() result = 31 * result + savedAt.hashCode() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt index efa90774..b810b155 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt @@ -175,7 +175,7 @@ class DatabaseNostrRepository( UnsignedNostrEvent( pubKey = publicKey, kind = SearchRelayListEvent.KIND, - tags = Relays.DefaultDMRelayList.map { + tags = Relays.DefaultSearchRelayList.map { RelayTag.assemble( it ) @@ -183,7 +183,6 @@ class DatabaseNostrRepository( .plus( SearchRelayListEvent.ALT_TAG ).toTypedArray(), - privateTags = emptyArray(), content = "" ) ) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/CreateProfileScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/CreateProfileScreen.kt index c03f7da5..72310562 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/CreateProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/CreateProfileScreen.kt @@ -42,7 +42,7 @@ fun CreateProfileScreen( onNavigateToEndThis: () -> Unit, nostrRepository: press.mantra.compose.repository.NostrRepository, marmotRepository: MarmotRepository, - writeSeed: (List) -> Unit + writeSeed: (mnemonics: List, onSeedWritten: () -> Unit, onSeedWriteError: () -> Unit) -> Unit ) { val createProfileViewModel: CreateProfileViewModel = viewModel ( factory = CreateProfileViewModel.factory( @@ -129,7 +129,7 @@ fun CreateProfileScreen( ), label = { Text( - text = "Name (eg. Alan Turin)", + text = "Name (eg. Alan Turing)", maxLines = 1, ) }, @@ -395,7 +395,7 @@ fun CreateAccountScreenPreview() { // ) // ), onNavigateToEndThis = {}, - writeSeed = {}, + writeSeed = { _, _, _ -> }, nostrRepository = press.mantra.compose.repository.NostrRepository.NO_OP_NOSTR_REPOSITORY, marmotRepository = MarmotRepository.NO_OP_MARMOT_KEY_PACKAGE_BUNDLE ) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt index 7a224b85..1a0c1236 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt @@ -368,11 +368,12 @@ fun MantraNavHost( }, nostrRepository = databaseNostrRepository, marmotRepository = databaseMarmotRepository, - writeSeed = { words -> + writeSeed = { words, onSeedWritten, onSeedWriteError -> sovereignWalletViewModel.writeSeed( words, isRestoringWallet = false, onSeedWritten = { walletId -> + onSeedWritten() sovereignWalletViewModel.loadSovereignData(walletId) sovereignWalletViewModel.listAvailableWallets { @@ -381,7 +382,8 @@ fun MantraNavHost( route = SovereignWalletStartupRoute ) } - } + }, + onSeedWriteError = { onSeedWriteError() } ) } ) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/CreateProfileViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/CreateProfileViewModel.kt index df62015d..96125e8e 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/CreateProfileViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/CreateProfileViewModel.kt @@ -12,7 +12,6 @@ import press.mantra.compose.repository.NostrRepository import press.mantra.compose.ui.view.state.CreateProfileUIState import press.mantra.compose.ui.view.state.form.CreateProfileFormState import co.touchlab.kermit.Logger -import fr.acinq.bitcoin.Chain import fr.acinq.bitcoin.MnemonicCode import fr.acinq.bitcoin.byteVector import fr.acinq.lightning.Lightning @@ -79,48 +78,67 @@ class CreateProfileViewModel( } + private fun failProfileCreation(e: Throwable? = null) { + if (e != null) { + logger.e("error when creating new profile: ", e) + } else { + logger.e { "seed write failed, aborting profile creation" } + } + createProfileUIState.value = CreateProfileUIState.Error + isActionPending.value = false + } + fun createAccount( - writeSeed: (List) -> Unit -// onNavigateToUnsignedProfile: (UnsignedProfileRoute) -> Unit, -// onNavigateToUnannouncedProfile: (UnannouncedProfileRoute) -> Unit + writeSeed: (mnemonics: List, onSeedWritten: () -> Unit, onSeedWriteError: () -> Unit) -> Unit ) { logger.d { "createAccount" } + if (isActionPending.value) return isActionPending.value = true viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> - logger.e("error when creating new wallet: ", e) - throw e + failProfileCreation(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 + chain = NodeParamsManager.chain, + remoteSwapInExtendedPublicKey = NodeParamsManager.remoteSwapInXpub ) val pubkey = localKeyManager.nostrPublicKey() logger.d("Pubkey: $pubkey" ) - nostrRepository.createNewProfile( - pubkey, - name = createProfileFormState.nameField.textFieldState.text.toString(), - biography = createProfileFormState.biographyField.textFieldState.text.toString(), - onCompletion = { - logger.d("Created: $pubkey" ) - marmotRepository.publishMarmotKeyPackageBundle( - publicKey = pubkey, - nsecPassword = localKeyManager.nsecPassword() - ) - } + // Only create the profile events once the seed backing this pubkey is safely + // on disk; otherwise the notary could never sign them. + writeSeed( + mnemonics, + { + viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> + failProfileCreation(e) + }) { + nostrRepository.createNewProfile( + pubkey, + name = createProfileFormState.nameField.textFieldState.text.toString(), + biography = createProfileFormState.biographyField.textFieldState.text.toString(), + onCompletion = { + logger.d("Created: $pubkey" ) + + marmotRepository.publishMarmotKeyPackageBundle( + publicKey = pubkey, + nsecPassword = localKeyManager.nsecPassword() + ) + } + ) + } + }, + { failProfileCreation() } ) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt index 56905778..3a653777 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/SovereignWalletViewModel.kt @@ -234,8 +234,13 @@ class SovereignWalletViewModel( fun writeSeed( mnemonics: List, isRestoringWallet: Boolean, - onSeedWritten: (WalletId) -> Unit + onSeedWritten: (WalletId) -> Unit, + onSeedWriteError: (WritingSeedState.Error) -> Unit = {} ) { + if (writingState is WritingSeedState.Error || writingState is WritingSeedState.WrittenToDisk) { + // A previous attempt finished; reset so platformWriteSeed does not silently skip this one. + writingState = WritingSeedState.Init + } platformWriteSeed( log = log, phoenixGlobal = phoenixGlobal, @@ -245,6 +250,7 @@ class SovereignWalletViewModel( mnemonics = mnemonics, onWritingSeedError = { writingSeedStateError -> writingState = writingSeedStateError + onSeedWriteError(writingSeedStateError) }, onWritingSeedStateWriting = { writing -> writingState = writing @@ -253,6 +259,7 @@ class SovereignWalletViewModel( isTorEnabled = isTorEnabled.value, customElectrumServer = customElectrumServer.value, onSeedWritten = { walletId -> + writingState = WritingSeedState.WrittenToDisk(walletId) onSeedWritten(walletId) } ) diff --git a/composeApp/src/iosMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.ios.kt b/composeApp/src/iosMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.ios.kt index 6399e2ea..6c04560d 100644 --- a/composeApp/src/iosMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.ios.kt +++ b/composeApp/src/iosMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.ios.kt @@ -124,7 +124,6 @@ actual fun platformWriteSeed( 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 {