diff --git a/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/EntityMetadataCollectionExtensions.kt b/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/EntityMetadataCollectionExtensions.kt index c5fe1aa473..ace2fbc168 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/EntityMetadataCollectionExtensions.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/EntityMetadataCollectionExtensions.kt @@ -38,6 +38,11 @@ import com.babylon.wallet.android.data.gateway.generated.models.PublicKeyEcdsaSe import com.babylon.wallet.android.data.gateway.generated.models.PublicKeyEddsaEd25519 import com.babylon.wallet.android.data.gateway.generated.models.PublicKeyHashEcdsaSecp256k1 import com.babylon.wallet.android.data.gateway.generated.models.PublicKeyHashEddsaEd25519 +import com.radixdlt.sargon.NonFungibleGlobalId +import com.radixdlt.sargon.NonFungibleLocalId +import com.radixdlt.sargon.ResourceAddress +import com.radixdlt.sargon.extensions.init +import com.radixdlt.sargon.extensions.string import rdx.works.core.domain.resources.metadata.Metadata import rdx.works.core.domain.resources.metadata.MetadataType import rdx.works.core.domain.resources.metadata.MetadataType.Integer.Size @@ -51,7 +56,8 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) is MetadataBoolValue -> Metadata.Primitive( key = key, value = typed.value.toString(), - valueType = MetadataType.Bool + valueType = MetadataType.Bool, + isLocked = isLocked ) is MetadataBoolArrayValue -> Metadata.Collection( @@ -60,15 +66,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it.toString(), - valueType = MetadataType.Bool + valueType = MetadataType.Bool, + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataDecimalValue -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Decimal + valueType = MetadataType.Decimal, + isLocked = isLocked ) is MetadataDecimalArrayValue -> Metadata.Collection( @@ -79,13 +88,15 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) value = it, valueType = MetadataType.Decimal ) - } + }, + isLocked = isLocked ) is MetadataGlobalAddressValue -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Address + valueType = MetadataType.Address, + isLocked = isLocked ) is MetadataGlobalAddressArrayValue -> Metadata.Collection( @@ -94,15 +105,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Address + valueType = MetadataType.Address, + isLocked = isLocked ) }, + isLocked = isLocked ) is MetadataI32Value -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Integer(signed = true, size = Size.INT) + valueType = MetadataType.Integer(signed = true, size = Size.INT), + isLocked = isLocked ) is MetadataI32ArrayValue -> Metadata.Collection( @@ -111,15 +125,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Integer(signed = true, size = Size.INT) + valueType = MetadataType.Integer(signed = true, size = Size.INT), + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataI64Value -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Integer(signed = true, size = Size.LONG) + valueType = MetadataType.Integer(signed = true, size = Size.LONG), + isLocked = isLocked ) is MetadataI64ArrayValue -> Metadata.Collection( @@ -128,27 +145,32 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Integer(signed = true, size = Size.LONG) + valueType = MetadataType.Integer(signed = true, size = Size.LONG), + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataU8Value -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Integer(signed = false, size = Size.INT) + valueType = MetadataType.Integer(signed = false, size = Size.INT), + isLocked = isLocked ) is MetadataU8ArrayValue -> Metadata.Primitive( key = key, value = typed.valueHex, - valueType = MetadataType.Bytes + valueType = MetadataType.Bytes, + isLocked = isLocked ) is MetadataU32Value -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Integer(signed = false, size = Size.INT) + valueType = MetadataType.Integer(signed = false, size = Size.INT), + isLocked = isLocked ) is MetadataU32ArrayValue -> Metadata.Collection( @@ -157,15 +179,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Integer(signed = false, size = Size.INT) + valueType = MetadataType.Integer(signed = false, size = Size.INT), + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataU64Value -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Integer(signed = false, size = Size.LONG) + valueType = MetadataType.Integer(signed = false, size = Size.LONG), + isLocked = isLocked ) is MetadataU64ArrayValue -> Metadata.Collection( @@ -174,15 +199,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Integer(signed = false, size = Size.LONG) + valueType = MetadataType.Integer(signed = false, size = Size.LONG), + isLocked = isLocked ) }, + isLocked = isLocked ) is MetadataInstantValue -> Metadata.Primitive( key = key, - value = typed.value, - valueType = MetadataType.Instant + value = typed.unixTimestampSeconds, + valueType = MetadataType.Instant, + isLocked = isLocked ) is MetadataInstantArrayValue -> Metadata.Collection( @@ -191,15 +219,21 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Instant + valueType = MetadataType.Instant, + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataNonFungibleGlobalIdValue -> Metadata.Primitive( key = key, - value = "${typed.resourceAddress}:${typed.nonFungibleId}", - valueType = MetadataType.NonFungibleGlobalId + value = NonFungibleGlobalId( + resourceAddress = ResourceAddress.init(typed.resourceAddress), + nonFungibleLocalId = NonFungibleLocalId.init(typed.nonFungibleId) + ).string, + valueType = MetadataType.NonFungibleGlobalId, + isLocked = isLocked ) is MetadataNonFungibleGlobalIdArrayValue -> Metadata.Collection( @@ -207,17 +241,23 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) values = typed.propertyValues.map { Metadata.Primitive( key = key, - value = "${it.resourceAddress}:${it.nonFungibleId}", - valueType = MetadataType.NonFungibleGlobalId + value = NonFungibleGlobalId( + resourceAddress = ResourceAddress.init(it.resourceAddress), + nonFungibleLocalId = NonFungibleLocalId.init(it.nonFungibleId) + ).string, + valueType = MetadataType.NonFungibleGlobalId, + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataNonFungibleLocalIdValue -> Metadata.Primitive( key = key, lastUpdatedAtStateVersion = lastUpdatedAtStateVersion, value = typed.value, - valueType = MetadataType.NonFungibleLocalId + valueType = MetadataType.NonFungibleLocalId, + isLocked = isLocked ) is MetadataNonFungibleLocalIdArrayValue -> Metadata.Collection( @@ -226,15 +266,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.NonFungibleLocalId + valueType = MetadataType.NonFungibleLocalId, + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataOriginValue -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Url + valueType = MetadataType.Url, + isLocked = isLocked ) is MetadataOriginArrayValue -> Metadata.Collection( @@ -243,15 +286,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Url + valueType = MetadataType.Url, + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataStringValue -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.String + valueType = MetadataType.String, + isLocked = isLocked ) is MetadataStringArrayValue -> Metadata.Collection( @@ -260,15 +306,18 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.String + valueType = MetadataType.String, + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataUrlValue -> Metadata.Primitive( key = key, value = typed.value, - valueType = MetadataType.Url + valueType = MetadataType.Url, + isLocked = isLocked ) is MetadataUrlArrayValue -> Metadata.Collection( @@ -277,9 +326,11 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) Metadata.Primitive( key = key, value = it, - valueType = MetadataType.Url + valueType = MetadataType.Url, + isLocked = isLocked ) - } + }, + isLocked = isLocked ) is MetadataPublicKeyValue -> when (typed.value) { @@ -287,14 +338,16 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) key = key, lastUpdatedAtStateVersion = lastUpdatedAtStateVersion, value = typed.value.keyHex, - valueType = MetadataType.PublicKeyEcdsaSecp256k1 + valueType = MetadataType.PublicKeyEcdsaSecp256k1, + isLocked = isLocked ) is PublicKeyEddsaEd25519 -> Metadata.Primitive( key = key, lastUpdatedAtStateVersion = lastUpdatedAtStateVersion, value = typed.value.keyHex, - valueType = MetadataType.PublicKeyEddsaEd25519 + valueType = MetadataType.PublicKeyEddsaEd25519, + isLocked = isLocked ) else -> error("Not supported MetadataPublicKeyValue type for $value") @@ -308,18 +361,21 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) is PublicKeyEcdsaSecp256k1 -> Metadata.Primitive( key = key, value = value.keyHex, - valueType = MetadataType.PublicKeyEcdsaSecp256k1 + valueType = MetadataType.PublicKeyEcdsaSecp256k1, + isLocked = isLocked ) is PublicKeyEddsaEd25519 -> Metadata.Primitive( key = key, value = value.keyHex, - valueType = MetadataType.PublicKeyEddsaEd25519 + valueType = MetadataType.PublicKeyEddsaEd25519, + isLocked = isLocked ) else -> error("Not supported MetadataPublicKeyValue type for $value") } - } + }, + isLocked = isLocked ) is MetadataPublicKeyHashValue -> when (typed.value) { @@ -327,14 +383,16 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) key = key, lastUpdatedAtStateVersion = lastUpdatedAtStateVersion, value = typed.value.hashHex, - valueType = MetadataType.PublicKeyHashEcdsaSecp256k1 + valueType = MetadataType.PublicKeyHashEcdsaSecp256k1, + isLocked = isLocked ) is PublicKeyHashEddsaEd25519 -> Metadata.Primitive( key = key, lastUpdatedAtStateVersion = lastUpdatedAtStateVersion, value = typed.value.hashHex, - valueType = MetadataType.PublicKeyHashEddsaEd25519 + valueType = MetadataType.PublicKeyHashEddsaEd25519, + isLocked = isLocked ) } @@ -347,17 +405,20 @@ fun EntityMetadataItem.toMetadata(): Metadata? = when (val typed = value.typed) key = key, lastUpdatedAtStateVersion = lastUpdatedAtStateVersion, value = value.hashHex, - valueType = MetadataType.PublicKeyHashEcdsaSecp256k1 + valueType = MetadataType.PublicKeyHashEcdsaSecp256k1, + isLocked = isLocked ) is PublicKeyHashEddsaEd25519 -> Metadata.Primitive( key = key, lastUpdatedAtStateVersion = lastUpdatedAtStateVersion, value = value.hashHex, - valueType = MetadataType.PublicKeyHashEddsaEd25519 + valueType = MetadataType.PublicKeyHashEddsaEd25519, + isLocked = isLocked ) } - } + }, + isLocked = isLocked ) else -> null diff --git a/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/StateEntityDetailsApiHelper.kt b/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/StateEntityDetailsApiHelper.kt index fe138a4042..9aeb9cb261 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/StateEntityDetailsApiHelper.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/gateway/extensions/StateEntityDetailsApiHelper.kt @@ -1,6 +1,7 @@ package com.babylon.wallet.android.data.gateway.extensions import com.babylon.wallet.android.data.gateway.apis.StateApi +import com.babylon.wallet.android.data.gateway.generated.models.EntityMetadataItem import com.babylon.wallet.android.data.gateway.generated.models.FungibleResourcesCollection import com.babylon.wallet.android.data.gateway.generated.models.FungibleResourcesCollectionItem import com.babylon.wallet.android.data.gateway.generated.models.LedgerState @@ -15,6 +16,7 @@ import com.babylon.wallet.android.data.gateway.generated.models.StateEntityDetai import com.babylon.wallet.android.data.gateway.generated.models.StateEntityDetailsResponseItem import com.babylon.wallet.android.data.gateway.generated.models.StateEntityFungiblesPageRequest import com.babylon.wallet.android.data.gateway.generated.models.StateEntityFungiblesPageRequestOptIns +import com.babylon.wallet.android.data.gateway.generated.models.StateEntityMetadataPageRequest import com.babylon.wallet.android.data.gateway.generated.models.StateEntityNonFungibleIdsPageRequest import com.babylon.wallet.android.data.gateway.generated.models.StateEntityNonFungiblesPageRequest import com.babylon.wallet.android.data.gateway.generated.models.StateEntityNonFungiblesPageRequestOptIns @@ -386,3 +388,28 @@ suspend fun StateApi.paginateNonFungibles( onPage(response) } } + +suspend fun StateApi.getAllMetadata( + resourceAddress: ResourceAddress, + stateVersion: Long, + initialCursor: String +): List { + val items = mutableListOf() + + var cursor: String? = initialCursor + while (cursor != null) { + val page = entityMetadataPage( + stateEntityMetadataPageRequest = StateEntityMetadataPageRequest( + address = resourceAddress.string, + cursor = initialCursor, + atLedgerState = LedgerStateSelector( + stateVersion = stateVersion + ) + ) + ).toResult().getOrThrow() + cursor = page.nextCursor + items.addAll(page.items) + } + + return items +} diff --git a/app/src/main/java/com/babylon/wallet/android/data/gateway/serialisers/MetadataTypedValueSerializer.kt b/app/src/main/java/com/babylon/wallet/android/data/gateway/serialisers/MetadataTypedValueSerializer.kt index 366ef32224..ec339650af 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/gateway/serialisers/MetadataTypedValueSerializer.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/gateway/serialisers/MetadataTypedValueSerializer.kt @@ -25,6 +25,7 @@ import com.babylon.wallet.android.data.gateway.generated.models.MetadataPublicKe import com.babylon.wallet.android.data.gateway.generated.models.MetadataStringArrayValue import com.babylon.wallet.android.data.gateway.generated.models.MetadataStringValue import com.babylon.wallet.android.data.gateway.generated.models.MetadataTypedValue +import com.babylon.wallet.android.data.gateway.generated.models.MetadataU32ArrayValue import com.babylon.wallet.android.data.gateway.generated.models.MetadataU32Value import com.babylon.wallet.android.data.gateway.generated.models.MetadataU64ArrayValue import com.babylon.wallet.android.data.gateway.generated.models.MetadataU64Value @@ -62,7 +63,7 @@ object MetadataTypedValueSerializer : JsonContentPolymorphicSerializer MetadataStringArrayValue.serializer() MetadataValueType.BoolArray -> MetadataBoolArrayValue.serializer() MetadataValueType.U8Array -> MetadataU8ArrayValue.serializer() - MetadataValueType.U32Array -> MetadataU32Value.serializer() + MetadataValueType.U32Array -> MetadataU32ArrayValue.serializer() MetadataValueType.U64Array -> MetadataU64ArrayValue.serializer() MetadataValueType.I32Array -> MetadataI32ArrayValue.serializer() MetadataValueType.I64Array -> MetadataI64ArrayValue.serializer() diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/DAppEntity.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/DAppEntity.kt index b7b18fa74e..b6ded0778b 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/DAppEntity.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/DAppEntity.kt @@ -3,7 +3,6 @@ package com.babylon.wallet.android.data.repository.cache.database import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.babylon.wallet.android.data.gateway.extensions.toMetadata import com.babylon.wallet.android.data.gateway.generated.models.StateEntityDetailsResponseItem import com.radixdlt.sargon.AccountAddress import com.radixdlt.sargon.extensions.init @@ -27,7 +26,10 @@ data class DAppEntity( companion object { fun from(item: StateEntityDetailsResponseItem, syncedAt: Instant) = DAppEntity( definitionAddress = AccountAddress.init(item.address), - metadata = item.explicitMetadata?.toMetadata()?.let { MetadataColumn(it) }, + metadata = MetadataColumn.from( + explicitMetadata = item.explicitMetadata, + implicitMetadata = item.metadata + ), synced = syncedAt ) } diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/NFTEntity.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/NFTEntity.kt index ac89cc3324..e1692d3cab 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/NFTEntity.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/NFTEntity.kt @@ -37,7 +37,7 @@ data class NFTEntity( metadata = toMetadata().takeIf { it.isNotEmpty() }?.let { - MetadataColumn(it) + MetadataColumn(metadata = it, implicitState = MetadataColumn.ImplicitMetadataState.Complete) }, synced = synced ) @@ -49,7 +49,7 @@ data class NFTEntity( metadata = metadata.takeIf { it.isNotEmpty() }?.let { - MetadataColumn(it) + MetadataColumn(metadata = it, implicitState = MetadataColumn.ImplicitMetadataState.Complete) }, synced = synced ) diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/PoolEntity.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/PoolEntity.kt index e69bdde5ce..972e4de6f1 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/PoolEntity.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/PoolEntity.kt @@ -6,7 +6,6 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import com.babylon.wallet.android.data.gateway.extensions.PoolsResponse -import com.babylon.wallet.android.data.gateway.extensions.toMetadata import com.babylon.wallet.android.data.gateway.generated.models.StateEntityDetailsResponseItem import com.babylon.wallet.android.data.repository.cache.database.PoolResourceJoin.Companion.asPoolResourceJoin import com.babylon.wallet.android.data.repository.cache.database.ResourceEntity.Companion.asEntity @@ -60,11 +59,14 @@ data class PoolEntity( } fun StateEntityDetailsResponseItem.asPoolEntity(): PoolEntity? { - val metadata = this.metadata.toMetadata() - val poolUnitResourceAddress = metadata.poolUnit() ?: return null + val metadataColumn = MetadataColumn.from( + explicitMetadata = explicitMetadata, + implicitMetadata = metadata + ) + val poolUnitResourceAddress = metadataColumn.metadata.poolUnit() ?: return null return PoolEntity( address = PoolAddress.init(address), - metadata = MetadataColumn(metadata), + metadata = metadataColumn, resourceAddress = poolUnitResourceAddress ) } diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ProvidedConverters.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ProvidedConverters.kt index 9981253788..bffd706390 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ProvidedConverters.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ProvidedConverters.kt @@ -2,6 +2,9 @@ package com.babylon.wallet.android.data.repository.cache.database import androidx.room.ProvidedTypeConverter import androidx.room.TypeConverter +import com.babylon.wallet.android.data.gateway.extensions.toMetadata +import com.babylon.wallet.android.data.gateway.generated.models.EntityMetadataCollection +import com.babylon.wallet.android.data.repository.cache.database.MetadataColumn.ImplicitMetadataState import com.radixdlt.sargon.AccountAddress import com.radixdlt.sargon.Decimal192 import com.radixdlt.sargon.NonFungibleLocalId @@ -12,6 +15,7 @@ import com.radixdlt.sargon.VaultAddress import com.radixdlt.sargon.extensions.init import com.radixdlt.sargon.extensions.string import com.radixdlt.sargon.extensions.toDecimal192OrNull +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -24,7 +28,75 @@ import java.time.Instant data class BehavioursColumn(val behaviours: Set) @Serializable -data class MetadataColumn(val metadata: List) +data class MetadataColumn( + /** + * A union of explicit and **currently known** implicit metadata + * As a client we don't need to expose the difference between explicit + * and implicit metadata. + */ + val metadata: List, + + /** + * The state of the next page of the implicit metadata. See [ImplicitMetadataState] + */ + @SerialName("implicit_state") + val implicitState: ImplicitMetadataState +) { + + val nextCursor: String? + get() = (implicitState as? ImplicitMetadataState.Incomplete)?.nextCursor + + @Serializable + sealed interface ImplicitMetadataState { + /** + * When we have no information yet regarding the existence of implicit metadata + * An example is when we query account information. In this request we receive + * resource data but with no information about implicit metadata. In order to make + * sure we received all metadata available we need to fetch details of this specific + * resource + */ + @Serializable + @SerialName("unknown") + data object Unknown : ImplicitMetadataState + + /** + * We have received an answer from a details request and we know that the [MetadataColumn.metadata] + * are complete. + */ + @Serializable + @SerialName("complete") + data object Complete : ImplicitMetadataState + + /** + * We have received an answer from a details request and we know that the [MetadataColumn.metadata] + * are incomplete. We need to query [nextCursor] to receive more. + */ + @Serializable + @SerialName("incomplete") + data class Incomplete( + @SerialName("next_cursor") + val nextCursor: String + ) : ImplicitMetadataState + } + + companion object { + fun from( + explicitMetadata: EntityMetadataCollection?, + implicitMetadata: EntityMetadataCollection + ): MetadataColumn { + val explicit = explicitMetadata?.toMetadata().orEmpty().toSet() + val implicit = implicitMetadata.toMetadata().toSet() + + val all = explicit union implicit + return MetadataColumn( + metadata = all.toList(), + implicitState = implicitMetadata.nextCursor?.let { + ImplicitMetadataState.Incomplete(nextCursor = it) + } ?: ImplicitMetadataState.Complete + ) + } + } +} @Suppress("TooManyFunctions") @ProvidedTypeConverter @@ -37,23 +109,23 @@ class StateDatabaseConverters { // Behaviours @TypeConverter fun stringToBehaviours(string: String?): BehavioursColumn? { - return string?.let { BehavioursColumn(behaviours = json.decodeFromString(string)) } + return string?.let { json.decodeFromString(string) } } @TypeConverter fun behavioursToString(column: BehavioursColumn?): String? { - return column?.let { json.encodeToString(it.behaviours) } + return column?.let { json.encodeToString(it) } } // Metadata @TypeConverter fun stringToMetadata(string: String?): MetadataColumn? { - return string?.let { MetadataColumn(metadata = json.decodeFromString(string)) } + return string?.let { json.decodeFromString(string) } } @TypeConverter fun metadataToString(column: MetadataColumn?): String? { - return column?.let { json.encodeToString(it.metadata) } + return column?.let { json.encodeToString(it) } } // Decimal192 diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ResourceEntity.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ResourceEntity.kt index cf7f2625a8..214ce47aae 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ResourceEntity.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ResourceEntity.kt @@ -22,10 +22,7 @@ import com.radixdlt.sargon.extensions.init import com.radixdlt.sargon.extensions.string import com.radixdlt.sargon.extensions.toDecimal192 import rdx.works.core.domain.resources.Divisibility -import rdx.works.core.domain.resources.ExplicitMetadataKey import rdx.works.core.domain.resources.Resource -import rdx.works.core.domain.resources.metadata.Metadata -import rdx.works.core.domain.resources.metadata.MetadataType import rdx.works.core.domain.resources.metadata.poolAddress import rdx.works.core.domain.resources.metadata.validatorAddress import java.time.Instant @@ -50,17 +47,14 @@ data class ResourceEntity( val synced: Instant ) { + val isDetailsAvailable: Boolean + get() = when (type) { + ResourceEntityType.FUNGIBLE -> supply != null && divisibility != null && behaviours != null + ResourceEntityType.NON_FUNGIBLE -> supply != null && behaviours != null + } + @Suppress("CyclomaticComplexMethod") fun toResource(amount: Decimal192?): Resource { - val validatorAndPoolMetadata = listOf( - validatorAddress?.let { - Metadata.Primitive(ExplicitMetadataKey.VALIDATOR.key, it.string, MetadataType.Address) - }, - poolAddress?.let { - Metadata.Primitive(ExplicitMetadataKey.POOL.key, it.string, MetadataType.Address) - } - ).mapNotNull { it } - return when (type) { ResourceEntityType.FUNGIBLE -> { Resource.FungibleResource( @@ -69,7 +63,7 @@ data class ResourceEntity( assetBehaviours = behaviours?.behaviours?.toSet(), currentSupply = supply, divisibility = divisibility, - metadata = metadata?.metadata.orEmpty() + validatorAndPoolMetadata + metadata = metadata?.metadata.orEmpty() ) } @@ -80,7 +74,7 @@ data class ResourceEntity( assetBehaviours = behaviours?.behaviours?.toSet(), items = emptyList(), currentSupply = supply?.string?.toIntOrNull(), - metadata = metadata?.metadata.orEmpty() + validatorAndPoolMetadata + metadata = metadata?.metadata.orEmpty() ) } } @@ -97,9 +91,8 @@ data class ResourceEntity( validatorAddress = metadata.validatorAddress(), poolAddress = metadata.poolAddress(), metadata = metadata - .filterNot { it.key in setOf(ExplicitMetadataKey.POOL.key, ExplicitMetadataKey.VALIDATOR.key) } .takeIf { it.isNotEmpty() } - ?.let { MetadataColumn(it) }, + ?.let { MetadataColumn(it, MetadataColumn.ImplicitMetadataState.Unknown) }, synced = synced ) @@ -112,9 +105,8 @@ data class ResourceEntity( validatorAddress = metadata.validatorAddress(), poolAddress = metadata.poolAddress(), metadata = metadata - .filterNot { it.key in setOf(ExplicitMetadataKey.POOL.key, ExplicitMetadataKey.VALIDATOR.key) } .takeIf { it.isNotEmpty() } - ?.let { MetadataColumn(it) }, + ?.let { MetadataColumn(it, MetadataColumn.ImplicitMetadataState.Unknown) }, synced = synced ) } @@ -126,7 +118,8 @@ data class ResourceEntity( details: StateEntityDetailsResponseItemDetails? = null ): ResourceEntity = from( address = ResourceAddress.init(resourceAddress), - metadataCollection = explicitMetadata, + explicitMetadata = explicitMetadata, + implicitMetadata = null, details = details, type = ResourceEntityType.FUNGIBLE, synced = synced @@ -139,7 +132,8 @@ data class ResourceEntity( details: StateEntityDetailsResponseItemDetails? = null ): ResourceEntity = from( address = ResourceAddress.init(resourceAddress), - metadataCollection = explicitMetadata, + explicitMetadata = explicitMetadata, + implicitMetadata = null, details = details, type = ResourceEntityType.NON_FUNGIBLE, synced = synced @@ -156,33 +150,44 @@ data class ResourceEntity( } return from( address = ResourceAddress.init(address), - metadataCollection = metadata, + explicitMetadata = explicitMetadata, + implicitMetadata = metadata, details = details, type = type, synced = synced ) } + @Suppress("LongParameterList") private fun from( address: ResourceAddress, - metadataCollection: EntityMetadataCollection?, + explicitMetadata: EntityMetadataCollection?, + implicitMetadata: EntityMetadataCollection?, details: StateEntityDetailsResponseItemDetails?, type: ResourceEntityType, synced: Instant ): ResourceEntity { - val metadata = metadataCollection?.toMetadata().orEmpty() + val metadataColumn = if (implicitMetadata != null) { + MetadataColumn.from( + explicitMetadata = explicitMetadata, + implicitMetadata = implicitMetadata + ) + } else { + MetadataColumn( + metadata = explicitMetadata?.toMetadata().orEmpty(), + implicitState = MetadataColumn.ImplicitMetadataState.Unknown + ) + }.takeIf { it.metadata.isNotEmpty() } + return ResourceEntity( address = address, type = type, divisibility = details?.divisibility(), behaviours = details?.let { BehavioursColumn(it.extractBehaviours()) }, supply = details?.totalSupply(), - validatorAddress = metadata.validatorAddress(), - poolAddress = metadata.poolAddress(), - metadata = metadata - .filterNot { it.key in setOf(ExplicitMetadataKey.VALIDATOR.key, ExplicitMetadataKey.POOL.key) } - .takeIf { it.isNotEmpty() } - ?.let { MetadataColumn(it) }, + validatorAddress = metadataColumn?.metadata?.validatorAddress(), + poolAddress = metadataColumn?.metadata?.poolAddress(), + metadata = metadataColumn, synced = synced ) } diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDao.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDao.kt index 2156845bdc..04f596388d 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDao.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDao.kt @@ -324,6 +324,15 @@ interface StateDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertDApps(dApps: List) + @Query( + """ + UPDATE ResourceEntity SET + metadata = :metadataColumn + WHERE address = :resourceAddress + """ + ) + fun updateMetadata(resourceAddress: ResourceAddress, metadataColumn: MetadataColumn) + companion object { val deleteDuration = 1.toDuration(DurationUnit.SECONDS) private val accountsCacheDuration = 2.toDuration(DurationUnit.HOURS) diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDatabase.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDatabase.kt index 90ef12b87d..98acbfa299 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDatabase.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/StateDatabase.kt @@ -20,7 +20,7 @@ import androidx.room.TypeConverters PoolDAppJoin::class, TokenPriceEntity::class ], - version = StateDatabase.VERSION_8 + version = StateDatabase.VERSION_9 ) @TypeConverters(StateDatabaseConverters::class) abstract class StateDatabase : RoomDatabase() { @@ -51,9 +51,12 @@ abstract class StateDatabase : RoomDatabase() { @Deprecated("Add TokenPriceEntity to schema") const val VERSION_7 = 7 - // Replace BigDecimal with Decimal192 + @Deprecated("Replace BigDecimal with Decimal192") const val VERSION_8 = 8 + // Added next cursor to metadata column and locked flag + const val VERSION_9 = 9 + private const val NAME = "STATE_DATABASE" fun factory(applicationContext: Context): StateDatabase = Room diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ValidatorEntity.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ValidatorEntity.kt index d48548e363..8c67d5d5c5 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ValidatorEntity.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/cache/database/ValidatorEntity.kt @@ -5,7 +5,6 @@ import androidx.room.Entity import androidx.room.PrimaryKey import com.babylon.wallet.android.data.gateway.extensions.claimTokenResourceAddress import com.babylon.wallet.android.data.gateway.extensions.stakeUnitResourceAddress -import com.babylon.wallet.android.data.gateway.extensions.toMetadata import com.babylon.wallet.android.data.gateway.extensions.totalXRDStake import com.babylon.wallet.android.data.gateway.generated.models.StateEntityDetailsResponseItem import com.radixdlt.sargon.Decimal192 @@ -38,28 +37,16 @@ data class ValidatorEntity( ) companion object { - fun Validator.asValidatorEntity(syncInfo: SyncInfo) = ValidatorEntity( - address = address, - stakeUnitResourceAddress = stakeUnitResourceAddress, - claimTokenResourceAddress = claimTokenResourceAddress, - totalStake = totalXrdStake, - metadata = metadata.takeIf { it.isNotEmpty() }?.let { MetadataColumn(it) }, + fun StateEntityDetailsResponseItem.asValidatorEntity(syncInfo: SyncInfo) = ValidatorEntity( + address = ValidatorAddress.init(address), + stakeUnitResourceAddress = details?.stakeUnitResourceAddress?.let { ResourceAddress.init(it) }, + claimTokenResourceAddress = details?.claimTokenResourceAddress?.let { ResourceAddress.init(it) }, + totalStake = totalXRDStake, + metadata = MetadataColumn.from( + explicitMetadata = explicitMetadata, + implicitMetadata = metadata + ), stateVersion = syncInfo.accountStateVersion ) - - fun List.asValidators() = map { item -> - val metadata = item.explicitMetadata?.toMetadata().orEmpty() - Validator( - address = ValidatorAddress.init(item.address), - totalXrdStake = item.totalXRDStake, - stakeUnitResourceAddress = item.details?.stakeUnitResourceAddress?.let { ResourceAddress.init(it) }, - claimTokenResourceAddress = item.details?.claimTokenResourceAddress?.let { ResourceAddress.init(it) }, - metadata = metadata - ) - } - - fun List.asValidatorEntities(syncInfo: SyncInfo) = map { item -> - item.asValidatorEntity(syncInfo) - } } } diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/state/AccountsStateCache.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/state/AccountsStateCache.kt index f07d9ac0ce..ff628ce264 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/state/AccountsStateCache.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/state/AccountsStateCache.kt @@ -11,8 +11,7 @@ import com.babylon.wallet.android.data.repository.cache.database.StateDao import com.babylon.wallet.android.data.repository.cache.database.StateDao.Companion.accountCacheValidity import com.babylon.wallet.android.data.repository.cache.database.StateDatabase import com.babylon.wallet.android.data.repository.cache.database.SyncInfo -import com.babylon.wallet.android.data.repository.cache.database.ValidatorEntity.Companion.asValidatorEntities -import com.babylon.wallet.android.data.repository.cache.database.ValidatorEntity.Companion.asValidators +import com.babylon.wallet.android.data.repository.cache.database.ValidatorEntity.Companion.asValidatorEntity import com.babylon.wallet.android.data.repository.cache.database.getCachedPools import com.babylon.wallet.android.data.repository.cache.database.getCachedValidators import com.babylon.wallet.android.di.coroutines.ApplicationScope @@ -234,57 +233,47 @@ class AccountsStateCache @Inject constructor( result } - private fun Flow>.compileAccountAddressAssets(): Flow> = - transform { cached -> - val stateVersion = cached.values.mapNotNull { it.stateVersion }.maxOrNull() ?: run { - emit(emptyList()) - return@transform + private fun Flow>.compileAccountAddressAssets() = transform { cached -> + val stateVersion = cached.values.mapNotNull { it.stateVersion }.maxOrNull() ?: run { + emit(emptyList()) + return@transform + } + + val allValidatorAddresses = cached.map { it.value.validatorAddresses() }.flatten().toSet() + val cachedValidators = dao.getCachedValidators(allValidatorAddresses, stateVersion).toMutableMap() + val newValidators = runCatching { + val validatorItems = api.fetchValidators( + allValidatorAddresses - cachedValidators.keys, + stateVersion + ).validators + + val syncInfo = SyncInfo(InstantGenerator(), stateVersion) + validatorItems.map { + it.asValidatorEntity(syncInfo) + }.onEach { entity -> + cachedValidators[entity.address] = entity.asValidatorDetail() } + }.onFailure { cacheErrors.value = it }.getOrNull() ?: return@transform - val allValidatorAddresses = cached.map { it.value.validatorAddresses() }.flatten().toSet() - val cachedValidators = dao.getCachedValidators(allValidatorAddresses, stateVersion).toMutableMap() - val newValidators = runCatching { - api.fetchValidators( - allValidatorAddresses - cachedValidators.keys, - stateVersion - ).validators.asValidators().onEach { - cachedValidators[it.address] = it - } + if (newValidators.isNotEmpty()) { + logger.d("\uD83D\uDCBD Inserting validators") + dao.insertValidators(newValidators) + } + + val allPoolAddresses = cached.map { it.value.poolAddresses() }.flatten().toSet() + val cachedPools = dao.getCachedPools(allPoolAddresses, stateVersion).toMutableMap() + val unknownPools = allPoolAddresses - cachedPools.keys + if (unknownPools.isNotEmpty()) { + logger.d("\uD83D\uDCBD Inserting pools") + val newPools = runCatching { + api.fetchPools(unknownPools.toSet(), stateVersion) }.onFailure { error -> cacheErrors.value = error }.getOrNull() ?: return@transform - if (newValidators.isNotEmpty()) { - logger.d("\uD83D\uDCBD Inserting validators") - dao.insertValidators(newValidators.asValidatorEntities(SyncInfo(InstantGenerator(), stateVersion))) - } - - val allPoolAddresses = cached.map { it.value.poolAddresses() }.flatten().toSet() - val cachedPools = dao.getCachedPools(allPoolAddresses, stateVersion).toMutableMap() - val unknownPools = allPoolAddresses - cachedPools.keys - if (unknownPools.isNotEmpty()) { - logger.d("\uD83D\uDCBD Inserting pools") - - val newPools = runCatching { - api.fetchPools(unknownPools.toSet(), stateVersion) - }.onFailure { error -> - cacheErrors.value = error - }.getOrNull() ?: return@transform - - if (newPools.poolItems.isNotEmpty()) { - val join = newPools.poolItems.asPoolsResourcesJoin(SyncInfo(InstantGenerator(), stateVersion)) - dao.updatePools(pools = join) - } else { - emit( - cached.mapNotNull { - it.value.toAccountAddressWithAssets( - accountAddress = it.key, - pools = cachedPools, - validators = cachedValidators - ) - } - ) - } + if (newPools.poolItems.isNotEmpty()) { + val join = newPools.poolItems.asPoolsResourcesJoin(SyncInfo(InstantGenerator(), stateVersion)) + dao.updatePools(pools = join) } else { emit( cached.mapNotNull { @@ -296,7 +285,18 @@ class AccountsStateCache @Inject constructor( } ) } + } else { + emit( + cached.mapNotNull { + it.value.toAccountAddressWithAssets( + accountAddress = it.key, + pools = cachedPools, + validators = cachedValidators + ) + } + ) } + } private data class AccountCachedData( val stateVersion: Long?, diff --git a/app/src/main/java/com/babylon/wallet/android/data/repository/state/StateRepository.kt b/app/src/main/java/com/babylon/wallet/android/data/repository/state/StateRepository.kt index 4b0c225141..2a3d5989b4 100644 --- a/app/src/main/java/com/babylon/wallet/android/data/repository/state/StateRepository.kt +++ b/app/src/main/java/com/babylon/wallet/android/data/repository/state/StateRepository.kt @@ -3,11 +3,13 @@ package com.babylon.wallet.android.data.repository.state import com.babylon.wallet.android.data.gateway.apis.StateApi import com.babylon.wallet.android.data.gateway.extensions.fetchPools import com.babylon.wallet.android.data.gateway.extensions.fetchValidators +import com.babylon.wallet.android.data.gateway.extensions.getAllMetadata import com.babylon.wallet.android.data.gateway.extensions.getNextNftItems import com.babylon.wallet.android.data.gateway.extensions.paginateDetails import com.babylon.wallet.android.data.gateway.extensions.paginateNonFungibles import com.babylon.wallet.android.data.gateway.extensions.toMetadata import com.babylon.wallet.android.data.repository.cache.database.DAppEntity +import com.babylon.wallet.android.data.repository.cache.database.MetadataColumn import com.babylon.wallet.android.data.repository.cache.database.NFTEntity.Companion.asEntity import com.babylon.wallet.android.data.repository.cache.database.PoolEntity.Companion.asPoolsResourcesJoin import com.babylon.wallet.android.data.repository.cache.database.ResourceEntity @@ -17,7 +19,6 @@ import com.babylon.wallet.android.data.repository.cache.database.StateDao.Compan import com.babylon.wallet.android.data.repository.cache.database.StateDao.Companion.resourcesCacheValidity import com.babylon.wallet.android.data.repository.cache.database.SyncInfo import com.babylon.wallet.android.data.repository.cache.database.ValidatorEntity.Companion.asValidatorEntity -import com.babylon.wallet.android.data.repository.cache.database.ValidatorEntity.Companion.asValidators import com.babylon.wallet.android.data.repository.cache.database.getCachedPools import com.babylon.wallet.android.data.repository.cache.database.storeAccountNFTsPortfolio import com.babylon.wallet.android.data.repository.cache.database.updateResourceDetails @@ -70,7 +71,8 @@ interface StateRepository { suspend fun getResources( addresses: Set, underAccountAddress: AccountAddress?, - withDetails: Boolean + withDetails: Boolean, + withAllMetadata: Boolean ): Result> suspend fun getPools(poolAddresses: Set): Result> @@ -318,23 +320,18 @@ class StateRepositoryImpl @Inject constructor( override suspend fun getResources( addresses: Set, underAccountAddress: AccountAddress?, - withDetails: Boolean + withDetails: Boolean, + withAllMetadata: Boolean ): Result> = withContext(dispatcher) { runCatching { - val addressesWithResources = addresses.associateWith { address -> - val cachedEntity = stateDao.getResourceDetails( + val addressesWithResourceEntities = addresses.associateWith { address -> + stateDao.getResourceDetails( resourceAddress = address, minValidity = resourcesCacheValidity() ) - - val amount = underAccountAddress?.let { accountAddress -> - stateDao.getAccountResourceJoin(resourceAddress = address, accountAddress = accountAddress)?.amount - } - - cachedEntity?.toResource(amount) }.toMutableMap() - val resourcesToFetch = addressesWithResources.mapNotNull { entry -> + val resourcesToFetch = addressesWithResourceEntities.mapNotNull { entry -> val cachedResource = entry.value if (cachedResource == null || !cachedResource.isDetailsAvailable && withDetails) entry.key else null } @@ -344,22 +341,48 @@ class StateRepositoryImpl @Inject constructor( metadataKeys = ExplicitMetadataKey.forAssets, onPage = { page -> page.items.forEach { item -> - val amount = underAccountAddress?.let { accountAddress -> - stateDao.getAccountResourceJoin( - resourceAddress = ResourceAddress.init(item.address), - accountAddress = accountAddress - )?.amount - } val updatedEntity = stateDao.updateResourceDetails(item) - val resource = updatedEntity.toResource(amount) - addressesWithResources[resource.address] = resource + addressesWithResourceEntities[updatedEntity.address] = updatedEntity } } ) } - addressesWithResources.values.filterNotNull() + addressesWithResourceEntities.values.filterNotNull().map { resourceEntity -> + val amount = underAccountAddress?.let { accountAddress -> + stateDao.getAccountResourceJoin(resourceAddress = resourceEntity.address, accountAddress = accountAddress)?.amount + } + + val nextMetadataCursor = resourceEntity.metadata?.nextCursor + if (withAllMetadata && nextMetadataCursor != null) { + val remainingMetadata = runCatching { + val stateVersion = requireNotNull(getLatestCachedStateVersionInNetwork()) + + stateApi.getAllMetadata( + resourceAddress = resourceEntity.address, + stateVersion = stateVersion, + initialCursor = nextMetadataCursor + ) + }.getOrNull()?.mapNotNull { it.toMetadata() }?.takeIf { it.isNotEmpty() }?.toSet() + + if (remainingMetadata != null) { + resourceEntity.copy( + metadata = resourceEntity.metadata.metadata.toMutableSet().apply { + this union remainingMetadata + }.let { + MetadataColumn(metadata = it.toList(), implicitState = MetadataColumn.ImplicitMetadataState.Complete) + }.also { + stateDao.updateMetadata(resourceAddress = resourceEntity.address, metadataColumn = it) + } + ) + } else { + resourceEntity + } + } else { + resourceEntity + }.toResource(amount) + } } } @@ -395,9 +418,7 @@ class StateRepositoryImpl @Inject constructor( runCatching { val stateVersion = getLatestCachedStateVersionInNetwork() val cachedValidators = if (stateVersion != null) { - stateDao.getValidators(addresses = validatorAddresses.toSet(), atStateVersion = stateVersion).map { - it.asValidatorDetail() - } + stateDao.getValidators(addresses = validatorAddresses.toSet(), atStateVersion = stateVersion) } else { emptyList() } @@ -408,15 +429,17 @@ class StateRepositoryImpl @Inject constructor( validatorsAddresses = unknownAddresses, stateVersion = stateVersion ) - val details = response.validators.asValidators() - if (details.isNotEmpty()) { - val syncInfo = SyncInfo(InstantGenerator(), requireNotNull(response.stateVersion)) - stateDao.insertValidators(details.map { it.asValidatorEntity(syncInfo) }) + + val syncInfo = SyncInfo(InstantGenerator(), requireNotNull(response.stateVersion)) + val newValidatorEntities = response.validators.map { it.asValidatorEntity(syncInfo) }.also { entities -> + stateDao.insertValidators(entities) } - details + cachedValidators + newValidatorEntities + cachedValidators } else { cachedValidators } + }.mapCatching { entities -> + entities.map { it.asValidatorDetail() } } } diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetDAppWithResourcesUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetDAppWithResourcesUseCase.kt index de6170a35f..e0cd36d334 100644 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetDAppWithResourcesUseCase.kt +++ b/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetDAppWithResourcesUseCase.kt @@ -26,7 +26,8 @@ class GetDAppWithResourcesUseCase @Inject constructor( val resources = stateRepository.getResources( addresses = claimedResources.toSet(), underAccountAddress = null, - withDetails = false + withDetails = false, + withAllMetadata = false ).getOrNull().orEmpty() DAppWithResources( diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetResourcesUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetResourcesUseCase.kt index 748614c112..21d4231adf 100644 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetResourcesUseCase.kt +++ b/app/src/main/java/com/babylon/wallet/android/domain/usecases/GetResourcesUseCase.kt @@ -8,6 +8,13 @@ class GetResourcesUseCase @Inject constructor( private val stateRepository: StateRepository ) { - suspend operator fun invoke(addresses: Set, withDetails: Boolean = false) = - stateRepository.getResources(addresses = addresses, underAccountAddress = null, withDetails = withDetails) + suspend operator fun invoke( + addresses: Set, + withDetails: Boolean = false + ) = stateRepository.getResources( + addresses = addresses, + underAccountAddress = null, + withDetails = withDetails, + withAllMetadata = false + ) } diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/GetLSUDetailsUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/GetLSUDetailsUseCase.kt deleted file mode 100644 index 47d8be8193..0000000000 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/GetLSUDetailsUseCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.babylon.wallet.android.domain.usecases.assets - -import com.babylon.wallet.android.data.repository.state.StateRepository -import com.radixdlt.sargon.AccountAddress -import com.radixdlt.sargon.ResourceAddress -import rdx.works.core.domain.assets.LiquidStakeUnit -import rdx.works.core.domain.assets.ValidatorWithStakes -import rdx.works.core.domain.resources.Resource -import rdx.works.core.then -import java.lang.RuntimeException -import javax.inject.Inject - -/** - * Returns the LSU with validator details. Currently this use case does not return the associated claims. - */ -class GetLSUDetailsUseCase @Inject constructor( - private val stateRepository: StateRepository -) { - - suspend operator fun invoke(resourceAddress: ResourceAddress, accountAddress: AccountAddress): Result = - stateRepository.getResources( - addresses = setOf(resourceAddress), - underAccountAddress = accountAddress, - withDetails = true - ).mapCatching { - it.first() as Resource.FungibleResource - }.then { stake -> - val validatorAddress = stake.validatorAddress - ?: return@then Result.failure(RuntimeException("Resource $resourceAddress has no associated validator")) - stateRepository.getValidators(validatorAddresses = setOf(validatorAddress)).mapCatching { validators -> - val validator = validators.first() - ValidatorWithStakes( - validator = validator, - liquidStakeUnit = LiquidStakeUnit(stake, validator) - ) - } - } -} diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/ObserveResourceUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/ObserveResourceUseCase.kt deleted file mode 100644 index a2bcdc9b7f..0000000000 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/ObserveResourceUseCase.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.babylon.wallet.android.domain.usecases.assets - -import com.babylon.wallet.android.data.repository.state.StateRepository -import com.radixdlt.sargon.AccountAddress -import com.radixdlt.sargon.ResourceAddress -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import rdx.works.core.domain.resources.Resource -import javax.inject.Inject - -/** - * Fetches details regarding this fungible/non-fungible by looking it up through its address. - * If an account address is provided, the repository will try to also fetch amount information. - */ -class ObserveResourceUseCase @Inject constructor( - private val stateRepository: StateRepository -) { - - operator fun invoke( - resourceAddress: ResourceAddress, - accountAddress: AccountAddress? = null, - withDetails: Boolean = true - ): Flow = flow { - val resource = stateRepository.getResources( - addresses = setOf(resourceAddress), - underAccountAddress = accountAddress, - withDetails = false - ).getOrThrow().first() - - emit(resource) - - if (!resource.isDetailsAvailable && withDetails) { - val resourceWithDetails = stateRepository.getResources( - addresses = setOf(resourceAddress), - underAccountAddress = accountAddress, - withDetails = true - ).getOrThrow().first() - - emit(resourceWithDetails) - } - } -} diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/ResolveAssetsFromAddressUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/ResolveAssetsFromAddressUseCase.kt index 75ecff1798..fe61486e2e 100644 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/ResolveAssetsFromAddressUseCase.kt +++ b/app/src/main/java/com/babylon/wallet/android/domain/usecases/assets/ResolveAssetsFromAddressUseCase.kt @@ -21,12 +21,14 @@ class ResolveAssetsFromAddressUseCase @Inject constructor( ) { suspend operator fun invoke( fungibleAddresses: Set, - nonFungibleIds: Map> + nonFungibleIds: Map>, + withAllMetadata: Boolean = false ): Result> = stateRepository .getResources( addresses = fungibleAddresses + nonFungibleIds.keys, underAccountAddress = null, - withDetails = true + withDetails = true, + withAllMetadata = withAllMetadata ).mapCatching { resources -> val nfts = nonFungibleIds.mapValues { entry -> stateRepository.getNFTDetails(entry.key, entry.value.toSet()).getOrThrow() diff --git a/app/src/main/java/com/babylon/wallet/android/domain/usecases/transaction/GetTransactionBadgesUseCase.kt b/app/src/main/java/com/babylon/wallet/android/domain/usecases/transaction/GetTransactionBadgesUseCase.kt index 020f794add..70de3905cb 100644 --- a/app/src/main/java/com/babylon/wallet/android/domain/usecases/transaction/GetTransactionBadgesUseCase.kt +++ b/app/src/main/java/com/babylon/wallet/android/domain/usecases/transaction/GetTransactionBadgesUseCase.kt @@ -14,7 +14,8 @@ class GetTransactionBadgesUseCase @Inject constructor( ): Result> = stateRepository.getResources( addresses = addresses, underAccountAddress = null, - withDetails = false + withDetails = false, + withAllMetadata = false ).mapCatching { resources -> resources.map { Badge( diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/composable/AssetMetadataRow.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/composable/AssetMetadataRow.kt deleted file mode 100644 index bb64f10e4e..0000000000 --- a/app/src/main/java/com/babylon/wallet/android/presentation/account/composable/AssetMetadataRow.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.babylon.wallet.android.presentation.account.composable - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import com.babylon.wallet.android.R -import com.babylon.wallet.android.designsystem.theme.RadixTheme -import com.babylon.wallet.android.presentation.ui.composables.ExpandableText -import com.babylon.wallet.android.presentation.ui.composables.actionableaddress.ActionableAddressView -import com.babylon.wallet.android.utils.openUrl -import com.radixdlt.sargon.Address -import com.radixdlt.sargon.NonFungibleGlobalId -import com.radixdlt.sargon.NonFungibleLocalId -import com.radixdlt.sargon.extensions.formatted -import com.radixdlt.sargon.extensions.init -import com.radixdlt.sargon.extensions.toDecimal192OrNull -import rdx.works.core.domain.resources.metadata.Metadata -import rdx.works.core.domain.resources.metadata.MetadataType -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import java.util.Locale - -@Composable -fun AssetMetadataRow( - modifier: Modifier, - key: String, - valueView: @Composable RowScope.() -> Unit -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.padding(end = RadixTheme.dimensions.paddingMedium), - text = key.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() - }, - style = RadixTheme.typography.body1Regular, - color = RadixTheme.colors.gray2 - ) - - valueView() - } -} - -@Composable -fun Metadata.View(modifier: Modifier) { - if (isRenderedInNewLine) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(RadixTheme.dimensions.paddingSmall) - ) { - KeyView() - ValueView(isRenderedInNewLine = true) - } - } else { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - KeyView() - Spacer(modifier = Modifier.width(RadixTheme.dimensions.paddingMedium)) - ValueView(isRenderedInNewLine = false) - } - } -} - -@Composable -fun Metadata.KeyView( - modifier: Modifier = Modifier -) { - Text( - modifier = modifier.padding(end = RadixTheme.dimensions.paddingMedium), - text = key, - style = RadixTheme.typography.body1Regular, - color = RadixTheme.colors.gray2, - textAlign = TextAlign.Start - ) -} - -@Suppress("CyclomaticComplexMethod") -@Composable -fun Metadata.ValueView( - modifier: Modifier = Modifier, - isRenderedInNewLine: Boolean -) { - val context = LocalContext.current - when (this) { - is Metadata.Collection, is Metadata.Map -> Text( - modifier = modifier, - text = stringResource(id = R.string.assetDetails_NFTDetails_complexData), - style = RadixTheme.typography.body1HighImportance, - color = RadixTheme.colors.gray1, - textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, - maxLines = 2 - ) - - is Metadata.Primitive -> when (valueType) { - MetadataType.Bool, - is MetadataType.Integer, - MetadataType.Bytes, - MetadataType.Enum, - MetadataType.PublicKeyEcdsaSecp256k1, - MetadataType.PublicKeyEddsaEd25519, - MetadataType.PublicKeyHashEcdsaSecp256k1, - MetadataType.PublicKeyHashEddsaEd25519 -> Text( - modifier = modifier, - text = value, - style = RadixTheme.typography.body1HighImportance, - color = RadixTheme.colors.gray1, - textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, - maxLines = 2 - ) - - MetadataType.String -> ExpandableText( - modifier = modifier, - text = value, - style = RadixTheme.typography.body1HighImportance.copy( - color = RadixTheme.colors.gray1, - textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, - ), - toggleStyle = RadixTheme.typography.body1HighImportance.copy( - color = RadixTheme.colors.gray2 - ), - ) - - MetadataType.Instant -> { - val displayable = remember(value) { - val epochSeconds = value.toLongOrNull() ?: return@remember value - val dateTime = Instant.ofEpochSecond(epochSeconds) - .atZone(ZoneId.systemDefault()) - .toLocalDateTime() - - dateTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) - } - Text( - modifier = modifier, - text = displayable, - style = RadixTheme.typography.body1HighImportance, - color = RadixTheme.colors.gray1, - textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, - maxLines = 2 - ) - } - - MetadataType.Address -> ActionableAddressView( - modifier = modifier, - address = remember(value) { - Address.init(value) - } - ) - - MetadataType.NonFungibleGlobalId -> ActionableAddressView( - modifier = modifier, - globalId = remember(value) { - NonFungibleGlobalId.init(value) - } - ) - - MetadataType.NonFungibleLocalId -> ActionableAddressView( - modifier = modifier, - localId = remember(value) { - NonFungibleLocalId.init(value) - } - ) - - MetadataType.Decimal -> Text( - modifier = modifier, - // If value is unable to transform to big decimal we just display raw value - text = value.toDecimal192OrNull()?.formatted() ?: value, - style = RadixTheme.typography.body1HighImportance, - color = RadixTheme.colors.gray1, - textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, - maxLines = 2 - ) - - MetadataType.Url -> Row( - modifier = modifier - .fillMaxWidth() - .clickable { context.openUrl(value) }, - horizontalArrangement = Arrangement.spacedBy(RadixTheme.dimensions.paddingDefault), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = value, - style = RadixTheme.typography.body1StandaloneLink, - color = RadixTheme.colors.blue1 - ) - Icon( - painter = painterResource(id = R.drawable.ic_external_link), - contentDescription = null, - tint = RadixTheme.colors.gray3 - ) - } - } - } -} - -private const val ASSET_METADATA_SHORT_STRING_THRESHOLD = 40 -private val Metadata.isRenderedInNewLine: Boolean - get() = this is Metadata.Primitive && ( - valueType is MetadataType.Url || - (valueType is MetadataType.String && value.length > ASSET_METADATA_SHORT_STRING_THRESHOLD) - ) diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/account/composable/MetadataView.kt b/app/src/main/java/com/babylon/wallet/android/presentation/account/composable/MetadataView.kt new file mode 100644 index 0000000000..693803322b --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/presentation/account/composable/MetadataView.kt @@ -0,0 +1,308 @@ +package com.babylon.wallet.android.presentation.account.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import com.babylon.wallet.android.R +import com.babylon.wallet.android.designsystem.theme.RadixTheme +import com.babylon.wallet.android.presentation.ui.composables.ExpandableText +import com.babylon.wallet.android.presentation.ui.composables.LinkText +import com.babylon.wallet.android.presentation.ui.composables.actionableaddress.ActionableAddressView +import com.babylon.wallet.android.presentation.ui.modifier.throttleClickable +import com.babylon.wallet.android.utils.copyToClipboard +import com.radixdlt.sargon.Address +import com.radixdlt.sargon.NonFungibleGlobalId +import com.radixdlt.sargon.NonFungibleLocalId +import com.radixdlt.sargon.extensions.formatted +import com.radixdlt.sargon.extensions.init +import com.radixdlt.sargon.extensions.toDecimal192OrNull +import rdx.works.core.domain.resources.metadata.Metadata +import rdx.works.core.domain.resources.metadata.MetadataType +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +@Composable +fun MetadataView( + modifier: Modifier = Modifier, + key: String, + isLocked: Boolean = false, + isRenderedInNewLine: Boolean = false, + valueContent: @Composable () -> Unit +) { + if (isRenderedInNewLine) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(RadixTheme.dimensions.paddingSmall) + ) { + MetadataKeyView(key = key, isLocked = isLocked) + valueContent() + } + } else { + ConstraintLayout(modifier = modifier) { + val (keyView, valueView) = createRefs() + + MetadataKeyView( + modifier = Modifier.constrainAs(keyView) { + start.linkTo(parent.start) + top.linkTo(parent.top) + end.linkTo(valueView.start, margin = 12.dp) + width = if (key.length > SHORT_KEY_THRESHOLD) { + Dimension.fillToConstraints + } else { + Dimension.preferredWrapContent + } + height = Dimension.wrapContent + }, + key = key, + isLocked = isLocked + ) + Box( + modifier = Modifier.constrainAs(valueView) { + start.linkTo(keyView.end) + end.linkTo(parent.end) + top.linkTo(parent.top) + width = Dimension.fillToConstraints + height = Dimension.wrapContent + }, + contentAlignment = Alignment.TopEnd + ) { + valueContent() + } + } + } +} + +@Composable +fun MetadataView( + modifier: Modifier = Modifier, + metadata: Metadata +) { + MetadataView( + modifier = modifier, + key = metadata.key, + isLocked = metadata.isLocked, + isRenderedInNewLine = metadata.isRenderedInNewLine, + ) { + MetadataValueView( + modifier = Modifier.wrapContentSize( + align = if (metadata.isRenderedInNewLine) Alignment.TopStart else Alignment.TopEnd + ), + metadata = metadata, + isRenderedInNewLine = metadata.isRenderedInNewLine + ) + } +} + +@Composable +fun MetadataKeyView( + modifier: Modifier = Modifier, + metadata: Metadata, + style: TextStyle = RadixTheme.typography.body1Regular, + color: Color = RadixTheme.colors.gray2, +) { + MetadataKeyView( + modifier = modifier, + key = metadata.key, + isLocked = metadata.isLocked, + style = style, + color = color + ) +} + +@Composable +fun MetadataKeyView( + modifier: Modifier = Modifier, + key: String, + isLocked: Boolean, + style: TextStyle = RadixTheme.typography.body1Regular, + color: Color = RadixTheme.colors.gray2, +) { + Text( + modifier = modifier, + text = buildAnnotatedString { + append(key) + append(" ") + if (isLocked) { + appendInlineContent(id = "lock_icon") + } + }, + style = style, + color = color, + textAlign = TextAlign.Start, + inlineContent = mapOf( + "lock_icon" to InlineTextContent( + Placeholder(style.fontSize, style.fontSize, PlaceholderVerticalAlign.TextCenter) + ) { + Icon( + painter = painterResource(id = com.babylon.wallet.android.designsystem.R.drawable.ic_lock), + contentDescription = null, + tint = color + ) + } + ) + ) +} + +@Suppress("CyclomaticComplexMethod") +@Composable +fun MetadataValueView( + modifier: Modifier = Modifier, + metadata: Metadata, + isRenderedInNewLine: Boolean, + style: TextStyle = RadixTheme.typography.body1HighImportance, + color: Color = RadixTheme.colors.gray1 +) { + val context = LocalContext.current + when (metadata) { + is Metadata.Collection, is Metadata.Map -> Text( + modifier = modifier, + text = stringResource(id = R.string.assetDetails_NFTDetails_complexData), + style = style, + color = color, + textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, + maxLines = 2 + ) + + is Metadata.Primitive -> when (metadata.valueType) { + MetadataType.Bool, + is MetadataType.Integer, + MetadataType.Bytes, + MetadataType.Enum -> Text( + modifier = modifier, + text = metadata.value, + style = style, + color = color, + textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, + maxLines = 2 + ) + + MetadataType.PublicKeyEcdsaSecp256k1, + MetadataType.PublicKeyEddsaEd25519, + MetadataType.PublicKeyHashEcdsaSecp256k1, + MetadataType.PublicKeyHashEddsaEd25519 -> Text( + modifier = modifier.throttleClickable { + context.copyToClipboard( + label = metadata.key, + value = metadata.value, + successMessage = context.getString(R.string.addressAction_copiedToClipboard) + ) + }, + text = metadata.value, + style = style, + color = color, + textAlign = TextAlign.End, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + MetadataType.String -> ExpandableText( + modifier = modifier, + text = metadata.value, + style = style.copy( + color = color, + textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, + ), + toggleStyle = style.copy( + color = RadixTheme.colors.gray2 + ) + ) + + MetadataType.Instant -> { + val displayable = remember(metadata.value) { + val epochSeconds = metadata.value.toLongOrNull() ?: return@remember metadata.value + val dateTime = Instant.ofEpochSecond(epochSeconds) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + + dateTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) + } + Text( + modifier = modifier, + text = displayable, + style = style, + color = color, + textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, + maxLines = 2 + ) + } + + MetadataType.Address -> ActionableAddressView( + modifier = modifier, + address = remember(metadata.value) { + Address.init(metadata.value) + }, + textStyle = style, + textColor = color, + iconColor = color + ) + + MetadataType.NonFungibleGlobalId -> ActionableAddressView( + modifier = modifier, + globalId = remember(metadata.value) { + NonFungibleGlobalId.init(metadata.value) + }.copy(), + textStyle = style, + textColor = color, + iconColor = color + ) + + MetadataType.NonFungibleLocalId -> ActionableAddressView( + modifier = modifier, + localId = remember(metadata.value) { + NonFungibleLocalId.init(metadata.value) + }, + textStyle = style, + textColor = color, + iconColor = color + ) + + MetadataType.Decimal -> Text( + modifier = modifier, + // If value is unable to transform to big decimal we just display raw value + text = metadata.value.toDecimal192OrNull()?.formatted() ?: metadata.value, + style = style, + color = color, + textAlign = if (isRenderedInNewLine) TextAlign.Start else TextAlign.End, + maxLines = 2 + ) + + MetadataType.Url -> LinkText( + modifier = modifier.fillMaxWidth(), + url = metadata.value + ) + } + } +} + +private const val SHORT_KEY_THRESHOLD = 30 +private const val SHORT_VALUE_THRESHOLD = 40 +private val Metadata.isRenderedInNewLine: Boolean + get() = this is Metadata.Primitive && ( + valueType is MetadataType.Url || + (valueType is MetadataType.String && value.length > SHORT_VALUE_THRESHOLD) + ) diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/settings/approveddapps/dappdetail/DappDetailScreen.kt b/app/src/main/java/com/babylon/wallet/android/presentation/settings/approveddapps/dappdetail/DappDetailScreen.kt index 9449202a75..6262d4098c 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/settings/approveddapps/dappdetail/DappDetailScreen.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/settings/approveddapps/dappdetail/DappDetailScreen.kt @@ -4,7 +4,6 @@ package com.babylon.wallet.android.presentation.settings.approveddapps.dappdetai import androidx.activity.compose.BackHandler import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -26,7 +25,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -42,8 +40,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -64,6 +60,7 @@ import com.babylon.wallet.android.presentation.ui.RadixWalletPreviewTheme import com.babylon.wallet.android.presentation.ui.composables.BasicPromptAlertDialog import com.babylon.wallet.android.presentation.ui.composables.DefaultModalSheetLayout import com.babylon.wallet.android.presentation.ui.composables.GrayBackgroundWrapper +import com.babylon.wallet.android.presentation.ui.composables.LinkText import com.babylon.wallet.android.presentation.ui.composables.PersonaDataFieldRow import com.babylon.wallet.android.presentation.ui.composables.PersonaDataStringField import com.babylon.wallet.android.presentation.ui.composables.RadixCenteredTopAppBar @@ -75,7 +72,6 @@ import com.babylon.wallet.android.presentation.ui.composables.card.NonFungibleCa import com.babylon.wallet.android.presentation.ui.composables.card.PersonaCard import com.babylon.wallet.android.presentation.ui.modifier.radixPlaceholder import com.babylon.wallet.android.presentation.ui.modifier.throttleClickable -import com.babylon.wallet.android.utils.openUrl import com.radixdlt.sargon.AccountAddress import com.radixdlt.sargon.Address import com.radixdlt.sargon.AppearanceId @@ -487,7 +483,6 @@ fun DAppWebsiteAddressRow( modifier: Modifier = Modifier, website: String?, ) { - val context = LocalContext.current Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(dimensions.paddingSmall) @@ -499,31 +494,14 @@ fun DAppWebsiteAddressRow( style = RadixTheme.typography.body1Regular, color = RadixTheme.colors.gray2 ) - Row( + + LinkText( modifier = Modifier .fillMaxWidth() - .clickable(enabled = website != null) { - if (website != null) { - context.openUrl(website) - } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(dimensions.paddingSmall) - ) { - Text( - modifier = Modifier - .weight(1f) - .radixPlaceholder(visible = website == null), - text = website.orEmpty(), - style = RadixTheme.typography.body1HighImportance, - color = RadixTheme.colors.blue1 - ) - Icon( - painter = painterResource(id = com.babylon.wallet.android.designsystem.R.drawable.ic_link_out), - tint = RadixTheme.colors.gray3, - contentDescription = null - ) - } + .radixPlaceholder(visible = website == null), + clickable = website != null, + url = website.orEmpty() + ) } } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/settings/debug/profile/InspectProfileScreen.kt b/app/src/main/java/com/babylon/wallet/android/presentation/settings/debug/profile/InspectProfileScreen.kt index 48d348b791..330b6dee45 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/settings/debug/profile/InspectProfileScreen.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/settings/debug/profile/InspectProfileScreen.kt @@ -1,6 +1,5 @@ package com.babylon.wallet.android.presentation.settings.debug.profile -import android.content.ClipData import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.WindowInsets @@ -31,11 +30,11 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.Typeface import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp -import androidx.core.content.getSystemService import com.babylon.wallet.android.R import com.babylon.wallet.android.designsystem.theme.RadixTheme import com.babylon.wallet.android.presentation.common.FullscreenCircularProgressContent import com.babylon.wallet.android.presentation.ui.composables.RadixCenteredTopAppBar +import com.babylon.wallet.android.utils.copyToClipboard @Composable fun InspectProfileScreen( @@ -82,13 +81,10 @@ fun InspectProfileScreen( containerColor = RadixTheme.colors.gray4, contentColor = RadixTheme.colors.gray1, onClick = { - context.getSystemService()?.let { clipboardManager -> - val clipData = ClipData.newPlainText( - "Radix Address", - state.rawSnapshot - ) - clipboardManager.setPrimaryClip(clipData) - } + context.copyToClipboard( + label = "Radix Profile", + value = state.rawSnapshot.orEmpty() + ) } ) { Icon(painter = painterResource(id = R.drawable.ic_copy), contentDescription = null) diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialog.kt b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialog.kt index cd556a9e92..6c10d99f52 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialog.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialog.kt @@ -1,5 +1,6 @@ package com.babylon.wallet.android.presentation.status.assets +import android.net.Uri import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,21 +11,27 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.babylon.wallet.android.R import com.babylon.wallet.android.designsystem.theme.RadixTheme +import com.babylon.wallet.android.presentation.account.composable.MetadataKeyView +import com.babylon.wallet.android.presentation.account.composable.MetadataView import com.babylon.wallet.android.presentation.status.assets.fungible.FungibleDialogContent import com.babylon.wallet.android.presentation.status.assets.lsu.LSUDialogContent import com.babylon.wallet.android.presentation.status.assets.nonfungible.NonFungibleAssetDialogContent import com.babylon.wallet.android.presentation.status.assets.pool.PoolUnitDialogContent import com.babylon.wallet.android.presentation.ui.composables.BottomSheetDialogWrapper +import com.babylon.wallet.android.presentation.ui.composables.LinkText import com.babylon.wallet.android.presentation.ui.composables.SnackbarUiMessageHandler import com.babylon.wallet.android.presentation.ui.composables.assets.Behaviour import com.babylon.wallet.android.presentation.ui.composables.assets.Tag @@ -38,6 +45,7 @@ import rdx.works.core.domain.assets.AssetPrice import rdx.works.core.domain.assets.LiquidStakeUnit import rdx.works.core.domain.assets.PoolUnit import rdx.works.core.domain.assets.Token +import rdx.works.core.domain.resources.Resource import rdx.works.core.domain.resources.Tag @Composable @@ -139,6 +147,83 @@ fun Asset.displayTitle() = when (this) { } } +@Composable +fun DescriptionSection( + modifier: Modifier = Modifier, + description: String?, + infoUrl: Uri? +) { + Column( + modifier = modifier.fillMaxWidth() + ) { + if (!description.isNullOrBlank()) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = RadixTheme.dimensions.paddingSmall) + .padding(bottom = if (infoUrl != null) RadixTheme.dimensions.paddingSemiLarge else 0.dp), + text = description, + style = RadixTheme.typography.body1Regular, + color = RadixTheme.colors.gray1, + textAlign = TextAlign.Start + ) + } + + if (infoUrl != null) { + Text( + modifier = Modifier + .padding(horizontal = RadixTheme.dimensions.paddingSmall), + text = stringResource(id = R.string.assetDetails_moreInfo), + style = RadixTheme.typography.body1Regular, + color = RadixTheme.colors.gray2 + ) + + LinkText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = RadixTheme.dimensions.paddingSmall) + .padding(top = RadixTheme.dimensions.paddingXSmall), + url = infoUrl + ) + } + + if (!description.isNullOrBlank() || infoUrl != null) { + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) + HorizontalDivider(Modifier.fillMaxWidth(), color = RadixTheme.colors.gray4) + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) + } + } +} + +@Composable +fun NonStandardMetadataSection( + modifier: Modifier = Modifier, + resource: Resource +) { + val metadata = remember(resource.metadata) { + resource.nonStandardMetadata + } + + if (metadata.isNotEmpty()) { + Column( + modifier = modifier + ) { + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) + HorizontalDivider(Modifier.fillMaxWidth(), color = RadixTheme.colors.gray4) + + metadata.forEach { metadata -> + Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) + MetadataView( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = RadixTheme.dimensions.paddingSmall), + metadata = metadata + ) + } + } + } +} + @Composable fun BehavioursSection( modifier: Modifier = Modifier, @@ -194,11 +279,10 @@ fun TagsSection( if (!tags.isNullOrEmpty()) { Column(modifier = modifier) { Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - Text( + MetadataKeyView( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.assetDetails_tags), - style = RadixTheme.typography.body1Regular, - color = RadixTheme.colors.gray2 + key = stringResource(id = R.string.assetDetails_tags), + isLocked = false ) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) FlowRow( @@ -220,7 +304,6 @@ fun TagsSection( } } ) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingXXLarge)) } } } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialogViewModel.kt b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialogViewModel.kt index a16bf9ffde..0cc0ab10c8 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialogViewModel.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/AssetDialogViewModel.kt @@ -52,7 +52,8 @@ class AssetDialogViewModel @Inject constructor( is AssetDialogArgs.NFT -> mapOf( args.resourceAddress to args.localId?.let { setOf(it) }.orEmpty() ) - } + }, + withAllMetadata = true ).mapCatching { assets -> when (val asset = assets.first()) { // In case we receive a fungible asset, let's copy the custom amount diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/fungible/FungibleDialogContent.kt b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/fungible/FungibleDialogContent.kt index 732ac8c51b..c1926988dc 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/fungible/FungibleDialogContent.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/fungible/FungibleDialogContent.kt @@ -23,9 +23,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.babylon.wallet.android.R import com.babylon.wallet.android.designsystem.theme.RadixTheme -import com.babylon.wallet.android.presentation.account.composable.AssetMetadataRow +import com.babylon.wallet.android.presentation.account.composable.MetadataView import com.babylon.wallet.android.presentation.status.assets.AssetDialogArgs import com.babylon.wallet.android.presentation.status.assets.BehavioursSection +import com.babylon.wallet.android.presentation.status.assets.DescriptionSection +import com.babylon.wallet.android.presentation.status.assets.NonStandardMetadataSection import com.babylon.wallet.android.presentation.status.assets.TagsSection import com.babylon.wallet.android.presentation.ui.composables.ShimmeringView import com.babylon.wallet.android.presentation.ui.composables.Thumbnail @@ -61,22 +63,9 @@ fun FungibleDialogContent( ), horizontalAlignment = Alignment.CenterHorizontally ) { - if (token?.resource != null) { - Thumbnail.Fungible( - modifier = Modifier.size(104.dp), - token = token.resource - ) - } else { - Box( - modifier = Modifier - .size(104.dp) - .radixPlaceholder( - visible = true, - shape = CircleShape - ) - ) - } + FungibleIconSection(token = token) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) + if (amount != null) { TokenBalance( modifier = Modifier @@ -108,17 +97,12 @@ fun FungibleDialogContent( HorizontalDivider(Modifier.fillMaxWidth(), color = RadixTheme.colors.gray4) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - if (!token?.resource?.description.isNullOrBlank()) { - Text( - modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingSmall), - text = token?.resource?.description.orEmpty(), - style = RadixTheme.typography.body2Regular, - color = RadixTheme.colors.gray1 - ) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - HorizontalDivider(Modifier.fillMaxWidth(), color = RadixTheme.colors.gray4) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - } + DescriptionSection( + modifier = Modifier.fillMaxWidth(), + description = token?.resource?.description, + infoUrl = token?.resource?.infoUrl + ) + AddressRow( modifier = Modifier .fillMaxWidth() @@ -129,7 +113,7 @@ fun FungibleDialogContent( Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) if (!isNewlyCreated) { - AssetMetadataRow( + MetadataView( modifier = Modifier .fillMaxWidth() .padding(horizontal = RadixTheme.dimensions.paddingSmall), @@ -159,8 +143,34 @@ fun FungibleDialogContent( TagsSection( modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingSmall), - tags = token?.resource?.tags, + tags = token?.resource?.tags ) + + token?.resource?.let { resource -> + NonStandardMetadataSection(resource = resource) + } } } } + +@Composable +private fun FungibleIconSection( + modifier: Modifier = Modifier, + token: Token? +) { + if (token?.resource != null) { + Thumbnail.Fungible( + modifier = modifier.size(104.dp), + token = token.resource + ) + } else { + Box( + modifier = modifier + .size(104.dp) + .radixPlaceholder( + visible = true, + shape = CircleShape + ) + ) + } +} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/lsu/LSUDialogContent.kt b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/lsu/LSUDialogContent.kt index bfc6f2acea..6c3af190d8 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/lsu/LSUDialogContent.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/lsu/LSUDialogContent.kt @@ -26,9 +26,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.babylon.wallet.android.R import com.babylon.wallet.android.designsystem.theme.RadixTheme -import com.babylon.wallet.android.presentation.account.composable.AssetMetadataRow +import com.babylon.wallet.android.presentation.account.composable.MetadataView import com.babylon.wallet.android.presentation.status.assets.AssetDialogArgs import com.babylon.wallet.android.presentation.status.assets.BehavioursSection +import com.babylon.wallet.android.presentation.status.assets.DescriptionSection +import com.babylon.wallet.android.presentation.status.assets.NonStandardMetadataSection import com.babylon.wallet.android.presentation.status.assets.TagsSection import com.babylon.wallet.android.presentation.ui.composables.ShimmeringView import com.babylon.wallet.android.presentation.ui.composables.Thumbnail @@ -71,22 +73,9 @@ fun LSUDialogContent( ), horizontalAlignment = Alignment.CenterHorizontally ) { - if (lsu != null) { - Thumbnail.LSU( - modifier = Modifier.size(104.dp), - liquidStakeUnit = lsu - ) - } else { - Box( - modifier = Modifier - .size(104.dp) - .radixPlaceholder( - visible = true, - shape = CircleShape - ) - ) - } + LSUIconSection(lsu = lsu) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) + TokenBalance( modifier = Modifier .fillMaxWidth(fraction = if (lsu == null) 0.5f else 1f) @@ -146,17 +135,11 @@ fun LSUDialogContent( ) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - if (!lsu?.fungibleResource?.description.isNullOrBlank()) { - Text( - modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingSmall), - text = lsu?.fungibleResource?.description.orEmpty(), - style = RadixTheme.typography.body2Regular, - color = RadixTheme.colors.gray1 - ) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - HorizontalDivider(Modifier.fillMaxWidth(), color = RadixTheme.colors.gray4) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - } + DescriptionSection( + modifier = Modifier.fillMaxWidth(), + description = lsu?.fungibleResource?.description, + infoUrl = lsu?.fungibleResource?.infoUrl + ) AddressRow( modifier = Modifier @@ -178,7 +161,7 @@ fun LSUDialogContent( } Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - AssetMetadataRow( + MetadataView( modifier = Modifier .fillMaxWidth() .padding(horizontal = RadixTheme.dimensions.paddingSmall), @@ -186,7 +169,6 @@ fun LSUDialogContent( ) { Text( modifier = Modifier - .padding(start = RadixTheme.dimensions.paddingDefault) .widthIn(min = RadixTheme.dimensions.paddingXXXXLarge * 2) .radixPlaceholder(visible = lsu == null), text = lsu?.name().orEmpty(), @@ -196,7 +178,7 @@ fun LSUDialogContent( ) } Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - AssetMetadataRow( + MetadataView( modifier = Modifier .fillMaxWidth() .padding(horizontal = RadixTheme.dimensions.paddingSmall), @@ -204,7 +186,6 @@ fun LSUDialogContent( ) { Text( modifier = Modifier - .padding(start = RadixTheme.dimensions.paddingDefault) .widthIn(min = RadixTheme.dimensions.paddingXXXXLarge * 2) .radixPlaceholder( visible = lsu?.fungibleResource?.currentSupply == null @@ -219,7 +200,6 @@ fun LSUDialogContent( textAlign = TextAlign.End ) } - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) BehavioursSection( modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingSmall), @@ -230,6 +210,32 @@ fun LSUDialogContent( modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingSmall), tags = lsu?.fungibleResource?.tags ) + + lsu?.resource?.let { resource -> + NonStandardMetadataSection(resource = resource) + } + } +} + +@Composable +private fun LSUIconSection( + modifier: Modifier = Modifier, + lsu: LiquidStakeUnit? +) { + if (lsu != null) { + Thumbnail.LSU( + modifier = modifier.size(104.dp), + liquidStakeUnit = lsu + ) + } else { + Box( + modifier = modifier + .size(104.dp) + .radixPlaceholder( + visible = true, + shape = CircleShape + ) + ) } } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/nonfungible/NonFungibleAssetDialogContent.kt b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/nonfungible/NonFungibleAssetDialogContent.kt index fb2ce098be..039fb21c23 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/nonfungible/NonFungibleAssetDialogContent.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/nonfungible/NonFungibleAssetDialogContent.kt @@ -27,12 +27,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.babylon.wallet.android.R -import com.babylon.wallet.android.designsystem.composable.RadixTextButton +import com.babylon.wallet.android.designsystem.composable.RadixPrimaryButton import com.babylon.wallet.android.designsystem.theme.RadixTheme -import com.babylon.wallet.android.presentation.account.composable.AssetMetadataRow -import com.babylon.wallet.android.presentation.account.composable.View +import com.babylon.wallet.android.presentation.account.composable.MetadataView import com.babylon.wallet.android.presentation.status.assets.AssetDialogViewModel import com.babylon.wallet.android.presentation.status.assets.BehavioursSection +import com.babylon.wallet.android.presentation.status.assets.DescriptionSection +import com.babylon.wallet.android.presentation.status.assets.NonStandardMetadataSection import com.babylon.wallet.android.presentation.status.assets.TagsSection import com.babylon.wallet.android.presentation.ui.composables.GrayBackgroundWrapper import com.babylon.wallet.android.presentation.ui.composables.Thumbnail @@ -114,14 +115,13 @@ fun NonFungibleAssetDialogContent( } if (item != null) { - AssetMetadataRow( + MetadataView( modifier = Modifier .fillMaxWidth() .padding(horizontal = RadixTheme.dimensions.paddingXXLarge), key = stringResource(id = R.string.assetDetails_NFTDetails_id) ) { ActionableAddressView( - modifier = Modifier.padding(start = RadixTheme.dimensions.paddingDefault), globalId = item.globalId, visitableInDashboard = !isNewlyCreated, textStyle = RadixTheme.typography.body1HighImportance, @@ -154,10 +154,11 @@ fun NonFungibleAssetDialogContent( item?.nonStandardMetadata?.forEach { metadata -> Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - metadata.View( + MetadataView( modifier = Modifier .fillMaxWidth() - .padding(horizontal = RadixTheme.dimensions.paddingXXLarge) + .padding(horizontal = RadixTheme.dimensions.paddingXXLarge), + metadata = metadata ) } @@ -200,22 +201,14 @@ fun NonFungibleAssetDialogContent( ) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - if (!asset?.resource?.description.isNullOrBlank()) { - Text( - modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingXXLarge), - text = asset?.resource?.description.orEmpty(), - style = RadixTheme.typography.body2Regular, - color = RadixTheme.colors.gray1 - ) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - HorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = RadixTheme.dimensions.paddingLarge), - color = RadixTheme.colors.gray4 - ) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - } + DescriptionSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = RadixTheme.dimensions.paddingLarge), + description = asset?.resource?.description, + infoUrl = asset?.resource?.infoUrl + ) + AddressRow( modifier = Modifier .fillMaxWidth() @@ -225,7 +218,7 @@ fun NonFungibleAssetDialogContent( ) if (!asset?.resource?.name.isNullOrBlank() && localId != null) { Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - AssetMetadataRow( + MetadataView( modifier = Modifier .fillMaxWidth() .padding(horizontal = RadixTheme.dimensions.paddingXXLarge), @@ -241,7 +234,7 @@ fun NonFungibleAssetDialogContent( Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) if (!isNewlyCreated) { - AssetMetadataRow( + MetadataView( modifier = Modifier .fillMaxWidth() .padding(horizontal = RadixTheme.dimensions.paddingXXLarge), @@ -249,7 +242,6 @@ fun NonFungibleAssetDialogContent( ) { Text( modifier = Modifier - .padding(start = RadixTheme.dimensions.paddingDefault) .widthIn(min = RadixTheme.dimensions.paddingXXXXLarge * 2) .radixPlaceholder(visible = asset?.resource?.currentSupply == null), text = when { @@ -275,6 +267,13 @@ fun NonFungibleAssetDialogContent( modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingXXLarge), tags = asset?.resource?.tags ) + + asset?.resource?.let { resource -> + NonStandardMetadataSection( + modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingLarge), + resource = resource + ) + } } } } @@ -293,57 +292,77 @@ private fun ClaimNFTInfo( val showClaimButton = claimState is AssetDialogViewModel.State.ClaimState.ReadyToClaim && !accountContextMissing Column( modifier = modifier - .padding(horizontal = RadixTheme.dimensions.paddingXXLarge) + .padding(horizontal = RadixTheme.dimensions.paddingLarge) .fillMaxWidth() ) { - Row( - modifier = Modifier.padding(bottom = if (showClaimButton) 0.dp else RadixTheme.dimensions.paddingSmall), - horizontalArrangement = Arrangement.spacedBy(RadixTheme.dimensions.paddingSmall), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.weight(1f), - text = when (claimState) { - is AssetDialogViewModel.State.ClaimState.ReadyToClaim -> stringResource( - id = if (accountContextMissing) { - R.string.transactionReview_toBeClaimed - } else { - R.string.account_staking_readyToBeClaimed - } - ) - is AssetDialogViewModel.State.ClaimState.Unstaking -> - stringResource(id = R.string.account_staking_unstaking) - null -> "" - }.uppercase(), - style = RadixTheme.typography.body2HighImportance, - color = RadixTheme.colors.gray2 - ) + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = RadixTheme.dimensions.paddingDefault), + color = RadixTheme.colors.gray4 + ) - if (showClaimButton) { - RadixTextButton( - text = stringResource(id = R.string.account_staking_claim), - onClick = onClaimClick, - textStyle = RadixTheme.typography.body2Link - ) - } - } + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = RadixTheme.dimensions.paddingDefault, + horizontal = RadixTheme.dimensions.paddingMedium + ), + text = stringResource(id = R.string.assetDetails_staking_currentRedeemableValue), + style = RadixTheme.typography.secondaryHeader, + color = RadixTheme.colors.gray1, + textAlign = TextAlign.Center + ) val fiatPrice = remember(price, item) { price?.xrdPrice(item) } WorthXRD( + modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingMedium), amount = claimState?.amount, fiatPrice = fiatPrice, - isLoadingBalance = isLoadingBalance + isLoadingBalance = isLoadingBalance, + iconSize = 44.dp, + symbolStyle = RadixTheme.typography.body2HighImportance ) - if (claimState is AssetDialogViewModel.State.ClaimState.Unstaking) { - Text( - modifier = Modifier.padding(top = RadixTheme.dimensions.paddingSmall), - text = stringResource(id = R.string.assetDetails_staking_unstaking, claimState.approximateClaimMinutes), - style = RadixTheme.typography.body2HighImportance, - color = RadixTheme.colors.gray2 - ) + when (claimState) { + is AssetDialogViewModel.State.ClaimState.Unstaking -> { + Row( + modifier = Modifier + .padding(top = RadixTheme.dimensions.paddingDefault) + .padding(horizontal = RadixTheme.dimensions.paddingMedium) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Text( + text = stringResource(id = R.string.assetDetails_staking_readyToClaimIn), + style = RadixTheme.typography.body1Regular, + color = RadixTheme.colors.gray2 + ) + + Text( + text = stringResource(id = R.string.assetDetails_staking_readyToClaimInMinutes, claimState.approximateClaimMinutes), + style = RadixTheme.typography.body1HighImportance, + color = RadixTheme.colors.gray1 + ) + } + } + is AssetDialogViewModel.State.ClaimState.ReadyToClaim -> { + if (showClaimButton) { + RadixPrimaryButton( + modifier = Modifier + .padding(top = RadixTheme.dimensions.paddingDefault) + .padding(horizontal = RadixTheme.dimensions.paddingMedium) + .fillMaxWidth(), + text = stringResource(id = R.string.assetDetails_staking_readyToClaim), + onClick = onClaimClick + ) + } + } + null -> {} } } } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/pool/PoolUnitDialogContent.kt b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/pool/PoolUnitDialogContent.kt index 90ff3ffbf0..649689cfaf 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/pool/PoolUnitDialogContent.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/status/assets/pool/PoolUnitDialogContent.kt @@ -25,9 +25,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.babylon.wallet.android.R import com.babylon.wallet.android.designsystem.theme.RadixTheme -import com.babylon.wallet.android.presentation.account.composable.AssetMetadataRow +import com.babylon.wallet.android.presentation.account.composable.MetadataView import com.babylon.wallet.android.presentation.status.assets.AssetDialogArgs import com.babylon.wallet.android.presentation.status.assets.BehavioursSection +import com.babylon.wallet.android.presentation.status.assets.DescriptionSection +import com.babylon.wallet.android.presentation.status.assets.NonStandardMetadataSection import com.babylon.wallet.android.presentation.status.assets.TagsSection import com.babylon.wallet.android.presentation.ui.composables.Thumbnail import com.babylon.wallet.android.presentation.ui.composables.assets.PoolResourcesValues @@ -150,20 +152,12 @@ fun PoolUnitDialogContent( ) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - if (!poolUnit?.stake?.description.isNullOrBlank()) { - Text( - modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingDefault), - text = poolUnit?.stake?.description.orEmpty(), - style = RadixTheme.typography.body2Regular, - color = RadixTheme.colors.gray1 - ) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingLarge)) - HorizontalDivider( - modifier = Modifier.fillMaxWidth(), - color = RadixTheme.colors.gray4 - ) - Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - } + DescriptionSection( + modifier = Modifier.fillMaxWidth(), + description = poolUnit?.stake?.description, + infoUrl = poolUnit?.stake?.infoUrl + ) + AddressRow( modifier = Modifier .fillMaxWidth() @@ -172,7 +166,7 @@ fun PoolUnitDialogContent( ) Spacer(modifier = Modifier.height(RadixTheme.dimensions.paddingDefault)) - AssetMetadataRow( + MetadataView( modifier = Modifier .fillMaxWidth() .padding(horizontal = RadixTheme.dimensions.paddingSmall), @@ -180,7 +174,6 @@ fun PoolUnitDialogContent( ) { Text( modifier = Modifier - .padding(start = RadixTheme.dimensions.paddingDefault) .widthIn(min = RadixTheme.dimensions.paddingXXXXLarge * 2) .radixPlaceholder(visible = poolUnit?.stake?.currentSupply == null), text = when (val supply = poolUnit?.stake?.currentSupply) { @@ -201,7 +194,11 @@ fun PoolUnitDialogContent( TagsSection( modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingSmall), - tags = poolUnit?.resource?.tags, + tags = poolUnit?.resource?.tags ) + + poolUnit?.resource?.let { resource -> + NonStandardMetadataSection(resource = resource) + } } } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/LinkText.kt b/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/LinkText.kt new file mode 100644 index 0000000000..557aebcb76 --- /dev/null +++ b/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/LinkText.kt @@ -0,0 +1,80 @@ +package com.babylon.wallet.android.presentation.ui.composables + +import android.net.Uri +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import com.babylon.wallet.android.R +import com.babylon.wallet.android.designsystem.theme.RadixTheme +import com.babylon.wallet.android.presentation.ui.modifier.throttleClickable +import com.babylon.wallet.android.utils.openUrl + +@Composable +fun LinkText( + modifier: Modifier = Modifier, + url: Uri, + clickable: Boolean = true, + linkStyle: TextStyle = RadixTheme.typography.body1StandaloneLink, + linkColor: Color = RadixTheme.colors.blue1, + linkIconColor: Color = RadixTheme.colors.gray2 +) { + LinkText( + modifier = modifier, + url = url.toString(), + clickable = clickable, + linkStyle = linkStyle, + linkColor = linkColor, + linkIconColor = linkIconColor + ) +} + +@Composable +fun LinkText( + modifier: Modifier = Modifier, + url: String, + clickable: Boolean = true, + linkStyle: TextStyle = RadixTheme.typography.body1StandaloneLink, + linkColor: Color = RadixTheme.colors.blue1, + linkIconColor: Color = RadixTheme.colors.gray2 +) { + val context = LocalContext.current + Text( + modifier = modifier.throttleClickable(enabled = clickable) { + context.openUrl(url) + }, + text = buildAnnotatedString { + append(url) + append(" ") + appendInlineContent(id = "link_icon") + }, + style = linkStyle, + color = linkColor, + inlineContent = mapOf( + "link_icon" to InlineTextContent( + Placeholder( + linkStyle.fontSize, + linkStyle.fontSize, + PlaceholderVerticalAlign.TextCenter + ) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_external_link), + contentDescription = null, + tint = linkIconColor + ) + } + ), + textAlign = TextAlign.Start, + ) +} diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/actionableaddress/ActionableAddressView.kt b/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/actionableaddress/ActionableAddressView.kt index 7ce9189383..35b396ea2a 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/actionableaddress/ActionableAddressView.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/actionableaddress/ActionableAddressView.kt @@ -2,10 +2,8 @@ package com.babylon.wallet.android.presentation.ui.composables.actionableaddress -import android.content.ClipData import android.content.Context import android.net.Uri -import android.os.Build import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.VisibleForTesting @@ -45,7 +43,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import androidx.core.content.getSystemService import androidx.core.net.toUri import com.babylon.wallet.android.R import com.babylon.wallet.android.designsystem.theme.RadixTheme @@ -53,6 +50,7 @@ import com.babylon.wallet.android.domain.usecases.VerifyAddressOnLedgerUseCase import com.babylon.wallet.android.presentation.ui.RadixWalletPreviewTheme import com.babylon.wallet.android.presentation.ui.composables.AccountQRCodeView import com.babylon.wallet.android.presentation.ui.composables.BottomSheetWrapper +import com.babylon.wallet.android.utils.copyToClipboard import com.babylon.wallet.android.utils.encodeUtf8 import com.babylon.wallet.android.utils.openUrl import com.radixdlt.sargon.AccountAddress @@ -469,20 +467,11 @@ private sealed interface OnAction { ) : CallbackBasedAction { override fun onAction(context: Context) { - context.getSystemService()?.let { clipboardManager -> - - val clipData = ClipData.newPlainText( - "Radix Address", - actionableAddress.copyable - ) - - clipboardManager.setPrimaryClip(clipData) - - // From Android 13, the system handles the copy confirmation - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(context, R.string.addressAction_copiedToClipboard, Toast.LENGTH_SHORT).show() - } - } + context.copyToClipboard( + label = "Radix Address", + value = actionableAddress.copyable.orEmpty(), + successMessage = context.getString(R.string.addressAction_copiedToClipboard) + ) } } diff --git a/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/assets/StakingTab.kt b/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/assets/StakingTab.kt index 1abad4e9bc..7154fded5e 100644 --- a/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/assets/StakingTab.kt +++ b/app/src/main/java/com/babylon/wallet/android/presentation/ui/composables/assets/StakingTab.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.babylon.wallet.android.R import com.babylon.wallet.android.designsystem.composable.RadixTextButton @@ -714,6 +715,8 @@ fun WorthXRD( amount: Decimal192?, fiatPrice: FiatPrice?, isLoadingBalance: Boolean, + iconSize: Dp = 24.dp, + symbolStyle: TextStyle = RadixTheme.typography.body2HighImportance, trailingContent: @Composable (() -> Unit)? = null ) { Row( @@ -733,15 +736,17 @@ fun WorthXRD( painter = painterResource(id = com.babylon.wallet.android.designsystem.R.drawable.ic_xrd_token), contentDescription = null, modifier = Modifier - .size(24.dp) + .size(iconSize) .clip(RadixTheme.shapes.circle), tint = Color.Unspecified ) Text( - modifier = Modifier.padding(horizontal = RadixTheme.dimensions.paddingSmall), + modifier = Modifier.padding( + horizontal = RadixTheme.dimensions.paddingSmall + ), text = XrdResource.SYMBOL, - style = RadixTheme.typography.body2HighImportance, + style = symbolStyle, color = RadixTheme.colors.gray1, maxLines = 1 ) diff --git a/app/src/main/java/com/babylon/wallet/android/utils/ContextExtensions.kt b/app/src/main/java/com/babylon/wallet/android/utils/ContextExtensions.kt index 92676dbc0d..b5888f71a3 100644 --- a/app/src/main/java/com/babylon/wallet/android/utils/ContextExtensions.kt +++ b/app/src/main/java/com/babylon/wallet/android/utils/ContextExtensions.kt @@ -1,13 +1,16 @@ package com.babylon.wallet.android.utils import android.content.ActivityNotFoundException +import android.content.ClipData import android.content.ComponentName import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.widget.Toast +import androidx.core.content.getSystemService import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import androidx.navigation.NavController @@ -44,6 +47,28 @@ suspend fun Context.biometricAuthenticateSuspend(allowIfDeviceIsNotSecure: Boole fun Context.openUrl(url: HttpUrl, browser: Browser? = null) = openUrl(url.toString(), browser) fun Context.openUrl(url: String, browser: Browser? = null) = openUrl(url.toUri(), browser) +fun Context.copyToClipboard( + label: String, + value: String, + // Used only for Android versions < Android 13 + successMessage: String? = null +) { + getSystemService()?.let { clipboardManager -> + + val clipData = ClipData.newPlainText( + label, + value + ) + + clipboardManager.setPrimaryClip(clipData) + + // From Android 13, the system handles the copy confirmation + if (successMessage != null && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(this, successMessage, Toast.LENGTH_SHORT).show() + } + } +} + @Suppress("SwallowedException") fun Context.openUrl(uri: Uri, browser: Browser? = null) { val intent = Intent(Intent.ACTION_VIEW).apply { diff --git a/app/src/test/java/com/babylon/wallet/android/data/cache/ProvidedConvertersTest.kt b/app/src/test/java/com/babylon/wallet/android/data/cache/ProvidedConvertersTest.kt index a5f10bb2ae..46a5a3e520 100644 --- a/app/src/test/java/com/babylon/wallet/android/data/cache/ProvidedConvertersTest.kt +++ b/app/src/test/java/com/babylon/wallet/android/data/cache/ProvidedConvertersTest.kt @@ -3,12 +3,12 @@ package com.babylon.wallet.android.data.cache import com.babylon.wallet.android.data.repository.cache.database.BehavioursColumn import com.babylon.wallet.android.data.repository.cache.database.MetadataColumn import com.babylon.wallet.android.data.repository.cache.database.StateDatabaseConverters -import rdx.works.core.domain.assets.AssetBehaviour -import rdx.works.core.domain.resources.metadata.Metadata -import rdx.works.core.domain.resources.metadata.MetadataType import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test +import rdx.works.core.domain.assets.AssetBehaviour +import rdx.works.core.domain.resources.metadata.Metadata +import rdx.works.core.domain.resources.metadata.MetadataType class ProvidedConvertersTest { @@ -100,7 +100,8 @@ class ProvidedConvertersTest { ) to Metadata.Primitive("value", "Value", MetadataType.String) ) ) - ) + ), + implicitState = MetadataColumn.ImplicitMetadataState.Unknown ) val converter = StateDatabaseConverters() diff --git a/app/src/test/java/com/babylon/wallet/android/domain/usecases/SearchFeePayersUseCaseTest.kt b/app/src/test/java/com/babylon/wallet/android/domain/usecases/SearchFeePayersUseCaseTest.kt index 36c95090ec..12c32db309 100644 --- a/app/src/test/java/com/babylon/wallet/android/domain/usecases/SearchFeePayersUseCaseTest.kt +++ b/app/src/test/java/com/babylon/wallet/android/domain/usecases/SearchFeePayersUseCaseTest.kt @@ -148,7 +148,8 @@ class SearchFeePayersUseCaseTest { override suspend fun getResources( addresses: Set, underAccountAddress: AccountAddress?, - withDetails: Boolean + withDetails: Boolean, + withAllMetadata: Boolean ): Result> { TODO("Not yet implemented") } diff --git a/app/src/test/java/com/babylon/wallet/android/fakes/StateRepositoryFake.kt b/app/src/test/java/com/babylon/wallet/android/fakes/StateRepositoryFake.kt index 589e7a67f9..353085772f 100644 --- a/app/src/test/java/com/babylon/wallet/android/fakes/StateRepositoryFake.kt +++ b/app/src/test/java/com/babylon/wallet/android/fakes/StateRepositoryFake.kt @@ -37,7 +37,12 @@ open class StateRepositoryFake : StateRepository { override suspend fun updateStakeClaims(account: Account, claims: List): Result> = Result.success(claims) - override suspend fun getResources(addresses: Set, underAccountAddress: AccountAddress?, withDetails: Boolean): Result> = + override suspend fun getResources( + addresses: Set, + underAccountAddress: AccountAddress?, + withDetails: Boolean, + withAllMetadata: Boolean + ): Result> = Result.failure(RuntimeException("Not implemented")) override suspend fun getPools(poolAddresses: Set): Result> = Result.failure(RuntimeException("Not implemented")) @@ -45,7 +50,10 @@ open class StateRepositoryFake : StateRepository { override suspend fun getValidators(validatorAddresses: Set): Result> = Result.failure(RuntimeException("Not implemented")) - override suspend fun getNFTDetails(resourceAddress: ResourceAddress, localIds: Set): Result> = + override suspend fun getNFTDetails( + resourceAddress: ResourceAddress, + localIds: Set + ): Result> = Result.failure(RuntimeException("Not implemented")) override suspend fun getOwnedXRD(accounts: List): Result> = diff --git a/app/src/test/java/com/babylon/wallet/android/presentation/dapp/login/DAppAuthorizedLoginViewModelTest.kt b/app/src/test/java/com/babylon/wallet/android/presentation/dapp/login/DAppAuthorizedLoginViewModelTest.kt index c53a311008..7915fdee49 100644 --- a/app/src/test/java/com/babylon/wallet/android/presentation/dapp/login/DAppAuthorizedLoginViewModelTest.kt +++ b/app/src/test/java/com/babylon/wallet/android/presentation/dapp/login/DAppAuthorizedLoginViewModelTest.kt @@ -305,7 +305,8 @@ class DAppAuthorizedLoginViewModelTest : StateViewModelTest, underAccountAddress: AccountAddress?, - withDetails: Boolean + withDetails: Boolean, + withAllMetadata: Boolean ): Result> { TODO("Not yet implemented") } diff --git a/core/src/main/java/rdx/works/core/domain/resources/Resource.kt b/core/src/main/java/rdx/works/core/domain/resources/Resource.kt index 504ffbd1b5..23a92e47fb 100644 --- a/core/src/main/java/rdx/works/core/domain/resources/Resource.kt +++ b/core/src/main/java/rdx/works/core/domain/resources/Resource.kt @@ -26,6 +26,7 @@ import rdx.works.core.domain.resources.metadata.claimAmount import rdx.works.core.domain.resources.metadata.claimEpoch import rdx.works.core.domain.resources.metadata.description import rdx.works.core.domain.resources.metadata.iconUrl +import rdx.works.core.domain.resources.metadata.infoUrl import rdx.works.core.domain.resources.metadata.keyImageUrl import rdx.works.core.domain.resources.metadata.name import rdx.works.core.domain.resources.metadata.poolAddress @@ -47,6 +48,12 @@ sealed class Resource { is NonFungibleResource -> currentSupply != null && behaviours != null } + val nonStandardMetadata: List by lazy { + metadata.filterNot { item -> + item.key in ExplicitMetadataKey.entries.map { it.key }.toSet() + } + } + data class FungibleResource( override val address: ResourceAddress, val ownedAmount: Decimal192?, @@ -55,6 +62,7 @@ sealed class Resource { val divisibility: Divisibility? = null, override val metadata: List = emptyList() ) : Resource(), Comparable { + override val name: String by lazy { metadata.name().orEmpty().truncate(maxNumberOfCharacters = NAME_MAX_CHARS) } @@ -71,6 +79,10 @@ sealed class Resource { metadata.iconUrl() } + val infoUrl: Uri? by lazy { + metadata.infoUrl() + } + override val validatorAddress: ValidatorAddress? by lazy { metadata.validatorAddress() } @@ -169,6 +181,10 @@ sealed class Resource { metadata.iconUrl() } + val infoUrl: Uri? by lazy { + metadata.infoUrl() + } + val tags: ImmutableList by lazy { metadata.tags().orEmpty().map { Tag.Dynamic(name = it.truncate(maxNumberOfCharacters = TAG_MAX_CHARS)) diff --git a/core/src/main/java/rdx/works/core/domain/resources/metadata/Metadata.kt b/core/src/main/java/rdx/works/core/domain/resources/metadata/Metadata.kt index b7f44b0131..c700c9b7a9 100644 --- a/core/src/main/java/rdx/works/core/domain/resources/metadata/Metadata.kt +++ b/core/src/main/java/rdx/works/core/domain/resources/metadata/Metadata.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable sealed interface Metadata { val key: String val lastUpdatedAtStateVersion: Long + val isLocked: Boolean @Serializable @SerialName("primitive") @@ -16,6 +17,7 @@ sealed interface Metadata { @SerialName("value_type") val valueType: MetadataType, override val lastUpdatedAtStateVersion: Long = 0, + override val isLocked: Boolean = false ) : Metadata @Serializable @@ -24,6 +26,7 @@ sealed interface Metadata { override val key: String, val values: List, override val lastUpdatedAtStateVersion: Long = 0, + override val isLocked: Boolean = false ) : Metadata @Serializable @@ -31,7 +34,8 @@ sealed interface Metadata { data class Map( override val key: String, val values: kotlin.collections.Map, - override val lastUpdatedAtStateVersion: Long = 0L + override val lastUpdatedAtStateVersion: Long = 0L, + override val isLocked: Boolean = false ) : Metadata } diff --git a/designsystem/src/main/res/drawable/ic_lock.xml b/designsystem/src/main/res/drawable/ic_lock.xml index 9d29f7c790..73f80fc366 100644 --- a/designsystem/src/main/res/drawable/ic_lock.xml +++ b/designsystem/src/main/res/drawable/ic_lock.xml @@ -1,14 +1,13 @@ + android:width="24dp" + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> + android:pathData="M0,0.72h24v24h-24z"/> + android:pathData="M18,8.72H17V6.72C17,3.96 14.76,1.72 12,1.72C9.24,1.72 7,3.96 7,6.72V8.72H6C4.9,8.72 4,9.62 4,10.72V20.72C4,21.82 4.9,22.72 6,22.72H18C19.1,22.72 20,21.82 20,20.72V10.72C20,9.62 19.1,8.72 18,8.72ZM9,6.72C9,5.06 10.34,3.72 12,3.72C13.66,3.72 15,5.06 15,6.72V8.72H9V6.72ZM18,20.72H6V10.72H18V20.72ZM12,17.72C13.1,17.72 14,16.82 14,15.72C14,14.62 13.1,13.72 12,13.72C10.9,13.72 10,14.62 10,15.72C10,16.82 10.9,17.72 12,17.72Z" + android:fillColor="#003057"/> -