Fix profile creation bugs

- platformWriteSeed (Android + iOS) invoked onSeedWritten twice on
  success, doubling wallet switch/navigation and navigating from the
  IO dispatcher; keep only the main-thread invocation.
- Gate profile event creation on the seed actually being written to
  disk: writeSeed now reports success/error, so a failed seed write no
  longer leaves orphaned unsigned events the notary can never sign.
- Stop rethrowing from createAccount's CoroutineExceptionHandler
  (crashed the app); failures now show the Error state and reset the
  pending flag instead of spinning forever. Add a re-entry guard
  against double taps.
- Reset WritingSeedState after a completed attempt so retries are not
  silently skipped, and record WrittenToDisk on success.
- Derive the nostr key with NodeParamsManager.chain instead of a
  hardcoded Chain.Mainnet.
- Build the SearchRelayListEvent from DefaultSearchRelayList instead
  of DM relays, and drop its empty privateTags array that caused a
  pointless NIP-44 encrypted empty list in content.
- Compare pubKey, privateTags and signedAt in UnsignedNostrEvent
  equals/hashCode so distinctUntilChanged cannot conflate rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-07-27 01:09:50 +02:00
parent 5e92e7f613
commit 0503464f6a
8 changed files with 62 additions and 31 deletions

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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 = ""
)
)

View File

@@ -42,7 +42,7 @@ fun CreateProfileScreen(
onNavigateToEndThis: () -> Unit,
nostrRepository: press.mantra.compose.repository.NostrRepository,
marmotRepository: MarmotRepository,
writeSeed: (List<String>) -> Unit
writeSeed: (mnemonics: List<String>, 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
)

View File

@@ -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() }
)
}
)

View File

@@ -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<String>) -> Unit
// onNavigateToUnsignedProfile: (UnsignedProfileRoute) -> Unit,
// onNavigateToUnannouncedProfile: (UnannouncedProfileRoute) -> Unit
writeSeed: (mnemonics: List<String>, 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() }
)
}
}

View File

@@ -234,8 +234,13 @@ class SovereignWalletViewModel(
fun writeSeed(
mnemonics: List<String>,
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)
}
)

View File

@@ -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 {