feat(wallet): store Cardano address in Matrix account data for discovery
Implements public Cardano address directory using Matrix account data:
Publishing (write side):
- After wallet creation, import, or SSSS restore, the Cardano address
is written to the user Matrix account data
- Key: com.sulkta.cardano.address
- Content: { "address": "addr1..." }
- This is public/unencrypted for discovery by other users
Lookup (read side):
- When entering a Matrix user in /pay, their account data is checked
- If they have a linked Cardano address, it auto-fills the recipient
- UI shows "Address loaded from @username profile ✓" when found
- Shows "@username has not linked a wallet" if not found
- Graceful fallback to manual address entry
New files:
- CardanoAddressService interface (wallet:api)
- DefaultCardanoAddressService implementation (wallet:impl)
Updated:
- WalletSetupPresenter: calls publishAddress after all wallet setup paths
- PaymentEntryPresenter: looks up recipient address from Matrix
- PaymentEntryState: added Resolving and Found states
- PaymentEntryView: shows lookup progress and result cards
This commit is contained in:
parent
699807e1bd
commit
c35289a3bd
6 changed files with 390 additions and 16 deletions
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.api.address
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
/**
|
||||
* Service for managing Cardano addresses in Matrix account data.
|
||||
*
|
||||
* This allows users to publish their Cardano address so other users can
|
||||
* look it up for payments - like a public address directory baked into Matrix.
|
||||
*
|
||||
* Account data key: `com.sulkta.cardano.address`
|
||||
* Content format: `{ "address": "addr1..." }`
|
||||
*/
|
||||
interface CardanoAddressService {
|
||||
/**
|
||||
* Publish the user's Cardano address to their Matrix account data.
|
||||
* This is public data, not encrypted.
|
||||
*
|
||||
* @param address The Cardano address to publish
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
suspend fun publishAddress(address: String): Result<Unit>
|
||||
|
||||
/**
|
||||
* Look up another user's Cardano address from their Matrix account data.
|
||||
*
|
||||
* @param userId The Matrix user ID to look up
|
||||
* @return The user's Cardano address if published, null if not found
|
||||
*/
|
||||
suspend fun lookupAddress(userId: UserId): Result<String?>
|
||||
|
||||
companion object {
|
||||
const val ACCOUNT_DATA_TYPE = "com.sulkta.cardano.address"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a Cardano address lookup.
|
||||
*/
|
||||
sealed interface AddressLookupResult {
|
||||
/** Address was found and retrieved successfully */
|
||||
data class Found(val address: String, val userId: UserId) : AddressLookupResult
|
||||
|
||||
/** User has no Cardano address linked */
|
||||
data class NotLinked(val userId: UserId) : AddressLookupResult
|
||||
|
||||
/** Lookup failed due to an error */
|
||||
data class Error(val userId: UserId, val message: String) : AddressLookupResult
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.impl.address
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.wallet.api.address.CardanoAddressService
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Implementation of [CardanoAddressService] that stores Cardano addresses
|
||||
* in Matrix account data for public discovery.
|
||||
*/
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultCardanoAddressService @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val sessionStore: SessionStore,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : CardanoAddressService {
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private val httpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class CardanoAddressData(
|
||||
val address: String
|
||||
)
|
||||
|
||||
override suspend fun publishAddress(address: String): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val sessionData = sessionStore.getSession(matrixClient.sessionId.value)
|
||||
?: throw IllegalStateException("No session found")
|
||||
|
||||
val userId = matrixClient.sessionId.value
|
||||
val url = "${sessionData.homeserverUrl}/_matrix/client/v3/user/$userId/account_data/${CardanoAddressService.ACCOUNT_DATA_TYPE}"
|
||||
|
||||
val body = json.encodeToString(CardanoAddressData(address))
|
||||
.toRequestBody("application/json".toMediaType())
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.put(body)
|
||||
.addHeader("Authorization", "Bearer ${sessionData.accessToken}")
|
||||
.build()
|
||||
|
||||
Timber.d("Publishing Cardano address to Matrix account data...")
|
||||
|
||||
val response = httpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
val errorBody = response.body?.string() ?: "Unknown error"
|
||||
throw RuntimeException("Failed to publish address: ${response.code} - $errorBody")
|
||||
}
|
||||
|
||||
Timber.i("Successfully published Cardano address to Matrix account data")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun lookupAddress(userId: UserId): Result<String?> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val sessionData = sessionStore.getSession(matrixClient.sessionId.value)
|
||||
?: throw IllegalStateException("No session found")
|
||||
|
||||
val url = "${sessionData.homeserverUrl}/_matrix/client/v3/user/${userId.value}/account_data/${CardanoAddressService.ACCOUNT_DATA_TYPE}"
|
||||
|
||||
Timber.d("Looking up Cardano address for ${userId.value}...")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.addHeader("Authorization", "Bearer ${sessionData.accessToken}")
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).execute()
|
||||
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val responseBody = response.body?.string()
|
||||
if (responseBody != null) {
|
||||
val data = json.decodeFromString<CardanoAddressData>(responseBody)
|
||||
Timber.i("Found Cardano address for ${userId.value}: ${data.address.take(20)}...")
|
||||
data.address
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
404 -> {
|
||||
Timber.d("No Cardano address found for ${userId.value}")
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
val errorBody = response.body?.string() ?: "Unknown error"
|
||||
throw RuntimeException("Failed to lookup address: ${response.code} - $errorBody")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import dev.zacsweers.metro.Assisted
|
|||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.wallet.api.CardanoClient
|
||||
import io.element.android.features.wallet.api.address.CardanoAddressService
|
||||
import io.element.android.features.wallet.impl.cardano.CardanoNetwork
|
||||
import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig
|
||||
import io.element.android.features.wallet.impl.cardano.CardanoWalletManager
|
||||
|
|
@ -25,6 +26,8 @@ import io.element.android.features.wallet.impl.slash.ParsedPayCommand
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -36,6 +39,7 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
private val matrixClient: MatrixClient,
|
||||
private val walletManager: CardanoWalletManager,
|
||||
private val cardanoClient: CardanoClient,
|
||||
private val cardanoAddressService: CardanoAddressService,
|
||||
) : Presenter<PaymentEntryState> {
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -44,6 +48,7 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PaymentEntryPresenter"
|
||||
private const val LOVELACE_PER_ADA = 1_000_000L
|
||||
private const val MIN_AMOUNT_LOVELACE = 1_000_000L
|
||||
private const val MAX_ADA_SUPPLY = 45_000_000_000L
|
||||
|
|
@ -100,6 +105,8 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
var senderAddress by remember { mutableStateOf<String?>(null) }
|
||||
var senderBalanceLovelace by remember { mutableStateOf<Lovelace?>(null) }
|
||||
var recipientResolutionState by remember { mutableStateOf<RecipientResolutionState>(RecipientResolutionState.NotNeeded) }
|
||||
// Track resolved address separately so we can use it for validation
|
||||
var resolvedCardanoAddress by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(walletInitialized) {
|
||||
if (walletInitialized) {
|
||||
|
|
@ -113,26 +120,69 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Look up Cardano address when a Matrix user is entered
|
||||
LaunchedEffect(recipientInput) {
|
||||
val isMatrixUser = MATRIX_USER_REGEX.matches(recipientInput)
|
||||
val isCardanoAddress = CARDANO_ADDRESS_REGEX.matches(recipientInput)
|
||||
|
||||
when {
|
||||
recipientInput.isBlank() -> {
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded
|
||||
resolvedCardanoAddress = null
|
||||
}
|
||||
isCardanoAddress -> {
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded
|
||||
resolvedCardanoAddress = recipientInput
|
||||
}
|
||||
isMatrixUser -> {
|
||||
// Start lookup
|
||||
recipientResolutionState = RecipientResolutionState.Resolving(recipientInput)
|
||||
resolvedCardanoAddress = null
|
||||
|
||||
Timber.tag(TAG).d("Looking up Cardano address for $recipientInput...")
|
||||
|
||||
val userId = UserId(recipientInput)
|
||||
cardanoAddressService.lookupAddress(userId)
|
||||
.onSuccess { address ->
|
||||
if (address != null) {
|
||||
Timber.tag(TAG).i("Found Cardano address for $recipientInput")
|
||||
recipientResolutionState = RecipientResolutionState.Found(
|
||||
matrixUserId = recipientInput,
|
||||
address = address
|
||||
)
|
||||
resolvedCardanoAddress = address
|
||||
} else {
|
||||
Timber.tag(TAG).d("No Cardano address linked for $recipientInput")
|
||||
recipientResolutionState = RecipientResolutionState.NeedsManualEntry(
|
||||
matrixUserId = recipientInput,
|
||||
displayName = null
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
Timber.tag(TAG).w(e, "Failed to lookup address for $recipientInput")
|
||||
recipientResolutionState = RecipientResolutionState.NeedsManualEntry(
|
||||
matrixUserId = recipientInput,
|
||||
displayName = null
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
recipientResolutionState = RecipientResolutionState.NotNeeded
|
||||
resolvedCardanoAddress = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val parsedAmountLovelace = parseAmountInput(amountInput)
|
||||
val amountError = validateAmount(parsedAmountLovelace, amountInput)
|
||||
|
||||
val isCardanoAddress = CARDANO_ADDRESS_REGEX.matches(recipientInput)
|
||||
val isMatrixUser = MATRIX_USER_REGEX.matches(recipientInput)
|
||||
val recipientError = validateRecipient(recipientInput, isCardanoAddress, isMatrixUser)
|
||||
val recipientError = validateRecipient(recipientInput, isCardanoAddress, isMatrixUser, recipientResolutionState)
|
||||
|
||||
LaunchedEffect(recipientInput, isMatrixUser, isCardanoAddress) {
|
||||
recipientResolutionState = when {
|
||||
recipientInput.isBlank() -> RecipientResolutionState.NotNeeded
|
||||
isCardanoAddress -> RecipientResolutionState.NotNeeded
|
||||
isMatrixUser -> RecipientResolutionState.NeedsManualEntry(
|
||||
matrixUserId = recipientInput,
|
||||
displayName = null
|
||||
)
|
||||
else -> RecipientResolutionState.NotNeeded
|
||||
}
|
||||
}
|
||||
|
||||
val isValidRecipient = isCardanoAddress
|
||||
// Recipient is valid if we have a direct Cardano address or a resolved one from Matrix lookup
|
||||
val isValidRecipient = isCardanoAddress || resolvedCardanoAddress != null
|
||||
val canContinue = parsedAmountLovelace != null &&
|
||||
parsedAmountLovelace >= MIN_AMOUNT_LOVELACE &&
|
||||
amountError == null &&
|
||||
|
|
@ -142,7 +192,11 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
fun handleEvent(event: PaymentFlowEvents) {
|
||||
when (event) {
|
||||
is PaymentFlowEvents.AmountChanged -> amountInput = event.amount
|
||||
is PaymentFlowEvents.RecipientChanged -> recipientInput = event.recipient
|
||||
is PaymentFlowEvents.RecipientChanged -> {
|
||||
recipientInput = event.recipient
|
||||
// Clear resolved address when input changes
|
||||
resolvedCardanoAddress = null
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -205,8 +259,25 @@ class PaymentEntryPresenter @AssistedInject constructor(
|
|||
return null
|
||||
}
|
||||
|
||||
private fun validateRecipient(input: String, isCardanoAddress: Boolean, isMatrixUser: Boolean): String? {
|
||||
private fun validateRecipient(
|
||||
input: String,
|
||||
isCardanoAddress: Boolean,
|
||||
isMatrixUser: Boolean,
|
||||
resolutionState: RecipientResolutionState
|
||||
): String? {
|
||||
if (input.isBlank()) return null
|
||||
|
||||
// Matrix user with ongoing resolution
|
||||
if (isMatrixUser) {
|
||||
return when (resolutionState) {
|
||||
is RecipientResolutionState.Resolving -> null // Still looking up
|
||||
is RecipientResolutionState.Found -> null // Found address
|
||||
is RecipientResolutionState.NeedsManualEntry -> "${resolutionState.matrixUserId} hasn't linked a Cardano wallet"
|
||||
is RecipientResolutionState.Error -> resolutionState.message
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCardanoAddress && !isMatrixUser) {
|
||||
return "Enter a Cardano address (addr1...) or Matrix user (@user:server)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,25 @@ data class PaymentEntryState(
|
|||
* State of resolving a Matrix user ID to a Cardano address.
|
||||
*/
|
||||
sealed interface RecipientResolutionState {
|
||||
/** Not a Matrix user ID - no resolution needed. */
|
||||
data object NotNeeded : RecipientResolutionState
|
||||
|
||||
/** Currently looking up the user's Cardano address. */
|
||||
data class Resolving(val matrixUserId: String) : RecipientResolutionState
|
||||
|
||||
/** Found the user's Cardano address from their Matrix profile. */
|
||||
data class Found(
|
||||
val matrixUserId: String,
|
||||
val address: String,
|
||||
val displayName: String? = null
|
||||
) : RecipientResolutionState
|
||||
|
||||
/** User has no Cardano address linked - needs manual entry. */
|
||||
data class NeedsManualEntry(val matrixUserId: String, val displayName: String?) : RecipientResolutionState
|
||||
|
||||
/** Successfully resolved to a Cardano address (manual entry or from lookup). */
|
||||
data class Resolved(val address: String) : RecipientResolutionState
|
||||
|
||||
/** Failed to look up address. */
|
||||
data class Error(val message: String) : RecipientResolutionState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
|
|
@ -205,7 +206,17 @@ private fun PaymentFormContent(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
// Show resolution state feedback
|
||||
when (val resolution = state.recipientResolutionState) {
|
||||
is RecipientResolutionState.Resolving -> {
|
||||
AddressLookupInProgressCard(matrixUserId = resolution.matrixUserId)
|
||||
}
|
||||
is RecipientResolutionState.Found -> {
|
||||
AddressFoundCard(
|
||||
matrixUserId = resolution.matrixUserId,
|
||||
address = resolution.address,
|
||||
)
|
||||
}
|
||||
is RecipientResolutionState.NeedsManualEntry -> {
|
||||
MatrixUserNeedsAddressCard(
|
||||
matrixUserId = resolution.matrixUserId,
|
||||
|
|
@ -266,6 +277,66 @@ private fun BalanceInfoCard(balanceAda: String, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressLookupInProgressCard(matrixUserId: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
Text(
|
||||
text = "Looking up address for $matrixUserId...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressFoundCard(matrixUserId: String, address: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Check(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
val displayName = matrixUserId.substringBefore(":").removePrefix("@")
|
||||
Text(
|
||||
text = "Address loaded from $displayName's profile",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
}
|
||||
// Show truncated address
|
||||
val truncatedAddress = if (address.length > 24) {
|
||||
"${address.take(12)}...${address.takeLast(8)}"
|
||||
} else {
|
||||
address
|
||||
}
|
||||
Text(
|
||||
text = truncatedAddress,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MatrixUserNeedsAddressCard(matrixUserId: String, displayName: String?, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
|
|
@ -296,6 +367,15 @@ internal class PaymentEntryStateProvider : PreviewParameterProvider<PaymentEntry
|
|||
senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true,
|
||||
amountError = null, recipientError = null, canContinue = false, eventSink = {},
|
||||
),
|
||||
// Address found from Matrix
|
||||
PaymentEntryState(
|
||||
noWalletSetup = false, isCheckingWallet = false,
|
||||
amountInput = "10", recipientInput = "@alice:matrix.org", prefillAmount = null, prefillRecipient = null,
|
||||
parsedAmountLovelace = 10_000_000L, isValidRecipient = true,
|
||||
recipientResolutionState = RecipientResolutionState.Found("@alice:matrix.org", "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer"),
|
||||
senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true,
|
||||
amountError = null, recipientError = null, canContinue = true, eventSink = {},
|
||||
),
|
||||
// No wallet state
|
||||
PaymentEntryState(
|
||||
noWalletSetup = true, isCheckingWallet = false,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.wallet.api.address.CardanoAddressService
|
||||
import io.element.android.features.wallet.api.backup.WalletBackupService
|
||||
import io.element.android.features.wallet.api.storage.CardanoKeyStorage
|
||||
import io.element.android.features.wallet.impl.cardano.CardanoWalletManager
|
||||
|
|
@ -28,6 +29,7 @@ class WalletSetupPresenter @Inject constructor(
|
|||
private val walletManager: CardanoWalletManager,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val walletBackupService: WalletBackupService,
|
||||
private val cardanoAddressService: CardanoAddressService,
|
||||
) : Presenter<WalletSetupState> {
|
||||
|
||||
companion object {
|
||||
|
|
@ -35,6 +37,20 @@ class WalletSetupPresenter @Inject constructor(
|
|||
private val VALID_WORD_COUNTS = listOf(12, 15, 18, 21, 24)
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the Cardano address to Matrix account data for discovery.
|
||||
* This is fire-and-forget - we don't fail the wallet setup if publishing fails.
|
||||
*/
|
||||
private suspend fun publishAddressToMatrix(address: String) {
|
||||
cardanoAddressService.publishAddress(address)
|
||||
.onSuccess {
|
||||
Timber.tag(TAG).i("Published Cardano address to Matrix account data")
|
||||
}
|
||||
.onFailure { e ->
|
||||
Timber.tag(TAG).w(e, "Failed to publish Cardano address (non-fatal)")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): WalletSetupState {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -144,6 +160,8 @@ class WalletSetupPresenter @Inject constructor(
|
|||
generatedMnemonic = words
|
||||
generatedAddress = address
|
||||
isImporting = false
|
||||
// Publish address to Matrix for discovery
|
||||
publishAddressToMatrix(address)
|
||||
// Skip to address confirmation (no backup prompt for imported wallets
|
||||
// since user already has their phrase)
|
||||
step = SetupStep.SHOW_ADDRESS
|
||||
|
|
@ -192,6 +210,8 @@ class WalletSetupPresenter @Inject constructor(
|
|||
generatedMnemonic = mnemonic
|
||||
generatedAddress = address
|
||||
isRestoringFromCloud = false
|
||||
// Publish address to Matrix for discovery
|
||||
publishAddressToMatrix(address)
|
||||
// Go directly to address confirmation
|
||||
step = SetupStep.SHOW_ADDRESS
|
||||
}
|
||||
|
|
@ -232,6 +252,8 @@ class WalletSetupPresenter @Inject constructor(
|
|||
hasConfirmedBackup = true
|
||||
step = SetupStep.COMPLETE
|
||||
scope.launch {
|
||||
// Publish address to Matrix for discovery
|
||||
generatedAddress?.let { publishAddressToMatrix(it) }
|
||||
walletManager.initialize(sessionId)
|
||||
}
|
||||
}
|
||||
|
|
@ -256,6 +278,8 @@ class WalletSetupPresenter @Inject constructor(
|
|||
isBackingUp = false
|
||||
hasConfirmedBackup = true
|
||||
step = SetupStep.COMPLETE
|
||||
// Publish address to Matrix for discovery
|
||||
generatedAddress?.let { publishAddressToMatrix(it) }
|
||||
walletManager.initialize(sessionId)
|
||||
}
|
||||
.onFailure { e ->
|
||||
|
|
@ -276,6 +300,8 @@ class WalletSetupPresenter @Inject constructor(
|
|||
hasConfirmedBackup = true
|
||||
step = SetupStep.COMPLETE
|
||||
scope.launch {
|
||||
// Publish address to Matrix for discovery
|
||||
generatedAddress?.let { publishAddressToMatrix(it) }
|
||||
walletManager.initialize(sessionId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue