diff --git a/app/src/main/res/xml/auto_backup_config.xml b/app/src/main/res/xml/auto_backup_config.xml
index 58d978da5..9cb0dfb6f 100644
--- a/app/src/main/res/xml/auto_backup_config.xml
+++ b/app/src/main/res/xml/auto_backup_config.xml
@@ -3,4 +3,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/auto_backup_config_android_12.xml b/app/src/main/res/xml/auto_backup_config_android_12.xml
index 481bbaea6..7a8cee609 100644
--- a/app/src/main/res/xml/auto_backup_config_android_12.xml
+++ b/app/src/main/res/xml/auto_backup_config_android_12.xml
@@ -4,5 +4,8 @@
+
\ No newline at end of file
diff --git a/preference-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/preference/api/PreferenceProvider.kt b/preference-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/preference/api/PreferenceProvider.kt
index c68615ccf..c21b67b51 100644
--- a/preference-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/preference/api/PreferenceProvider.kt
+++ b/preference-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/preference/api/PreferenceProvider.kt
@@ -11,6 +11,11 @@ interface PreferenceProvider {
value: String?
)
+ suspend fun putStringSet(
+ key: PreferenceKey,
+ value: Set?
+ )
+
suspend fun putLong(
key: PreferenceKey,
value: Long?
@@ -20,6 +25,8 @@ interface PreferenceProvider {
suspend fun getString(key: PreferenceKey): String?
+ suspend fun getStringSet(key: PreferenceKey): Set?
+
fun observe(key: PreferenceKey): Flow
suspend fun clearPreferences(): Boolean
diff --git a/preference-api-lib/src/commonTest/kotlin/co/electriccoin/zcash/preference/test/MockPreferenceProvider.kt b/preference-api-lib/src/commonTest/kotlin/co/electriccoin/zcash/preference/test/MockPreferenceProvider.kt
index a4f065e35..7a2267b47 100644
--- a/preference-api-lib/src/commonTest/kotlin/co/electriccoin/zcash/preference/test/MockPreferenceProvider.kt
+++ b/preference-api-lib/src/commonTest/kotlin/co/electriccoin/zcash/preference/test/MockPreferenceProvider.kt
@@ -15,6 +15,10 @@ class MockPreferenceProvider(
override suspend fun getString(key: PreferenceKey) = map[key.key]
+ override suspend fun getStringSet(key: PreferenceKey): Set? {
+ TODO("Not yet implemented")
+ }
+
// For the mock implementation, does not support observability of changes
override fun observe(key: PreferenceKey): Flow = flow { emit(getString(key)) }
@@ -32,6 +36,13 @@ class MockPreferenceProvider(
map[key.key] = value
}
+ override suspend fun putStringSet(
+ key: PreferenceKey,
+ value: Set?
+ ) {
+ TODO("Not yet implemented")
+ }
+
override suspend fun putLong(
key: PreferenceKey,
value: Long?
diff --git a/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt b/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt
index e273c0b50..370dceefe 100644
--- a/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt
+++ b/preference-impl-android-lib/src/main/java/co/electriccoin/zcash/preference/AndroidPreferenceProvider.kt
@@ -59,6 +59,22 @@ class AndroidPreferenceProvider private constructor(
}
}
+ @SuppressLint("ApplySharedPref")
+ override suspend fun putStringSet(
+ key: PreferenceKey,
+ value: Set?
+ ) = withContext(dispatcher) {
+ mutex.withLock {
+ val editor = sharedPreferences.edit()
+
+ editor.putStringSet(key.key, value)
+
+ editor.commit()
+
+ Unit
+ }
+ }
+
@SuppressLint("ApplySharedPref")
override suspend fun putLong(
key: PreferenceKey,
@@ -91,6 +107,11 @@ class AndroidPreferenceProvider private constructor(
sharedPreferences.getString(key.key, null)
}
+ override suspend fun getStringSet(key: PreferenceKey): Set? =
+ withContext(dispatcher) {
+ sharedPreferences.getStringSet(key.key, null)
+ }
+
@SuppressLint("ApplySharedPref")
override suspend fun clearPreferences() =
withContext(dispatcher) {
diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt
index b4d3c24e2..c850032c1 100644
--- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt
+++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiBottomBar.kt
@@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
@@ -22,6 +23,7 @@ import co.electriccoin.zcash.ui.design.util.stringRes
fun ZashiBottomBar(
isElevated: Boolean,
modifier: Modifier = Modifier,
+ contentPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable ColumnScope.() -> Unit,
) {
Surface(
@@ -29,7 +31,9 @@ fun ZashiBottomBar(
color = ZashiColors.Surfaces.bgPrimary,
modifier = modifier,
) {
- Column {
+ Column(
+ modifier = Modifier.padding(contentPadding),
+ ) {
Spacer(modifier = Modifier.height(16.dp))
content()
Spacer(modifier = Modifier.height(24.dp))
diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt
index 19dd50a4f..afc0b8ec6 100644
--- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt
+++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt
@@ -36,13 +36,17 @@ fun Modifier.scaffoldScrollPadding(
)
@Stable
-fun PaddingValues.asScaffoldPaddingValues() =
- PaddingValues(
- top = calculateTopPadding() + ZashiDimensions.Spacing.spacingLg,
- bottom = calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl,
- start = ZashiDimensions.Spacing.spacing3xl,
- end = ZashiDimensions.Spacing.spacing3xl
- )
+fun PaddingValues.asScaffoldPaddingValues(
+ top: Dp = calculateTopPadding() + ZashiDimensions.Spacing.spacingLg,
+ bottom: Dp = calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl,
+ start: Dp = ZashiDimensions.Spacing.spacing3xl,
+ end: Dp = ZashiDimensions.Spacing.spacing3xl
+) = PaddingValues(
+ top = top,
+ bottom = bottom,
+ start = start,
+ end = end,
+)
@Stable
fun PaddingValues.asScaffoldScrollPaddingValues(
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/MetadataDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/MetadataDataSource.kt
index 2f511f9e8..b1766333a 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/MetadataDataSource.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/MetadataDataSource.kt
@@ -1,6 +1,5 @@
package co.electriccoin.zcash.ui.common.datasource
-import cash.z.ecc.android.sdk.model.AccountUuid
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.model.AccountMetadata
import co.electriccoin.zcash.ui.common.model.AnnotationMetadata
@@ -8,6 +7,7 @@ import co.electriccoin.zcash.ui.common.model.BookmarkMetadata
import co.electriccoin.zcash.ui.common.model.Metadata
import co.electriccoin.zcash.ui.common.provider.MetadataProvider
import co.electriccoin.zcash.ui.common.provider.MetadataStorageProvider
+import co.electriccoin.zcash.ui.common.serialization.METADATA_SERIALIZATION_V1
import co.electriccoin.zcash.ui.common.serialization.metada.MetadataKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
@@ -20,26 +20,22 @@ interface MetadataDataSource {
suspend fun flipTxAsBookmarked(
txId: String,
- account: AccountUuid,
key: MetadataKey
): Metadata
suspend fun createOrUpdateTxNote(
txId: String,
note: String,
- account: AccountUuid,
key: MetadataKey
): Metadata
suspend fun deleteTxNote(
txId: String,
- account: AccountUuid,
key: MetadataKey
): Metadata
suspend fun markTxMemoAsRead(
txId: String,
- account: AccountUuid,
key: MetadataKey
): Metadata
@@ -47,8 +43,6 @@ interface MetadataDataSource {
metadata: Metadata,
key: MetadataKey
)
-
- suspend fun resetMetadata()
}
@Suppress("TooManyFunctions")
@@ -56,8 +50,6 @@ class MetadataDataSourceImpl(
private val metadataStorageProvider: MetadataStorageProvider,
private val metadataProvider: MetadataProvider,
) : MetadataDataSource {
- private var metadata: Metadata? = null
-
private val mutex = Mutex()
override suspend fun getMetadata(key: MetadataKey): Metadata =
@@ -67,11 +59,10 @@ class MetadataDataSourceImpl(
override suspend fun flipTxAsBookmarked(
txId: String,
- account: AccountUuid,
key: MetadataKey,
): Metadata =
mutex.withLock {
- updateMetadataBookmark(txId = txId, account = account, key = key) {
+ updateMetadataBookmark(txId = txId, key = key) {
it.copy(
isBookmarked = !it.isBookmarked,
lastUpdated = Instant.now(),
@@ -82,13 +73,11 @@ class MetadataDataSourceImpl(
override suspend fun createOrUpdateTxNote(
txId: String,
note: String,
- account: AccountUuid,
key: MetadataKey
): Metadata =
mutex.withLock {
updateMetadataAnnotation(
txId = txId,
- account = account,
key = key
) {
it.copy(
@@ -100,13 +89,11 @@ class MetadataDataSourceImpl(
override suspend fun deleteTxNote(
txId: String,
- account: AccountUuid,
key: MetadataKey
): Metadata =
mutex.withLock {
updateMetadataAnnotation(
txId = txId,
- account = account,
key = key
) {
it.copy(
@@ -118,12 +105,10 @@ class MetadataDataSourceImpl(
override suspend fun markTxMemoAsRead(
txId: String,
- account: AccountUuid,
key: MetadataKey
): Metadata =
mutex.withLock {
updateMetadata(
- account = account,
key = key,
transform = { metadata ->
metadata.copy(
@@ -138,14 +123,8 @@ class MetadataDataSourceImpl(
key: MetadataKey
) = mutex.withLock {
writeToLocalStorage(metadata, key)
- this.metadata = metadata
}
- override suspend fun resetMetadata() =
- mutex.withLock {
- metadata = null
- }
-
private suspend fun getMetadataInternal(key: MetadataKey): Metadata {
fun readLocalFileToMetadata(key: MetadataKey): Metadata? {
val encryptedFile =
@@ -158,46 +137,38 @@ class MetadataDataSourceImpl(
}
return withContext(Dispatchers.IO) {
- val inMemory = metadata
-
- if (inMemory == null) {
- var new: Metadata? = readLocalFileToMetadata(key)
- if (new == null) {
- new =
- Metadata(
- version = 1,
- lastUpdated = Instant.now(),
- accountMetadata = emptyMap(),
- ).also {
- this@MetadataDataSourceImpl.metadata = it
- }
- writeToLocalStorage(new, key)
- }
- new
- } else {
- inMemory
+ var new: Metadata? = readLocalFileToMetadata(key)
+ if (new == null) {
+ new =
+ Metadata(
+ version = METADATA_SERIALIZATION_V1,
+ lastUpdated = Instant.now(),
+ accountMetadata = defaultAccountMetadata(),
+ )
+ writeToLocalStorage(new, key)
}
+ new
}
}
private suspend fun writeToLocalStorage(
metadata: Metadata,
key: MetadataKey
- ) = withContext(Dispatchers.IO) {
- runCatching {
- val file = metadataStorageProvider.getOrCreateStorageFile(key)
- metadataProvider.writeMetadataToFile(file, metadata, key)
- }.onFailure { e -> Twig.warn(e) { "Failed to write address book" } }
+ ) {
+ withContext(Dispatchers.IO) {
+ runCatching {
+ val file = metadataStorageProvider.getOrCreateStorageFile(key)
+ metadataProvider.writeMetadataToFile(file, metadata, key)
+ }.onFailure { e -> Twig.warn(e) { "Failed to write address book" } }
+ }
}
private suspend fun updateMetadataAnnotation(
txId: String,
- account: AccountUuid,
key: MetadataKey,
transform: (AnnotationMetadata) -> AnnotationMetadata
): Metadata {
return updateMetadata(
- account = account,
key = key,
transform = { metadata ->
metadata.copy(
@@ -217,12 +188,10 @@ class MetadataDataSourceImpl(
private suspend fun updateMetadataBookmark(
txId: String,
- account: AccountUuid,
key: MetadataKey,
transform: (BookmarkMetadata) -> BookmarkMetadata
): Metadata {
return updateMetadata(
- account = account,
key = key,
transform = { metadata ->
metadata.copy(
@@ -240,30 +209,21 @@ class MetadataDataSourceImpl(
)
}
- @OptIn(ExperimentalStdlibApi::class)
private suspend fun updateMetadata(
- account: AccountUuid,
key: MetadataKey,
transform: (AccountMetadata) -> AccountMetadata
): Metadata {
return withContext(Dispatchers.IO) {
val metadata = getMetadataInternal(key)
- val accountMetadata = metadata.accountMetadata[account.value.toHexString()] ?: defaultAccountMetadata()
+ val accountMetadata = metadata.accountMetadata
val updatedMetadata =
metadata.copy(
lastUpdated = Instant.now(),
- accountMetadata =
- metadata.accountMetadata
- .toMutableMap()
- .apply {
- put(account.value.toHexString(), transform(accountMetadata))
- }
- .toMap()
+ accountMetadata = transform(accountMetadata)
)
- this@MetadataDataSourceImpl.metadata = updatedMetadata
writeToLocalStorage(updatedMetadata, key)
updatedMetadata
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/model/Metadata.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/model/Metadata.kt
index 74a04479b..e8ccc0b40 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/model/Metadata.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/model/Metadata.kt
@@ -13,7 +13,7 @@ data class Metadata(
@Serializable(InstantSerializer::class)
val lastUpdated: Instant,
@SerialName("accountMetadata")
- val accountMetadata: Map
+ val accountMetadata: AccountMetadata
)
@Serializable
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/MetadataKeyStorageProvider.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/MetadataKeyStorageProvider.kt
index 8de897e09..54ddb1310 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/MetadataKeyStorageProvider.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/MetadataKeyStorageProvider.kt
@@ -2,8 +2,8 @@ package co.electriccoin.zcash.ui.common.provider
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.api.PreferenceProvider
-import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
+import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.serialization.metada.MetadataKey
import com.google.crypto.tink.InsecureSecretKeyAccess
import com.google.crypto.tink.SecretKeyAccess
@@ -12,9 +12,12 @@ import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
interface MetadataKeyStorageProvider {
- suspend fun get(): MetadataKey?
+ suspend fun get(account: WalletAccount): MetadataKey?
- suspend fun store(key: MetadataKey)
+ suspend fun store(
+ key: MetadataKey,
+ account: WalletAccount
+ )
}
class MetadataKeyStorageProviderImpl(
@@ -22,40 +25,74 @@ class MetadataKeyStorageProviderImpl(
) : MetadataKeyStorageProvider {
private val default = MetadataKeyPreferenceDefault()
- override suspend fun get(): MetadataKey? {
- return default.getValue(encryptedPreferenceProvider())
+ override suspend fun get(account: WalletAccount): MetadataKey? {
+ return default.getValue(
+ walletAccount = account,
+ preferenceProvider = encryptedPreferenceProvider(),
+ )
}
- override suspend fun store(key: MetadataKey) {
- default.putValue(encryptedPreferenceProvider(), key)
+ override suspend fun store(
+ key: MetadataKey,
+ account: WalletAccount
+ ) {
+ default.putValue(
+ newValue = key,
+ walletAccount = account,
+ preferenceProvider = encryptedPreferenceProvider(),
+ )
}
}
-private class MetadataKeyPreferenceDefault : PreferenceDefault {
+private class MetadataKeyPreferenceDefault {
private val secretKeyAccess: SecretKeyAccess?
get() = InsecureSecretKeyAccess.get()
- override val key: PreferenceKey = PreferenceKey("metadata_key")
-
- override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.decode()
-
- override suspend fun putValue(
+ suspend fun getValue(
+ walletAccount: WalletAccount,
preferenceProvider: PreferenceProvider,
- newValue: MetadataKey?
- ) = preferenceProvider.putString(key, newValue?.encode())
+ ): MetadataKey? {
+ return preferenceProvider.getStringSet(
+ key = getKey(walletAccount)
+ )?.decode()
+ }
+
+ suspend fun putValue(
+ newValue: MetadataKey?,
+ walletAccount: WalletAccount,
+ preferenceProvider: PreferenceProvider,
+ ) {
+ preferenceProvider.putStringSet(
+ key = getKey(walletAccount),
+ value = newValue?.encode()
+ )
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private fun getKey(walletAccount: WalletAccount): PreferenceKey =
+ PreferenceKey("metadata_key_${walletAccount.sdkAccount.accountUuid.value.toHexString()}")
@OptIn(ExperimentalEncodingApi::class)
- private fun MetadataKey?.encode() =
- if (this != null) {
- Base64.encode(this.key.toByteArray(secretKeyAccess))
+ private fun MetadataKey?.encode(): Set? {
+ return if (this != null) {
+ setOfNotNull(
+ Base64.encode(this.encryptionBytes.toByteArray(secretKeyAccess)),
+ this.decryptionBytes?.let { Base64.encode(it.toByteArray(secretKeyAccess)) }
+ )
} else {
null
}
+ }
@OptIn(ExperimentalEncodingApi::class)
- private fun String?.decode() =
+ private fun Set?.decode() =
if (this != null) {
- MetadataKey(SecretBytes.copyFrom(Base64.decode(this), secretKeyAccess))
+ MetadataKey(
+ encryptionBytes = SecretBytes.copyFrom(Base64.decode(this.toList()[0]), secretKeyAccess),
+ decryptionBytes =
+ this.toList().getOrNull(1)
+ ?.let { SecretBytes.copyFrom(Base64.decode(it), secretKeyAccess) },
+ )
} else {
null
}
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/MetadataRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/MetadataRepository.kt
index 52a4fb6df..9ad8fdd9a 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/MetadataRepository.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/MetadataRepository.kt
@@ -1,26 +1,29 @@
package co.electriccoin.zcash.ui.common.repository
-import cash.z.ecc.android.sdk.model.AccountUuid
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.datasource.MetadataDataSource
import co.electriccoin.zcash.ui.common.model.Metadata
+import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.provider.MetadataKeyStorageProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
import co.electriccoin.zcash.ui.common.serialization.metada.MetadataKey
+import co.electriccoin.zcash.ui.util.CloseableScopeHolder
+import co.electriccoin.zcash.ui.util.CloseableScopeHolderImpl
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.onSubscription
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
interface MetadataRepository {
val metadata: Flow
@@ -36,125 +39,158 @@ interface MetadataRepository {
suspend fun markTxMemoAsRead(txId: String)
- suspend fun resetMetadata()
-
fun observeTransactionMetadataByTxId(txId: String): Flow
}
class MetadataRepositoryImpl(
+ private val accountDataSource: AccountDataSource,
private val metadataDataSource: MetadataDataSource,
private val metadataKeyStorageProvider: MetadataKeyStorageProvider,
- private val accountDataSource: AccountDataSource,
private val persistableWalletProvider: PersistableWalletProvider,
-) : MetadataRepository {
- private val semaphore = Mutex()
-
- private val cache = MutableStateFlow(null)
+) : MetadataRepository, CloseableScopeHolder by CloseableScopeHolderImpl(Dispatchers.IO) {
+ private val command = Channel()
+ @OptIn(ExperimentalCoroutinesApi::class)
override val metadata: Flow =
- cache
- .onSubscription {
- withNonCancellableSemaphore {
- ensureSynchronization()
+ accountDataSource
+ .selectedAccount
+ .flatMapLatest { account ->
+ channelFlow {
+ send(null)
+
+ if (account != null) {
+ val metadataKey = getMetadataKey(account)
+ send(metadataDataSource.getMetadata(metadataKey))
+
+ launch {
+ command
+ .receiveAsFlow()
+ .filter {
+ it.account.sdkAccount.accountUuid == account.sdkAccount.accountUuid
+ }
+ .collect { command ->
+ val new =
+ when (command) {
+ is Command.CreateOrUpdateTxNote ->
+ metadataDataSource.createOrUpdateTxNote(
+ txId = command.txId,
+ key = metadataKey,
+ note = command.note
+ )
+
+ is Command.DeleteTxNote ->
+ metadataDataSource.deleteTxNote(
+ txId = command.txId,
+ key = metadataKey,
+ )
+
+ is Command.FlipTxBookmark ->
+ metadataDataSource.flipTxAsBookmarked(
+ txId = command.txId,
+ key = metadataKey,
+ )
+
+ is Command.MarkTxMemoAsRead ->
+ metadataDataSource.markTxMemoAsRead(
+ txId = command.txId,
+ key = metadataKey,
+ )
+ }
+
+ send(new)
+ }
+ }
+ }
+
+ awaitClose {
+ // do nothing
+ }
}
}
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.Lazily,
+ initialValue = null
+ )
- override suspend fun flipTxBookmark(txId: String) =
- mutateMetadata {
- metadataDataSource.flipTxAsBookmarked(
- txId = txId,
- key = getMetadataKey(),
- account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
+ override suspend fun flipTxBookmark(txId: String) {
+ scope.launch {
+ command.send(
+ Command.FlipTxBookmark(
+ txId = txId,
+ account = accountDataSource.getSelectedAccount()
+ )
)
}
+ }
override suspend fun createOrUpdateTxNote(
txId: String,
note: String
- ) = mutateMetadata {
- metadataDataSource.createOrUpdateTxNote(
- txId = txId,
- note = note,
- key = getMetadataKey(),
- account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
- )
- }
-
- override suspend fun deleteTxNote(txId: String) =
- mutateMetadata {
- metadataDataSource.deleteTxNote(
- txId = txId,
- key = getMetadataKey(),
- account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
+ ) {
+ scope.launch {
+ command.send(
+ Command.CreateOrUpdateTxNote(
+ txId = txId,
+ note = note,
+ account = accountDataSource.getSelectedAccount()
+ )
+ )
+ }
+ }
+
+ override suspend fun deleteTxNote(txId: String) {
+ scope.launch {
+ command.send(
+ Command.DeleteTxNote(
+ txId = txId,
+ account = accountDataSource.getSelectedAccount()
+ )
+ )
+ }
+ }
+
+ override suspend fun markTxMemoAsRead(txId: String) {
+ scope.launch {
+ command.send(
+ Command.MarkTxMemoAsRead(
+ txId = txId,
+ account = accountDataSource.getSelectedAccount()
+ )
)
}
-
- override suspend fun markTxMemoAsRead(txId: String) =
- mutateMetadata {
- metadataDataSource.markTxMemoAsRead(
- txId = txId,
- key = getMetadataKey(),
- account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
- )
- }
-
- override suspend fun resetMetadata() {
- withNonCancellableSemaphore {
- metadataDataSource.resetMetadata()
- cache.update { null }
- }
}
- @OptIn(ExperimentalStdlibApi::class)
override fun observeTransactionMetadataByTxId(txId: String): Flow =
- combine(
- metadata,
- accountDataSource.selectedAccount.filterNotNull().map { it.sdkAccount.accountUuid }.distinctUntilChanged()
- ) { metadata, account ->
- val accountMetadata = metadata?.accountMetadata?.get(account.value.toHexString())
+ metadata
+ .map { metadata ->
+ val accountMetadata = metadata?.accountMetadata
- TransactionMetadata(
- isBookmarked = accountMetadata?.bookmarked?.find { it.txId == txId }?.isBookmarked == true,
- isRead = accountMetadata?.read?.any { it == txId } == true,
- note = accountMetadata?.annotations?.find { it.txId == txId }?.content,
- )
- }.distinctUntilChanged().onStart { emit(null) }
+ TransactionMetadata(
+ isBookmarked = accountMetadata?.bookmarked?.find { it.txId == txId }?.isBookmarked == true,
+ isRead = accountMetadata?.read?.any { it == txId } == true,
+ note = accountMetadata?.annotations?.find { it.txId == txId }?.content,
+ )
+ }
+ .distinctUntilChanged()
+ .onStart { emit(null) }
- private suspend fun ensureSynchronization() {
- if (cache.value == null) {
- val metadata = metadataDataSource.getMetadata(key = getMetadataKey())
- metadataDataSource.save(metadata = metadata, key = getMetadataKey())
- cache.update { metadata }
- }
- }
-
- private suspend fun mutateMetadata(block: suspend () -> Metadata) =
- withNonCancellableSemaphore {
- ensureSynchronization()
- val new = block()
- cache.update { new }
- }
-
- private suspend fun withNonCancellableSemaphore(block: suspend () -> Unit) =
- withContext(NonCancellable + Dispatchers.Default) {
- semaphore.withLock { block() }
- }
-
- private suspend fun getMetadataKey(): MetadataKey {
- val key = metadataKeyStorageProvider.get()
+ private suspend fun getMetadataKey(selectedAccount: WalletAccount): MetadataKey {
+ val key = metadataKeyStorageProvider.get(selectedAccount)
return if (key != null) {
key
} else {
- val account = accountDataSource.getZashiAccount()
val persistableWallet = persistableWalletProvider.getPersistableWallet()
+ val zashiAccount = accountDataSource.getZashiAccount()
val newKey =
MetadataKey.derive(
seedPhrase = persistableWallet.seedPhrase,
network = persistableWallet.network,
- account = account
+ zashiAccount = zashiAccount,
+ selectedAccount = selectedAccount
)
- metadataKeyStorageProvider.store(newKey)
+ metadataKeyStorageProvider.store(newKey, selectedAccount)
newKey
}
}
@@ -165,3 +201,28 @@ data class TransactionMetadata(
val isRead: Boolean,
val note: String?
)
+
+private sealed interface Command {
+ val account: WalletAccount
+
+ data class FlipTxBookmark(
+ val txId: String,
+ override val account: WalletAccount
+ ) : Command
+
+ data class CreateOrUpdateTxNote(
+ val txId: String,
+ val note: String,
+ override val account: WalletAccount
+ ) : Command
+
+ data class DeleteTxNote(
+ val txId: String,
+ override val account: WalletAccount
+ ) : Command
+
+ data class MarkTxMemoAsRead(
+ val txId: String,
+ override val account: WalletAccount
+ ) : Command
+}
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/Encryptor.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/Encryptor.kt
deleted file mode 100644
index fa1a9cac5..000000000
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/Encryptor.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package co.electriccoin.zcash.ui.common.serialization
-
-import com.google.crypto.tink.subtle.ChaCha20Poly1305
-import com.google.crypto.tink.subtle.Random
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.io.InputStream
-import java.io.OutputStream
-
-interface Encryptor {
- fun encrypt(
- key: KEY,
- outputStream: OutputStream,
- data: T
- )
-
- fun decrypt(
- key: KEY,
- inputStream: InputStream
- ): T
-}
-
-abstract class BaseEncryptor : BaseSerializer(), Encryptor {
- abstract val version: Int
- abstract val saltSize: Int
-
- protected abstract fun serialize(
- outputStream: ByteArrayOutputStream,
- data: T
- )
-
- protected abstract fun deserialize(inputStream: ByteArrayInputStream): T
-
- override fun encrypt(
- key: KEY,
- outputStream: OutputStream,
- data: T
- ) {
- // Generate a fresh one-time key for this ciphertext.
- val salt = Random.randBytes(saltSize)
- val cipherText =
- ByteArrayOutputStream()
- .use { stream ->
- serialize(stream, data)
- stream.toByteArray()
- }.let {
- val derivedKey = key.deriveEncryptionKey(salt)
- // Tink encodes the ciphertext as `nonce || ciphertext || tag`.
- val cipher = ChaCha20Poly1305.create(derivedKey)
- cipher.encrypt(it, null)
- }
-
- outputStream.write(version.createByteArray())
- outputStream.write(salt)
- outputStream.write(cipherText)
- }
-
- override fun decrypt(
- key: KEY,
- inputStream: InputStream
- ): T {
- val version = inputStream.readInt()
- if (version != this.version) {
- throw UnknownEncryptionVersionException()
- }
-
- val salt = ByteArray(saltSize)
- require(inputStream.read(salt) == salt.size) { "Input is too short" }
-
- val ciphertext = inputStream.readBytes()
-
- val derivedKey = key.deriveEncryptionKey(salt)
- val cipher = ChaCha20Poly1305.create(derivedKey)
- val plaintext = cipher.decrypt(ciphertext, null)
-
- return plaintext.inputStream().use { stream ->
- deserialize(stream)
- }
- }
-}
-
-class UnknownEncryptionVersionException : RuntimeException("Unknown encryption version")
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/UnknownEncryptionVersionException.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/UnknownEncryptionVersionException.kt
new file mode 100644
index 000000000..e5df37a6b
--- /dev/null
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/UnknownEncryptionVersionException.kt
@@ -0,0 +1,3 @@
+package co.electriccoin.zcash.ui.common.serialization
+
+class UnknownEncryptionVersionException : RuntimeException("Unknown encryption version")
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/addressbook/AddressBookEncryptor.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/addressbook/AddressBookEncryptor.kt
index 2390be606..f244539fe 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/addressbook/AddressBookEncryptor.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/addressbook/AddressBookEncryptor.kt
@@ -3,27 +3,89 @@ package co.electriccoin.zcash.ui.common.serialization.addressbook
import co.electriccoin.zcash.ui.common.model.AddressBook
import co.electriccoin.zcash.ui.common.serialization.ADDRESS_BOOK_ENCRYPTION_V1
import co.electriccoin.zcash.ui.common.serialization.ADDRESS_BOOK_SALT_SIZE
-import co.electriccoin.zcash.ui.common.serialization.BaseEncryptor
-import co.electriccoin.zcash.ui.common.serialization.Encryptor
+import co.electriccoin.zcash.ui.common.serialization.BaseSerializer
+import co.electriccoin.zcash.ui.common.serialization.UnknownEncryptionVersionException
+import com.google.crypto.tink.subtle.ChaCha20Poly1305
+import com.google.crypto.tink.subtle.Random
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.OutputStream
-interface AddressBookEncryptor : Encryptor
+interface AddressBookEncryptor {
+ fun encrypt(
+ key: AddressBookKey,
+ outputStream: OutputStream,
+ data: AddressBook
+ )
+
+ fun decrypt(
+ key: AddressBookKey,
+ inputStream: InputStream
+ ): AddressBook
+}
class AddressBookEncryptorImpl(
private val addressBookSerializer: AddressBookSerializer,
-) : AddressBookEncryptor, BaseEncryptor() {
- override val version: Int = ADDRESS_BOOK_ENCRYPTION_V1
- override val saltSize: Int = ADDRESS_BOOK_SALT_SIZE
+) : AddressBookEncryptor, BaseSerializer() {
+ private val version: Int = ADDRESS_BOOK_ENCRYPTION_V1
+ private val saltSize: Int = ADDRESS_BOOK_SALT_SIZE
- override fun serialize(
+ override fun encrypt(
+ key: AddressBookKey,
+ outputStream: OutputStream,
+ data: AddressBook
+ ) {
+ // Generate a fresh one-time key for this ciphertext.
+ val salt = Random.randBytes(saltSize)
+ val cipherText =
+ ByteArrayOutputStream()
+ .use { stream ->
+ serialize(stream, data)
+ stream.toByteArray()
+ }.let {
+ val derivedKey = key.deriveEncryptionKey(salt)
+ // Tink encodes the ciphertext as `nonce || ciphertext || tag`.
+ val cipher = ChaCha20Poly1305.create(derivedKey)
+ cipher.encrypt(it, null)
+ }
+
+ outputStream.write(version.createByteArray())
+ outputStream.write(salt)
+ outputStream.write(cipherText)
+ }
+
+ override fun decrypt(
+ key: AddressBookKey,
+ inputStream: InputStream
+ ): AddressBook {
+ val version = inputStream.readInt()
+ if (version != this.version) {
+ throw UnknownEncryptionVersionException()
+ }
+
+ val salt = ByteArray(saltSize)
+ require(inputStream.read(salt) == salt.size) { "Input is too short" }
+
+ val ciphertext = inputStream.readBytes()
+
+ val derivedKey = key.deriveEncryptionKey(salt)
+ val cipher = ChaCha20Poly1305.create(derivedKey)
+ val plaintext = cipher.decrypt(ciphertext, null)
+
+ return plaintext.inputStream().use { stream ->
+ deserialize(stream)
+ }
+ }
+
+ private fun serialize(
outputStream: ByteArrayOutputStream,
data: AddressBook
) {
addressBookSerializer.serializeAddressBook(outputStream, data)
}
- override fun deserialize(inputStream: ByteArrayInputStream): AddressBook {
+ private fun deserialize(inputStream: ByteArrayInputStream): AddressBook {
return addressBookSerializer.deserializeAddressBook(inputStream)
}
}
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadaEncryptor.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadaEncryptor.kt
index 950b3aca0..6ca8650f8 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadaEncryptor.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadaEncryptor.kt
@@ -1,36 +1,38 @@
package co.electriccoin.zcash.ui.common.serialization.metada
import co.electriccoin.zcash.ui.common.model.Metadata
-import co.electriccoin.zcash.ui.common.serialization.BaseEncryptor
-import co.electriccoin.zcash.ui.common.serialization.Encryptor
+import co.electriccoin.zcash.ui.common.serialization.BaseSerializer
import co.electriccoin.zcash.ui.common.serialization.METADATA_ENCRYPTION_V1
import co.electriccoin.zcash.ui.common.serialization.METADATA_SALT_SIZE
import co.electriccoin.zcash.ui.common.serialization.UnknownEncryptionVersionException
+import com.google.crypto.tink.aead.ChaCha20Poly1305Key
import com.google.crypto.tink.subtle.ChaCha20Poly1305
import com.google.crypto.tink.subtle.Random
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
+import kotlin.jvm.Throws
-interface MetadataEncryptor : Encryptor
+interface MetadataEncryptor {
+ fun encrypt(
+ key: MetadataKey,
+ outputStream: OutputStream,
+ data: Metadata
+ )
+
+ @Throws(DecryptionException::class)
+ fun decrypt(
+ key: MetadataKey,
+ inputStream: InputStream
+ ): Metadata
+}
class MetadataEncryptorImpl(
private val metadataSerializer: MetadataSerializer,
-) : MetadataEncryptor, BaseEncryptor() {
- override val version: Int = METADATA_ENCRYPTION_V1
- override val saltSize: Int = METADATA_SALT_SIZE
-
- override fun serialize(
- outputStream: ByteArrayOutputStream,
- data: Metadata
- ) {
- metadataSerializer.serialize(outputStream, data)
- }
-
- override fun deserialize(inputStream: ByteArrayInputStream): Metadata {
- return metadataSerializer.deserialize(inputStream)
- }
+) : MetadataEncryptor, BaseSerializer() {
+ private val version: Int = METADATA_ENCRYPTION_V1
+ private val saltSize: Int = METADATA_SALT_SIZE
override fun encrypt(
key: MetadataKey,
@@ -72,12 +74,34 @@ class MetadataEncryptorImpl(
val ciphertext = inputStream.readBytes()
- val derivedKey = key.deriveEncryptionKey(salt)
- val cipher = ChaCha20Poly1305.create(derivedKey)
- val plaintext = cipher.decrypt(ciphertext, null)
+ return decrypt(key.deriveFirstDecryptionKey(salt), ciphertext)
+ ?: decrypt(key.deriveSecondDecryptionKey(salt), ciphertext)
+ ?: throw DecryptionException()
+ }
- return plaintext.inputStream().use { stream ->
- deserialize(stream)
- }
+ private fun decrypt(
+ key: ChaCha20Poly1305Key?,
+ ciphertext: ByteArray
+ ): Metadata? {
+ if (key == null) return null
+
+ return runCatching {
+ val cipher = ChaCha20Poly1305.create(key)
+ val plaintext = cipher.decrypt(ciphertext, null)
+ plaintext.inputStream().use { stream -> deserialize(stream) }
+ }.getOrNull()
+ }
+
+ private fun serialize(
+ outputStream: ByteArrayOutputStream,
+ data: Metadata
+ ) {
+ metadataSerializer.serialize(outputStream, data)
+ }
+
+ private fun deserialize(inputStream: ByteArrayInputStream): Metadata {
+ return metadataSerializer.deserialize(inputStream)
}
}
+
+class DecryptionException : Exception()
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadataKey.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadataKey.kt
index 222aea672..873401b06 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadataKey.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/serialization/metada/MetadataKey.kt
@@ -3,8 +3,9 @@ package co.electriccoin.zcash.ui.common.serialization.metada
import cash.z.ecc.android.sdk.model.SeedPhrase
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
+import co.electriccoin.zcash.ui.common.model.KeystoneAccount
import co.electriccoin.zcash.ui.common.model.WalletAccount
-import co.electriccoin.zcash.ui.common.serialization.Key
+import co.electriccoin.zcash.ui.common.model.ZashiAccount
import co.electriccoin.zcash.ui.common.serialization.METADATA_ENCRYPTION_KEY_SIZE
import co.electriccoin.zcash.ui.common.serialization.METADATA_FILE_IDENTIFIER_SIZE
import co.electriccoin.zcash.ui.common.serialization.METADATA_SALT_SIZE
@@ -16,17 +17,20 @@ import com.google.crypto.tink.util.SecretBytes
/**
* The long-term key that can decrypt an account's encrypted address book.
*/
-class MetadataKey(val key: SecretBytes) : Key {
+class MetadataKey(
+ val encryptionBytes: SecretBytes,
+ val decryptionBytes: SecretBytes?
+) {
/**
* Derives the filename that this key is able to decrypt.
*/
@OptIn(ExperimentalStdlibApi::class)
- override fun fileIdentifier(): String {
+ fun fileIdentifier(): String {
val access = InsecureSecretKeyAccess.get()
val fileIdentifier =
Hkdf.computeHkdf(
"HMACSHA256",
- key.toByteArray(access),
+ encryptionBytes.toByteArray(access),
null,
"file_identifier".toByteArray(),
METADATA_FILE_IDENTIFIER_SIZE
@@ -34,19 +38,13 @@ class MetadataKey(val key: SecretBytes) : Key {
return "zashi-metadata-" + fileIdentifier.toHexString()
}
- /**
- * Derives a one-time address book encryption key.
- *
- * At encryption time, the one-time property MUST be ensured by generating a
- * random 32-byte salt.
- */
- override fun deriveEncryptionKey(salt: ByteArray): ChaCha20Poly1305Key {
+ fun deriveEncryptionKey(salt: ByteArray): ChaCha20Poly1305Key {
assert(salt.size == METADATA_SALT_SIZE)
val access = InsecureSecretKeyAccess.get()
val subKey =
Hkdf.computeHkdf(
"HMACSHA256",
- key.toByteArray(access),
+ encryptionBytes.toByteArray(access),
null,
salt + "encryption_key".toByteArray(),
METADATA_ENCRYPTION_KEY_SIZE
@@ -54,28 +52,64 @@ class MetadataKey(val key: SecretBytes) : Key {
return ChaCha20Poly1305Key.create(SecretBytes.copyFrom(subKey, access))
}
+ fun deriveFirstDecryptionKey(salt: ByteArray): ChaCha20Poly1305Key {
+ return deriveDecryptionkey(salt, encryptionBytes, "encryption_key")
+ }
+
+ fun deriveSecondDecryptionKey(salt: ByteArray): ChaCha20Poly1305Key? {
+ if (decryptionBytes == null) return null
+ return deriveDecryptionkey(salt, decryptionBytes, "decryption_key")
+ }
+
+ private fun deriveDecryptionkey(
+ salt: ByteArray,
+ decryptionBytes: SecretBytes,
+ infoKey: String
+ ): ChaCha20Poly1305Key {
+ assert(salt.size == METADATA_SALT_SIZE)
+ val access = InsecureSecretKeyAccess.get()
+ val subKey =
+ Hkdf.computeHkdf(
+ "HMACSHA256",
+ decryptionBytes.toByteArray(access),
+ null,
+ salt + infoKey.toByteArray(),
+ METADATA_ENCRYPTION_KEY_SIZE
+ )
+ return ChaCha20Poly1305Key.create(SecretBytes.copyFrom(subKey, access))
+ }
+
companion object {
- /**
- * Derives the long-term key that can decrypt the given account's encrypted
- * address book.
- *
- * This requires access to the seed phrase. If the app has separate access
- * control requirements for the seed phrase and the address book, this key
- * should be cached in the app's keystore.
- */
suspend fun derive(
seedPhrase: SeedPhrase,
network: ZcashNetwork,
- account: WalletAccount
+ zashiAccount: ZashiAccount,
+ selectedAccount: WalletAccount
): MetadataKey {
val key =
- DerivationTool.getInstance().deriveArbitraryAccountKey(
- contextString = "ZashiMetadataEncryptionV1".toByteArray(),
- seed = seedPhrase.toByteArray(),
- network = network,
- accountIndex = account.hdAccountIndex,
- )
- return MetadataKey(SecretBytes.copyFrom(key, InsecureSecretKeyAccess.get()))
+ DerivationTool.getInstance()
+ .deriveAccountMetadataKey(
+ seed = seedPhrase.toByteArray(),
+ network = network,
+ accountIndex = zashiAccount.hdAccountIndex,
+ )
+ .derivePrivateUseMetadataKey(
+ ufvk =
+ when (selectedAccount) {
+ is KeystoneAccount -> selectedAccount.sdkAccount.ufvk
+ is ZashiAccount -> null
+ },
+ network = network,
+ privateUseSubject = "metadata".toByteArray()
+ )
+ return MetadataKey(
+ encryptionBytes = SecretBytes.copyFrom(key[0], InsecureSecretKeyAccess.get()),
+ decryptionBytes =
+ key.getOrNull(1)
+ ?.let {
+ SecretBytes.copyFrom(it, InsecureSecretKeyAccess.get())
+ }
+ )
}
}
}
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ResetInMemoryDataUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ResetInMemoryDataUseCase.kt
index 791550594..1234f4480 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ResetInMemoryDataUseCase.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ResetInMemoryDataUseCase.kt
@@ -1,14 +1,11 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
-import co.electriccoin.zcash.ui.common.repository.MetadataRepository
class ResetInMemoryDataUseCase(
private val addressBookRepository: AddressBookRepository,
- private val metadataRepository: MetadataRepository
) {
suspend operator fun invoke() {
addressBookRepository.resetAddressBook()
- metadataRepository.resetMetadata()
}
}
diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactiondetail/TransactionDetailView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactiondetail/TransactionDetailView.kt
index e636313c9..8bd0facb5 100644
--- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactiondetail/TransactionDetailView.kt
+++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactiondetail/TransactionDetailView.kt
@@ -32,6 +32,7 @@ import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
+import co.electriccoin.zcash.ui.design.util.asScaffoldPaddingValues
import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes
@@ -143,7 +144,7 @@ private fun BottomBar(
) {
ZashiBottomBar(
isElevated = scrollState.value > 0,
- modifier = Modifier.scaffoldPadding(paddingValues, top = 0.dp, bottom = 0.dp)
+ contentPadding = paddingValues.asScaffoldPaddingValues(top = 0.dp, bottom = 0.dp)
) {
Row(
modifier = Modifier.fillMaxWidth()