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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = ""
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user