Phase 3: Wallet panel UI and full /pay flow wiring

- Add WalletPanelView with 4 tabs (Overview, Assets, History, Settings)
- Overview tab shows balance, QR code for receiving, and Send ADA button
- Assets tab shows native tokens held at address
- History tab shows recent transactions with explorer links
- Settings tab shows address, network, and backup/delete options

- Add NativeAsset and TxSummary models to wallet API
- Add getAddressAssets() and getAddressTransactions() to CardanoClient
- Implement new methods in KoiosCardanoClient and FakeCardanoClient

- Add wallet button to MessagesViewTopBar (DM rooms only)
- Add isDmRoom to MessagesState for conditional UI
- Wire navigateToWallet() callback through to MessagesFlowNode
- Add NavTarget.WalletPanel and WalletPanelNode integration

- Add string resources for wallet panel UI

Known limitations:
- Uses Chart icon as placeholder for wallet (Compound lacks wallet icon)
- Wallet setup flow not implemented (TODO)
- Transaction amounts in history need additional API calls to calculate
This commit is contained in:
Kayos 2026-03-28 09:23:58 -07:00
parent b867fa783e
commit e33c87c164
24 changed files with 1685 additions and 1 deletions

View file

@ -53,4 +53,21 @@ interface CardanoClient {
* @return Current [ProtocolParameters] from the latest epoch
*/
suspend fun getProtocolParameters(): Result<ProtocolParameters>
/**
* Get native assets (tokens) for a given address.
*
* @param address Bech32 Cardano address
* @return List of [NativeAsset] objects
*/
suspend fun getAddressAssets(address: String): Result<List<NativeAsset>>
/**
* Get transaction history for a given address.
*
* @param address Bech32 Cardano address
* @param limit Maximum number of transactions to return (default 20)
* @return List of [TxSummary] objects, most recent first
*/
suspend fun getAddressTransactions(address: String, limit: Int = 20): Result<List<TxSummary>>
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.api
/**
* Represents a native asset (token) on Cardano.
*
* @property policyId The minting policy ID (hex)
* @property assetName The asset name (hex or decoded)
* @property quantity The amount of this asset
* @property displayName Human-readable name if available
* @property fingerprint The asset fingerprint (CIP-14)
*/
data class NativeAsset(
val policyId: String,
val assetName: String,
val quantity: Long,
val displayName: String?,
val fingerprint: String?,
) {
/**
* Truncated policy ID for display.
*/
val truncatedPolicyId: String
get() = if (policyId.length > 16) {
"${policyId.take(8)}...${policyId.takeLast(8)}"
} else {
policyId
}
/**
* Display name, falling back to truncated asset name.
*/
val name: String
get() = displayName ?: assetName.takeIf { it.isNotEmpty() }?.let {
// Try to decode hex to ASCII if it looks printable
try {
val decoded = it.chunked(2).map { hex -> hex.toInt(16).toChar() }.joinToString("")
if (decoded.all { c -> c.isLetterOrDigit() || c in " -_" }) decoded else it
} catch (_: Exception) {
it
}
} ?: "Unknown"
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.api
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
/**
* Summary of a Cardano transaction for history display.
*
* @property txHash The transaction hash
* @property blockTime Unix timestamp when the tx was included in a block
* @property totalOutput Total output in lovelace
* @property fee Transaction fee in lovelace
* @property direction Whether this was sent or received
*/
data class TxSummary(
val txHash: String,
val blockTime: Long,
val totalOutput: Long,
val fee: Long,
val direction: Direction,
) {
enum class Direction {
SENT,
RECEIVED,
}
/**
* Formatted date for display.
*/
val formattedDate: String
get() = try {
val instant = Instant.ofEpochSecond(blockTime)
val formatter = DateTimeFormatter.ofPattern("MMM d, yyyy")
.withZone(ZoneId.systemDefault())
formatter.format(instant)
} catch (_: Exception) {
"Unknown date"
}
/**
* Truncated tx hash for display.
*/
val truncatedTxHash: String
get() = if (txHash.length > 16) {
"${txHash.take(8)}...${txHash.takeLast(8)}"
} else {
txHash
}
/**
* Amount formatted as ADA.
*/
val amountAda: String
get() {
val ada = totalOutput / 1_000_000.0
return if (ada == ada.toLong().toDouble()) {
"${ada.toLong()} ADA"
} else {
val formatted = "%.6f".format(ada).trimEnd('0').trimEnd('.')
"$formatted ADA"
}
}
/**
* Explorer URL for this transaction.
*/
fun explorerUrl(isTestnet: Boolean): String {
return if (isTestnet) {
"https://preprod.cardanoscan.io/transaction/$txHash"
} else {
"https://cardanoscan.io/transaction/$txHash"
}
}
}