mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 02:07:58 +00:00
Initial commit: Pezkuwi Wallet Android
Security hardened release: - Code obfuscation enabled (minifyEnabled=true, shrinkResources=true) - Sensitive files excluded (google-services.json, keystores) - Branch.io key moved to BuildConfig placeholder - Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77 - Comprehensive ProGuard rules for crypto wallet - Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,67 @@
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: '../tests.gradle'
|
||||
apply from: '../scripts/secrets.gradle'
|
||||
|
||||
android {
|
||||
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
namespace 'io.novafoundation.nova.feature_nft_impl'
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core-db')
|
||||
implementation project(':common')
|
||||
implementation project(':feature-wallet-api')
|
||||
implementation project(':feature-account-api')
|
||||
implementation project(':feature-nft-api')
|
||||
implementation project(':runtime')
|
||||
|
||||
implementation kotlinDep
|
||||
|
||||
implementation androidDep
|
||||
implementation materialDep
|
||||
implementation constraintDep
|
||||
|
||||
implementation coroutinesDep
|
||||
implementation coroutinesAndroidDep
|
||||
implementation viewModelKtxDep
|
||||
implementation liveDataKtxDep
|
||||
implementation lifeCycleKtxDep
|
||||
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
|
||||
implementation daggerDep
|
||||
ksp daggerCompiler
|
||||
|
||||
implementation roomDep
|
||||
ksp roomCompiler
|
||||
|
||||
implementation lifecycleDep
|
||||
ksp lifecycleCompiler
|
||||
|
||||
testImplementation jUnitDep
|
||||
testImplementation mockitoDep
|
||||
|
||||
implementation substrateSdkDep
|
||||
|
||||
implementation gsonDep
|
||||
implementation retrofitDep
|
||||
|
||||
implementation insetterDep
|
||||
|
||||
implementation shimmerDep
|
||||
|
||||
androidTestImplementation androidTestRunnerDep
|
||||
androidTestImplementation androidTestRulesDep
|
||||
androidTestImplementation androidJunitDep
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_nft_impl
|
||||
|
||||
interface NftRouter {
|
||||
|
||||
fun openNftDetails(nftId: String)
|
||||
|
||||
fun back()
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.mappers
|
||||
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.core_db.model.NftLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import java.math.BigInteger
|
||||
|
||||
fun mapNftTypeLocalToTypeKey(
|
||||
typeLocal: NftLocal.Type
|
||||
): Nft.Type.Key = when (typeLocal) {
|
||||
NftLocal.Type.UNIQUES -> Nft.Type.Key.UNIQUES
|
||||
NftLocal.Type.RMRK1 -> Nft.Type.Key.RMRKV1
|
||||
NftLocal.Type.RMRK2 -> Nft.Type.Key.RMRKV2
|
||||
NftLocal.Type.PDC20 -> Nft.Type.Key.PDC20
|
||||
NftLocal.Type.KODADOT -> Nft.Type.Key.KODADOT
|
||||
NftLocal.Type.UNIQUE_NETWORK -> Nft.Type.Key.UNIQUE_NETWORK
|
||||
}
|
||||
|
||||
fun nftIssuance(
|
||||
typeLocal: NftLocal.IssuanceType,
|
||||
issuanceTotal: BigInteger?,
|
||||
issuanceMyEdition: String?,
|
||||
issuanceMyAmount: BigInteger?
|
||||
): Nft.Issuance {
|
||||
return when (typeLocal) {
|
||||
NftLocal.IssuanceType.UNLIMITED -> Nft.Issuance.Unlimited
|
||||
|
||||
NftLocal.IssuanceType.LIMITED -> {
|
||||
val myEditionInt = issuanceMyEdition?.toIntOrNull()
|
||||
|
||||
if (issuanceTotal != null && !issuanceTotal.isZero && myEditionInt != null) {
|
||||
Nft.Issuance.Limited(max = issuanceTotal.toInt(), edition = myEditionInt)
|
||||
} else {
|
||||
Nft.Issuance.Unlimited
|
||||
}
|
||||
}
|
||||
NftLocal.IssuanceType.FUNGIBLE -> if (issuanceTotal != null && issuanceMyAmount != null) {
|
||||
Nft.Issuance.Fungible(myAmount = issuanceMyAmount, totalSupply = issuanceTotal)
|
||||
} else {
|
||||
Nft.Issuance.Unlimited
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nftIssuance(nftLocal: NftLocal): Nft.Issuance {
|
||||
require(nftLocal.wholeDetailsLoaded)
|
||||
|
||||
return nftIssuance(nftLocal.issuanceType, nftLocal.issuanceTotal, nftLocal.issuanceMyEdition, nftLocal.issuanceMyAmount)
|
||||
}
|
||||
|
||||
fun nftPrice(nftLocal: NftLocal): Nft.Price? {
|
||||
val price = nftLocal.price
|
||||
if (price == null || price == BigInteger.ZERO) return null
|
||||
|
||||
return when (val units = nftLocal.pricedUnits) {
|
||||
null -> Nft.Price.NonFungible(price)
|
||||
else -> Nft.Price.Fungible(units = units, totalPrice = price)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapNftLocalToNft(
|
||||
chainsById: Map<ChainId, Chain>,
|
||||
metaAccount: MetaAccount,
|
||||
nftLocal: NftLocal
|
||||
): Nft? {
|
||||
val chain = chainsById[nftLocal.chainId] ?: return null
|
||||
|
||||
val type = when (nftLocal.type) {
|
||||
NftLocal.Type.UNIQUES -> Nft.Type.Uniques
|
||||
NftLocal.Type.RMRK1 -> Nft.Type.Rmrk1
|
||||
NftLocal.Type.RMRK2 -> Nft.Type.Rmrk2
|
||||
NftLocal.Type.PDC20 -> Nft.Type.Pdc20
|
||||
NftLocal.Type.KODADOT -> Nft.Type.Kodadot
|
||||
NftLocal.Type.UNIQUE_NETWORK -> Nft.Type.UniqueNetwork
|
||||
}
|
||||
|
||||
val details = if (nftLocal.wholeDetailsLoaded) {
|
||||
val issuance = nftIssuance(nftLocal)
|
||||
|
||||
Nft.Details.Loaded(
|
||||
name = nftLocal.name,
|
||||
label = nftLocal.label,
|
||||
media = nftLocal.media,
|
||||
price = nftPrice(nftLocal),
|
||||
issuance = issuance,
|
||||
)
|
||||
} else {
|
||||
Nft.Details.Loadable
|
||||
}
|
||||
|
||||
return Nft(
|
||||
identifier = nftLocal.identifier,
|
||||
instanceId = nftLocal.instanceId,
|
||||
collectionId = nftLocal.collectionId,
|
||||
chain = chain,
|
||||
owner = metaAccount.accountIdIn(chain)!!,
|
||||
metadataRaw = nftLocal.metadata,
|
||||
type = type,
|
||||
details = details
|
||||
)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.network.distributed
|
||||
|
||||
enum class FileStorage(val prefix: String, val additionalPaths: List<String>, val defaultHttpsGateway: String?) {
|
||||
IPFS("ipfs://", listOf("ipfs/"), "https://bucket.chaotic.art/ipfs/"),
|
||||
HTTPS("https://", emptyList(), null),
|
||||
HTTP("http://", emptyList(), null);
|
||||
|
||||
init {
|
||||
validateHttpsGateway(defaultHttpsGateway)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateHttpsGateway(gateway: String?) {
|
||||
require(gateway == null || gateway.endsWith("/")) {
|
||||
"Gateway should end with '/' separator"
|
||||
}
|
||||
}
|
||||
|
||||
object FileStorageAdapter {
|
||||
|
||||
fun String.adoptFileStorageLinkToHttps() = adaptToHttps(this)
|
||||
|
||||
fun adaptToHttps(distributedStorageLink: String): String {
|
||||
val distributedStorage = FileStorage.values().firstOrNull { storage ->
|
||||
distributedStorageLink.pointsTo(storage)
|
||||
} ?: FileStorage.IPFS
|
||||
|
||||
val gateway = distributedStorage.defaultHttpsGateway ?: return distributedStorageLink
|
||||
|
||||
validateHttpsGateway(gateway)
|
||||
|
||||
var path = distributedStorageLink.removePrefix(distributedStorage.prefix)
|
||||
distributedStorage.additionalPaths.forEach {
|
||||
path = path.removePrefix(it)
|
||||
}
|
||||
|
||||
return "$gateway$path"
|
||||
}
|
||||
|
||||
private fun String.pointsTo(fileStorage: FileStorage) = startsWith(fileStorage.prefix)
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.repository
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
|
||||
import io.novafoundation.nova.common.utils.transformLatestDiffed
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftSyncTrigger
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.mapNftLocalToNft
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.mapNftTypeLocalToTypeKey
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.JobOrchestrator
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvidersRegistry
|
||||
import io.novafoundation.nova.runtime.ext.level
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainById
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChains
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainsFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val NFT_TAG = "NFT"
|
||||
|
||||
class NftRepositoryImpl(
|
||||
private val nftProvidersRegistry: NftProvidersRegistry,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val jobOrchestrator: JobOrchestrator,
|
||||
private val nftDao: NftDao,
|
||||
private val exceptionHandler: HttpExceptionHandler,
|
||||
) : NftRepository {
|
||||
|
||||
override fun allNftFlow(metaAccount: MetaAccount): Flow<List<Nft>> {
|
||||
return nftDao.nftsFlow(metaAccount.id)
|
||||
.map { nftsLocal ->
|
||||
val chainsById = chainRegistry.enabledChainById()
|
||||
|
||||
nftsLocal.mapNotNull { nftLocal ->
|
||||
mapNftLocalToNft(chainsById, metaAccount, nftLocal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun nftDetails(nftId: String): Flow<NftDetails> {
|
||||
return flow {
|
||||
val nftTypeKey = mapNftTypeLocalToTypeKey(nftDao.getNftType(nftId))
|
||||
val nftProvider = nftProvidersRegistry.get(nftTypeKey)
|
||||
|
||||
emitAll(nftProvider.nftDetailsFlow(nftId))
|
||||
}.catch { throw exceptionHandler.transformException(it) }
|
||||
}
|
||||
|
||||
override fun initialNftSyncTrigger(): Flow<NftSyncTrigger> {
|
||||
return chainRegistry.enabledChainsFlow()
|
||||
.map { chains -> chains.filter { nftProvidersRegistry.nftSupported(it) } }
|
||||
.transformLatestDiffed { emit(NftSyncTrigger(it)) }
|
||||
}
|
||||
|
||||
override suspend fun initialNftSync(
|
||||
metaAccount: MetaAccount,
|
||||
forceOverwrite: Boolean,
|
||||
): Unit = withContext(Dispatchers.IO) {
|
||||
val chains = chainRegistry.enabledChains()
|
||||
|
||||
val syncJobs = chains.flatMap { chain ->
|
||||
nftSyncJobs(chain, metaAccount, forceOverwrite)
|
||||
}
|
||||
|
||||
syncJobs.joinAll()
|
||||
}
|
||||
|
||||
override suspend fun initialNftSync(metaAccount: MetaAccount, chain: Chain) = withContext(Dispatchers.IO) {
|
||||
val syncJobs = nftSyncJobs(chain, metaAccount, forceOverwrite = false)
|
||||
|
||||
syncJobs.joinAll()
|
||||
}
|
||||
|
||||
private fun CoroutineScope.nftSyncJobs(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean): List<Job> {
|
||||
return nftProvidersRegistry.get(chain)
|
||||
.filter { it.canSyncIn(chain) }
|
||||
.map { nftProvider ->
|
||||
// launch separate job per each nftProvider
|
||||
launch {
|
||||
// prevent whole sync from failing if some particular provider fails
|
||||
runCatching {
|
||||
nftProvider.initialNftsSync(chain, metaAccount, forceOverwrite)
|
||||
|
||||
Log.d(NFT_TAG, "Completed sync in ${chain.name} using ${nftProvider::class.simpleName}")
|
||||
}.onFailure {
|
||||
Log.e(NFT_TAG, "Failed to sync nfts in ${chain.name} using ${nftProvider::class.simpleName}", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fullNftSync(nft: Nft) = withContext(Dispatchers.IO) {
|
||||
jobOrchestrator.runUniqueJob(nft.identifier) {
|
||||
runCatching {
|
||||
nftProvidersRegistry.get(nft.type.key).nftFullSync(nft)
|
||||
}.onFailure {
|
||||
Log.e(NFT_TAG, "Failed to fully sync nft ${nft.identifier} in ${nft.chain.name} with type ${nft.type::class.simpleName}", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NftProvider.canSyncIn(chain: Chain): Boolean {
|
||||
val requiredStage = if (requireFullChainSync) Chain.ConnectionState.FULL_SYNC else Chain.ConnectionState.LIGHT_SYNC
|
||||
|
||||
return chain.connectionState.level >= requiredStage.level
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class JobOrchestrator {
|
||||
|
||||
private val runningJobs: MutableSet<String> = Collections.newSetFromMap(ConcurrentHashMap())
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun runUniqueJob(id: String, action: suspend () -> Unit) = mutex.withLock {
|
||||
if (id in runningJobs) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
runningJobs += id
|
||||
|
||||
CoroutineScope(coroutineContext).async { action() }
|
||||
.invokeOnCompletion { runningJobs -= id }
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NftProvider {
|
||||
|
||||
val requireFullChainSync: Boolean
|
||||
|
||||
suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean)
|
||||
|
||||
suspend fun nftFullSync(nft: Nft)
|
||||
|
||||
fun nftDetailsFlow(nftIdentifier: String): Flow<NftDetails>
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source
|
||||
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.KodadotProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.Pdc20Provider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.RmrkV1NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.RmrkV2NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.UniquesNftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.UniqueNetworkNftProvider
|
||||
import io.novafoundation.nova.runtime.ext.Geneses
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class NftProvidersRegistry(
|
||||
private val uniquesNftProvider: UniquesNftProvider,
|
||||
private val rmrkV1NftProvider: RmrkV1NftProvider,
|
||||
private val rmrkV2NftProvider: RmrkV2NftProvider,
|
||||
private val pdc20Provider: Pdc20Provider,
|
||||
private val kodadotProvider: KodadotProvider,
|
||||
private val uniqueNetworkNftProvider: UniqueNetworkNftProvider,
|
||||
) {
|
||||
|
||||
private val kusamaAssetHubProviders = listOf(uniquesNftProvider, kodadotProvider)
|
||||
private val kusamaProviders = listOf(rmrkV1NftProvider, rmrkV2NftProvider)
|
||||
private val polkadotProviders = listOf(pdc20Provider)
|
||||
private val polkadotAssetHubProviders = listOf(kodadotProvider)
|
||||
private val uniqueNetworkProviders = listOf(uniqueNetworkNftProvider)
|
||||
|
||||
fun get(chain: Chain): List<NftProvider> {
|
||||
return when (chain.id) {
|
||||
Chain.Geneses.KUSAMA_ASSET_HUB -> kusamaAssetHubProviders
|
||||
Chain.Geneses.KUSAMA -> kusamaProviders
|
||||
Chain.Geneses.POLKADOT -> polkadotProviders
|
||||
Chain.Geneses.POLKADOT_ASSET_HUB -> polkadotAssetHubProviders
|
||||
Chain.Geneses.UNIQUE_NETWORK -> uniqueNetworkProviders
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun nftSupported(chain: Chain): Boolean {
|
||||
return get(chain).isNotEmpty()
|
||||
}
|
||||
|
||||
fun get(nftTypeKey: Nft.Type.Key): NftProvider {
|
||||
return when (nftTypeKey) {
|
||||
Nft.Type.Key.RMRKV1 -> rmrkV1NftProvider
|
||||
Nft.Type.Key.RMRKV2 -> rmrkV2NftProvider
|
||||
Nft.Type.Key.UNIQUES -> uniquesNftProvider
|
||||
Nft.Type.Key.PDC20 -> pdc20Provider
|
||||
Nft.Type.Key.KODADOT -> kodadotProvider
|
||||
Nft.Type.Key.UNIQUE_NETWORK -> uniqueNetworkNftProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.scopeAsync
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.core_db.model.NftLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice
|
||||
import io.novafoundation.nova.feature_nft_impl.data.network.distributed.FileStorageAdapter.adoptFileStorageLinkToHttps
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.KodadotApi
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request.KodadotCollectionRequest
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request.KodadotMetadataRequest
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response.KodadotNftRemote
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request.KodadotNftsRequest
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response.KodadotCollectionRemote
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response.KodadotMetadataRemote
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigInteger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
private const val NO_COLLECTION_LOCAL_ID = "no_collection_local_id"
|
||||
|
||||
class KodadotProvider(
|
||||
private val api: KodadotApi,
|
||||
private val nftDao: NftDao,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) : NftProvider {
|
||||
|
||||
override val requireFullChainSync: Boolean = false
|
||||
|
||||
override suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean) {
|
||||
val address = metaAccount.addressIn(chain) ?: return
|
||||
|
||||
val apiUrl = getApiUrl(chain) ?: return
|
||||
val request = KodadotNftsRequest(address)
|
||||
val nfts = api.getNfts(apiUrl, request)
|
||||
|
||||
val toSave = nfts.data.nftEntities.map { nftRemote ->
|
||||
NftLocal(
|
||||
identifier = nftIdentifier(chain, nftRemote),
|
||||
metaId = metaAccount.id,
|
||||
chainId = chain.id,
|
||||
collectionId = nftRemote.collection?.id ?: NO_COLLECTION_LOCAL_ID,
|
||||
instanceId = nftRemote.id,
|
||||
metadata = nftRemote.metadata?.encodeToByteArray(),
|
||||
type = NftLocal.Type.KODADOT,
|
||||
wholeDetailsLoaded = true,
|
||||
name = nftRemote.name,
|
||||
label = nftRemote.sn,
|
||||
media = nftRemote.image?.adoptFileStorageLinkToHttps(),
|
||||
issuanceType = nftRemote.collection?.max?.let { NftLocal.IssuanceType.LIMITED } ?: NftLocal.IssuanceType.UNLIMITED,
|
||||
issuanceTotal = nftRemote.collection?.max?.let { BigInteger(it) },
|
||||
issuanceMyAmount = null,
|
||||
price = nftRemote.price?.let { BigInteger(it) },
|
||||
pricedUnits = null
|
||||
)
|
||||
}
|
||||
|
||||
nftDao.insertNftsDiff(NftLocal.Type.KODADOT, chain.id, metaAccount.id, toSave, forceOverwrite)
|
||||
}
|
||||
|
||||
override suspend fun nftFullSync(nft: Nft) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun nftDetailsFlow(nftIdentifier: String): Flow<NftDetails> {
|
||||
return flowOf {
|
||||
val nftLocal = nftDao.getNft(nftIdentifier)
|
||||
require(nftLocal.wholeDetailsLoaded) {
|
||||
"Cannot load details of non fully-synced NFT"
|
||||
}
|
||||
|
||||
val chain = chainRegistry.getChain(nftLocal.chainId)
|
||||
val metaAccount = accountRepository.getMetaAccount(nftLocal.metaId)
|
||||
|
||||
val metadataDeferred = scopeAsync { fetchMetadata(nftLocal, chain) }
|
||||
val collectionDeferred = scopeAsync { fetchCollection(nftLocal, chain) }
|
||||
|
||||
val metadata = metadataDeferred.await()
|
||||
val collection = collectionDeferred.await()
|
||||
|
||||
NftDetails(
|
||||
identifier = nftLocal.identifier,
|
||||
chain = chain,
|
||||
owner = metaAccount.requireAccountIdIn(chain),
|
||||
creator = collection?.issuer?.let { chain.accountIdOf(it) },
|
||||
media = metadata?.image?.adoptFileStorageLinkToHttps() ?: nftLocal.media,
|
||||
name = metadata?.name ?: nftLocal.name ?: nftLocal.instanceId!!,
|
||||
description = metadata?.description,
|
||||
issuance = nftIssuance(nftLocal),
|
||||
price = nftPrice(nftLocal),
|
||||
collection = collection?.let {
|
||||
NftDetails.Collection(
|
||||
id = nftLocal.collectionId,
|
||||
name = it.name,
|
||||
media = it.image
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchMetadata(nftLocal: NftLocal, chain: Chain): KodadotMetadataRemote? {
|
||||
val metadataId = nftLocal.metadata?.decodeToString() ?: return null
|
||||
val apiUrl = getApiUrl(chain) ?: return null
|
||||
val request = KodadotMetadataRequest(metadataId)
|
||||
return api.getMetadata(apiUrl, request)
|
||||
.data
|
||||
.metadataEntityById
|
||||
}
|
||||
|
||||
private suspend fun fetchCollection(nftLocal: NftLocal, chain: Chain): KodadotCollectionRemote? {
|
||||
val collectionId = nftLocal.collectionId
|
||||
if (collectionId == NO_COLLECTION_LOCAL_ID) {
|
||||
return null
|
||||
}
|
||||
|
||||
val apiUrl = getApiUrl(chain) ?: return null
|
||||
val request = KodadotCollectionRequest(collectionId)
|
||||
return api.getCollection(apiUrl, request)
|
||||
.data
|
||||
.collectionEntityById
|
||||
}
|
||||
|
||||
private fun nftIdentifier(chain: Chain, nft: KodadotNftRemote): String {
|
||||
return "kodadot-${chain.id}-${nft.id}"
|
||||
}
|
||||
|
||||
private fun getApiUrl(chain: Chain): String? {
|
||||
return when (chain.id) {
|
||||
ChainGeneses.POLKADOT_ASSET_HUB -> KodadotApi.POLKADOT_ASSET_HUB_URL
|
||||
ChainGeneses.KUSAMA_ASSET_HUB -> KodadotApi.KUSAMA_ASSET_HUB_URL
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network
|
||||
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request.KodadotCollectionRequest
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request.KodadotMetadataRequest
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request.KodadotNftsRequest
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response.KodadotCollectionResponse
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response.KodadotMetadataResponse
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response.KodadotNftResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface KodadotApi {
|
||||
|
||||
companion object {
|
||||
const val POLKADOT_ASSET_HUB_URL = "https://ahp.gql.api.kodadot.xyz"
|
||||
const val KUSAMA_ASSET_HUB_URL = "https://ahk.gql.api.kodadot.xyz"
|
||||
}
|
||||
|
||||
@POST
|
||||
suspend fun getNfts(@Url url: String, @Body request: KodadotNftsRequest): SubQueryResponse<KodadotNftResponse>
|
||||
|
||||
@POST
|
||||
suspend fun getCollection(@Url url: String, @Body request: KodadotCollectionRequest): SubQueryResponse<KodadotCollectionResponse>
|
||||
|
||||
@POST
|
||||
suspend fun getMetadata(@Url url: String, @Body request: KodadotMetadataRequest): SubQueryResponse<KodadotMetadataResponse>
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request
|
||||
|
||||
class KodadotCollectionRequest(collectionId: String) {
|
||||
|
||||
val query = """
|
||||
{
|
||||
collectionEntityById(id: "$collectionId") {
|
||||
name
|
||||
image
|
||||
issuer
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request
|
||||
|
||||
class KodadotMetadataRequest(metadataId: String) {
|
||||
|
||||
val query = """
|
||||
{
|
||||
metadataEntityById(id: "$metadataId") {
|
||||
image
|
||||
name
|
||||
type
|
||||
description
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.request
|
||||
|
||||
class KodadotNftsRequest(userAddress: String) {
|
||||
|
||||
val query = """
|
||||
query nftListByOwner(${'$'}id: String!) {
|
||||
nftEntities(where: {currentOwner_eq: ${'$'}id, burned_eq: false}) {
|
||||
id
|
||||
image
|
||||
metadata
|
||||
name
|
||||
price
|
||||
sn
|
||||
currentOwner
|
||||
collection {
|
||||
id
|
||||
max
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val variables = mapOf("id" to userAddress)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response
|
||||
|
||||
class KodadotCollectionResponse(
|
||||
val collectionEntityById: KodadotCollectionRemote?
|
||||
)
|
||||
|
||||
class KodadotCollectionRemote(
|
||||
val name: String?,
|
||||
val image: String?,
|
||||
val issuer: String?
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response
|
||||
|
||||
class KodadotMetadataResponse(
|
||||
val metadataEntityById: KodadotMetadataRemote?
|
||||
)
|
||||
|
||||
class KodadotMetadataRemote(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val type: String?,
|
||||
val image: String?
|
||||
)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.response
|
||||
|
||||
class KodadotNftResponse(
|
||||
val nftEntities: List<KodadotNftRemote>
|
||||
)
|
||||
|
||||
class KodadotNftRemote(
|
||||
val id: String,
|
||||
val image: String?,
|
||||
val metadata: String?,
|
||||
val name: String?,
|
||||
val price: String?,
|
||||
val sn: String?,
|
||||
val currentOwner: String,
|
||||
val collection: Collection?
|
||||
) {
|
||||
|
||||
class Collection(
|
||||
val id: String,
|
||||
val max: String
|
||||
)
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.core_db.model.NftLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Api
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Listing
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Request
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class Pdc20Provider(
|
||||
private val api: Pdc20Api,
|
||||
private val nftDao: NftDao,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) : NftProvider {
|
||||
|
||||
override val requireFullChainSync: Boolean = false
|
||||
|
||||
override suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean) {
|
||||
val address = metaAccount.addressIn(chain) ?: return
|
||||
|
||||
val request = Pdc20Request(address, network = Pdc20Api.NETWORK_POLKADOT)
|
||||
val nfts = api.getNfts(request)
|
||||
|
||||
val aggregatedListingsByToken = nfts.data.listings.groupBy { it.token.id }
|
||||
.mapValues { (_, listings) ->
|
||||
listings.reduce(Pdc20Listing::plus)
|
||||
}
|
||||
|
||||
val toSave = nfts.data.userTokenBalances.map { nftRemote ->
|
||||
val listing = aggregatedListingsByToken[nftRemote.token.id]
|
||||
|
||||
NftLocal(
|
||||
identifier = nftRemote.token.id,
|
||||
metaId = metaAccount.id,
|
||||
chainId = chain.id,
|
||||
collectionId = nftRemote.token.id,
|
||||
instanceId = nftRemote.token.id,
|
||||
metadata = null,
|
||||
type = NftLocal.Type.PDC20,
|
||||
wholeDetailsLoaded = true,
|
||||
name = nftRemote.token.ticker,
|
||||
label = null,
|
||||
media = nftRemote.token.logo,
|
||||
issuanceType = NftLocal.IssuanceType.FUNGIBLE,
|
||||
// We dont know if supply or holding amount can be fractional or not so we are behaving safe
|
||||
issuanceTotal = nftRemote.token.totalSupply?.toBigIntegerOrNull(),
|
||||
issuanceMyAmount = nftRemote.balance.toBigIntegerOrNull(),
|
||||
price = listing?.value?.let { chain.utilityAsset.planksFromAmount(it) },
|
||||
pricedUnits = listing?.amount
|
||||
)
|
||||
}
|
||||
|
||||
nftDao.insertNftsDiff(NftLocal.Type.PDC20, chain.id, metaAccount.id, toSave, forceOverwrite)
|
||||
}
|
||||
|
||||
override suspend fun nftFullSync(nft: Nft) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun nftDetailsFlow(nftIdentifier: String): Flow<NftDetails> {
|
||||
return flowOf {
|
||||
val nftLocal = nftDao.getNft(nftIdentifier)
|
||||
require(nftLocal.wholeDetailsLoaded) {
|
||||
"Cannot load details of non fully-synced NFT"
|
||||
}
|
||||
|
||||
val chain = chainRegistry.getChain(nftLocal.chainId)
|
||||
val metaAccount = accountRepository.getMetaAccount(nftLocal.metaId)
|
||||
|
||||
NftDetails(
|
||||
identifier = nftLocal.identifier,
|
||||
chain = chain,
|
||||
owner = metaAccount.requireAccountIdIn(chain),
|
||||
creator = null,
|
||||
media = nftLocal.media,
|
||||
name = nftLocal.name ?: nftLocal.instanceId!!,
|
||||
description = null,
|
||||
issuance = nftIssuance(nftLocal),
|
||||
price = nftPrice(nftLocal),
|
||||
collection = null // pdc20 token is the same as collection
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Pdc20Listing.plus(other: Pdc20Listing): Pdc20Listing {
|
||||
require(this.from.address == other.from.address)
|
||||
require(this.token.id == other.token.id)
|
||||
|
||||
return Pdc20Listing(
|
||||
from = from,
|
||||
token = token,
|
||||
amount = amount + other.amount,
|
||||
value = value + other.value
|
||||
)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network
|
||||
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface Pdc20Api {
|
||||
|
||||
companion object {
|
||||
const val NETWORK_POLKADOT = "polkadot"
|
||||
}
|
||||
|
||||
@POST("https://squid.subsquid.io/dot-ordinals/graphql")
|
||||
suspend fun getNfts(@Body request: Pdc20Request): SubQueryResponse<Pdc20NftResponse>
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
class Pdc20NftResponse(
|
||||
val userTokenBalances: List<Pdc20NftRemote>,
|
||||
val listings: List<Pdc20Listing>
|
||||
)
|
||||
|
||||
class Pdc20NftRemote(
|
||||
val balance: String,
|
||||
val address: PdcAddress,
|
||||
val token: Token
|
||||
) {
|
||||
|
||||
class Token(
|
||||
val id: String,
|
||||
val logo: String?,
|
||||
val ticker: String?,
|
||||
val totalSupply: String?,
|
||||
val network: String
|
||||
)
|
||||
}
|
||||
|
||||
class Pdc20Listing(
|
||||
val from: PdcAddress,
|
||||
val token: Token,
|
||||
val amount: BigInteger,
|
||||
val value: BigDecimal
|
||||
) {
|
||||
|
||||
class Token(
|
||||
val id: String
|
||||
)
|
||||
}
|
||||
|
||||
class PdcAddress(val address: String)
|
||||
|
||||
class RmrkV1NftMetadataRemote(
|
||||
val image: String,
|
||||
val description: String
|
||||
)
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network
|
||||
|
||||
class Pdc20Request(userAddress: String, network: String) {
|
||||
|
||||
val query = """
|
||||
query {
|
||||
userTokenBalances(
|
||||
where: {
|
||||
address: {
|
||||
address_eq: "$userAddress"
|
||||
}
|
||||
standard_eq: "pdc-20"
|
||||
token: { network_eq: "$network" }
|
||||
}
|
||||
) {
|
||||
balance
|
||||
address {
|
||||
address
|
||||
}
|
||||
token {
|
||||
id
|
||||
logo
|
||||
ticker
|
||||
totalSupply
|
||||
network
|
||||
}
|
||||
}
|
||||
|
||||
listings(
|
||||
where: {
|
||||
from: { address_eq: "$userAddress" }
|
||||
standard_eq: "pdc-20"
|
||||
token: { network_eq: "$network" }
|
||||
}
|
||||
) {
|
||||
from {
|
||||
address
|
||||
}
|
||||
|
||||
token {
|
||||
id
|
||||
}
|
||||
|
||||
amount
|
||||
value
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.core_db.model.NftLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.network.RmrkV1Api
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RmrkV1NftProvider(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val api: RmrkV1Api,
|
||||
private val nftDao: NftDao
|
||||
) : NftProvider {
|
||||
|
||||
override val requireFullChainSync: Boolean = false
|
||||
|
||||
override suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean) {
|
||||
throw UnsupportedOperationException("RmrkV1 not supported")
|
||||
}
|
||||
|
||||
override suspend fun nftFullSync(nft: Nft) {
|
||||
throw UnsupportedOperationException("RmrkV1 not supported")
|
||||
}
|
||||
|
||||
override fun nftDetailsFlow(nftIdentifier: String): Flow<NftDetails> {
|
||||
return flowOf {
|
||||
val nftLocal = nftDao.getNft(nftIdentifier)
|
||||
require(nftLocal.wholeDetailsLoaded) {
|
||||
"Cannot load details of non fully-synced NFT"
|
||||
}
|
||||
val chain = chainRegistry.getChain(nftLocal.chainId)
|
||||
val metaAccount = accountRepository.getMetaAccount(nftLocal.metaId)
|
||||
|
||||
NftDetails(
|
||||
identifier = nftLocal.identifier,
|
||||
chain = chain,
|
||||
owner = metaAccount.accountIdIn(chain)!!,
|
||||
creator = null,
|
||||
media = nftLocal.media,
|
||||
name = nftLocal.name!!,
|
||||
description = nftLocal.label,
|
||||
issuance = nftIssuance(NftLocal.IssuanceType.LIMITED, nftLocal.issuanceTotal, nftLocal.issuanceMyEdition, nftLocal.issuanceMyAmount),
|
||||
price = nftPrice(nftLocal),
|
||||
collection = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.network
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface RmrkV1Api {
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://singular.rmrk-api.xyz/api/"
|
||||
}
|
||||
|
||||
@GET("https://singular.rmrk.app/api/rmrk1/collection/{collectionId}")
|
||||
suspend fun getCollection(@Path("collectionId") collectionId: String): List<RmrkV1CollectionRemote>
|
||||
|
||||
@GET
|
||||
suspend fun getIpfsMetadata(@Url url: String): RmrkV1NftMetadataRemote
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.network
|
||||
|
||||
class RmrkV1CollectionRemote(
|
||||
val max: Int,
|
||||
val name: String,
|
||||
val issuer: String,
|
||||
val metadata: String?
|
||||
)
|
||||
|
||||
class RmrkV1NftMetadataRemote(
|
||||
val image: String,
|
||||
val description: String
|
||||
)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.core_db.model.NftLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice
|
||||
import io.novafoundation.nova.feature_nft_impl.data.network.distributed.FileStorageAdapter.adoptFileStorageLinkToHttps
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.network.singular.SingularV2Api
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RmrkV2NftProvider(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val singularV2Api: SingularV2Api,
|
||||
private val nftDao: NftDao
|
||||
) : NftProvider {
|
||||
|
||||
override val requireFullChainSync: Boolean = false
|
||||
|
||||
override suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean) {
|
||||
val address = metaAccount.addressIn(chain) ?: return
|
||||
val nfts = singularV2Api.getAccountNfts(address)
|
||||
|
||||
val toSave = nfts.map {
|
||||
NftLocal(
|
||||
identifier = localIdentifier(chain.id, it.id),
|
||||
metaId = metaAccount.id,
|
||||
chainId = chain.id,
|
||||
collectionId = it.collectionId,
|
||||
instanceId = it.id,
|
||||
metadata = it.metadata?.encodeToByteArray(),
|
||||
media = it.image?.adoptFileStorageLinkToHttps(),
|
||||
|
||||
// let name default to symbol and label to edition in case full sync wont be able to determine them from metadata
|
||||
name = it.symbol,
|
||||
label = it.edition,
|
||||
|
||||
price = it.price,
|
||||
type = NftLocal.Type.RMRK2,
|
||||
issuanceMyEdition = it.edition,
|
||||
wholeDetailsLoaded = false,
|
||||
issuanceType = NftLocal.IssuanceType.LIMITED
|
||||
)
|
||||
}
|
||||
|
||||
nftDao.insertNftsDiff(NftLocal.Type.RMRK2, chain.id, metaAccount.id, toSave, forceOverwrite)
|
||||
}
|
||||
|
||||
override suspend fun nftFullSync(nft: Nft) {
|
||||
val metadata = nft.metadataRaw?.let {
|
||||
val metadataLink = it.decodeToString().adoptFileStorageLinkToHttps()
|
||||
|
||||
singularV2Api.getIpfsMetadata(metadataLink)
|
||||
}
|
||||
|
||||
val collection = singularV2Api.getCollection(nft.collectionId).first()
|
||||
|
||||
nftDao.updateNft(nft.identifier) { local ->
|
||||
// media fetched during initial sync (prerender) has more priority than one from metadata
|
||||
val image = local.media ?: metadata?.image?.adoptFileStorageLinkToHttps()
|
||||
|
||||
local.copy(
|
||||
media = image,
|
||||
issuanceTotal = collection.max?.toBigInteger(),
|
||||
name = metadata?.name ?: local.name,
|
||||
label = metadata?.description ?: local.label,
|
||||
wholeDetailsLoaded = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nftDetailsFlow(nftIdentifier: String): Flow<NftDetails> {
|
||||
return flowOf {
|
||||
val nftLocal = nftDao.getNft(nftIdentifier)
|
||||
require(nftLocal.wholeDetailsLoaded) {
|
||||
"Cannot load details of non fully-synced NFT"
|
||||
}
|
||||
val chain = chainRegistry.getChain(nftLocal.chainId)
|
||||
val metaAccount = accountRepository.getMetaAccount(nftLocal.metaId)
|
||||
|
||||
val collection = singularV2Api.getCollection(nftLocal.collectionId).first()
|
||||
val collectionMetadata = collection.metadata?.let {
|
||||
singularV2Api.getIpfsMetadata(it.adoptFileStorageLinkToHttps())
|
||||
}
|
||||
|
||||
NftDetails(
|
||||
identifier = nftLocal.identifier,
|
||||
chain = chain,
|
||||
owner = metaAccount.accountIdIn(chain)!!,
|
||||
creator = chain.accountIdOf(collection.issuer),
|
||||
media = nftLocal.media,
|
||||
name = nftLocal.name!!,
|
||||
description = nftLocal.label,
|
||||
issuance = nftIssuance(nftLocal),
|
||||
price = nftPrice(nftLocal),
|
||||
collection = NftDetails.Collection(
|
||||
id = nftLocal.collectionId,
|
||||
name = collectionMetadata?.name,
|
||||
media = collectionMetadata?.image?.adoptFileStorageLinkToHttps()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun localIdentifier(chainId: ChainId, remoteId: String): String {
|
||||
return "$chainId-$remoteId"
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.network.singular
|
||||
|
||||
import io.novafoundation.nova.common.data.network.http.CacheControl
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface SingularV2Api {
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://singular.rmrk-api.xyz/api/"
|
||||
}
|
||||
|
||||
@GET("account/{accountAddress}")
|
||||
@Headers(CacheControl.NO_CACHE)
|
||||
suspend fun getAccountNfts(@Path("accountAddress") accountAddress: String): List<SingularV2NftRemote>
|
||||
|
||||
@GET("https://singular.app/api/rmrk2/collection/{collectionId}")
|
||||
suspend fun getCollection(@Path("collectionId") collectionId: String): List<SingularV2CollectionRemote>
|
||||
|
||||
@GET
|
||||
suspend fun getIpfsMetadata(@Url url: String): SingularV2CollectionMetadata
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.network.singular
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.math.BigInteger
|
||||
|
||||
class SingularV2CollectionRemote(
|
||||
val metadata: String?,
|
||||
val issuer: String,
|
||||
val max: Int?
|
||||
)
|
||||
|
||||
class SingularV2NftRemote(
|
||||
val id: String,
|
||||
@SerializedName("forsale")
|
||||
val price: BigInteger?,
|
||||
val collectionId: String,
|
||||
@SerializedName("sn")
|
||||
val edition: String,
|
||||
val image: String?, // prerender, non-null if nft is composable
|
||||
val metadata: String?,
|
||||
val symbol: String,
|
||||
)
|
||||
|
||||
class SingularV2CollectionMetadata(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
|
||||
@SerializedName("image", alternate = ["mediaUri"])
|
||||
val image: String?
|
||||
)
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.core_db.model.NftLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.UniqueNetworkApi
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.response.UniqueNetworkNft
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class UniqueNetworkNftProvider(
|
||||
private val uniqueNetworkApi: UniqueNetworkApi,
|
||||
private val nftDao: NftDao,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) : NftProvider {
|
||||
override val requireFullChainSync: Boolean = false
|
||||
|
||||
override suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean) {
|
||||
val owner = metaAccount.addressIn(chain) ?: return
|
||||
val pageSize = 100
|
||||
var offset = 0
|
||||
val allNfts = mutableListOf<NftLocal>()
|
||||
|
||||
while (true) {
|
||||
val page = uniqueNetworkApi.getNftsPage(
|
||||
owner = owner,
|
||||
offset = offset,
|
||||
limit = pageSize
|
||||
)
|
||||
|
||||
if (page.items.isEmpty()) break
|
||||
|
||||
val pageNfts = page.items.map { remote ->
|
||||
NftLocal(
|
||||
identifier = nftIdentifier(chain, remote),
|
||||
metaId = metaAccount.id,
|
||||
chainId = chain.id,
|
||||
collectionId = remote.collectionId.toString(),
|
||||
instanceId = remote.tokenId.toString(),
|
||||
metadata = null,
|
||||
type = NftLocal.Type.UNIQUE_NETWORK,
|
||||
wholeDetailsLoaded = false,
|
||||
name = remote.name,
|
||||
label = "#${remote.tokenId}",
|
||||
media = remote.image,
|
||||
issuanceType = NftLocal.IssuanceType.UNLIMITED,
|
||||
issuanceTotal = null,
|
||||
issuanceMyEdition = remote.tokenId.toString(),
|
||||
issuanceMyAmount = null,
|
||||
price = null,
|
||||
pricedUnits = null
|
||||
)
|
||||
}
|
||||
|
||||
allNfts += pageNfts
|
||||
offset += pageSize
|
||||
|
||||
if (allNfts.size >= page.count) break
|
||||
}
|
||||
|
||||
nftDao.insertNftsDiff(NftLocal.Type.UNIQUE_NETWORK, chain.id, metaAccount.id, allNfts, forceOverwrite)
|
||||
}
|
||||
|
||||
override suspend fun nftFullSync(nft: Nft) {
|
||||
val collection = uniqueNetworkApi.getCollection(
|
||||
collectionId = nft.collectionId.toInt(),
|
||||
)
|
||||
|
||||
val issuanceTotal = collection.limits?.tokenLimit?.toBigInteger() ?: collection.lastTokenId?.toBigInteger()
|
||||
|
||||
val issuanceType = when {
|
||||
collection.limits?.tokenLimit != null -> NftLocal.IssuanceType.LIMITED
|
||||
else -> NftLocal.IssuanceType.UNLIMITED
|
||||
}
|
||||
|
||||
nftDao.updateNft(nft.identifier) { local ->
|
||||
local.copy(
|
||||
issuanceType = issuanceType,
|
||||
issuanceTotal = issuanceTotal,
|
||||
wholeDetailsLoaded = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nftDetailsFlow(nftIdentifier: String): Flow<NftDetails> {
|
||||
return flowOf {
|
||||
val nftLocal = nftDao.getNft(nftIdentifier)
|
||||
require(nftLocal.wholeDetailsLoaded) {
|
||||
"Cannot load details of non fully-synced NFT"
|
||||
}
|
||||
|
||||
val chain = chainRegistry.getChain(nftLocal.chainId)
|
||||
val metaAccount = accountRepository.getMetaAccount(nftLocal.metaId)
|
||||
|
||||
val remoteNft = uniqueNetworkApi.getNft(
|
||||
collectionId = nftLocal.collectionId.toInt(),
|
||||
tokenId = nftLocal.instanceId!!.toInt()
|
||||
)
|
||||
|
||||
val collection = uniqueNetworkApi.getCollection(
|
||||
collectionId = nftLocal.collectionId.toInt(),
|
||||
)
|
||||
|
||||
NftDetails(
|
||||
identifier = nftLocal.identifier,
|
||||
chain = chain,
|
||||
owner = metaAccount.requireAccountIdIn(chain),
|
||||
creator = null,
|
||||
media = nftLocal.media,
|
||||
name = nftLocal.name ?: nftLocal.instanceId!!,
|
||||
description = remoteNft.description,
|
||||
issuance = nftIssuance(nftLocal),
|
||||
price = nftPrice(nftLocal),
|
||||
collection = collection.let {
|
||||
NftDetails.Collection(
|
||||
id = nftLocal.collectionId,
|
||||
name = it.name,
|
||||
media = it.coverImage?.url
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nftIdentifier(chain: Chain, nft: UniqueNetworkNft): String {
|
||||
return "unique-${chain.id}-${nft.collectionId}-${nft.tokenId}"
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network
|
||||
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.response.UniqueNetworkCollection
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.response.UniqueNetworkNft
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.response.UniqueNetworkPaginatedResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface UniqueNetworkApi {
|
||||
companion object {
|
||||
const val BASE_URL = "https://api-unique.uniquescan.io/v2/"
|
||||
}
|
||||
|
||||
@GET("nfts")
|
||||
suspend fun getNftsPage(
|
||||
@Query("ownerIn") owner: String,
|
||||
@Query("offset") offset: Int,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("orderByTokenId") order: String = "asc"
|
||||
): UniqueNetworkPaginatedResponse<UniqueNetworkNft>
|
||||
|
||||
@GET("nfts/{collectionId}/{tokenId}")
|
||||
suspend fun getNft(
|
||||
@Path("collectionId") collectionId: Int,
|
||||
@Path("tokenId") tokenId: Int,
|
||||
): UniqueNetworkNft
|
||||
|
||||
@GET("collections/{collectionId}")
|
||||
suspend fun getCollection(
|
||||
@Path("collectionId") collectionId: Int,
|
||||
): UniqueNetworkCollection
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.response
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class UniqueNetworkCollection(
|
||||
val collectionId: Int,
|
||||
val name: String?,
|
||||
val coverImage: CoverImage?,
|
||||
val lastTokenId: Int?,
|
||||
val limits: Limits?
|
||||
) {
|
||||
data class CoverImage(
|
||||
val url: String?
|
||||
)
|
||||
|
||||
data class Limits(
|
||||
@SerializedName("token_limit")
|
||||
val tokenLimit: Int?,
|
||||
)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.response
|
||||
|
||||
data class UniqueNetworkNft(
|
||||
val key: String,
|
||||
val collectionId: Int,
|
||||
val tokenId: Int,
|
||||
val image: String?,
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.response
|
||||
|
||||
data class UniqueNetworkPaginatedResponse<T>(
|
||||
val items: List<T>,
|
||||
val count: Int
|
||||
)
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.cast
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.getTyped
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.uniques
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.core_db.model.NftLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice
|
||||
import io.novafoundation.nova.feature_nft_impl.data.network.distributed.FileStorageAdapter.adoptFileStorageLinkToHttps
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.network.IpfsApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novafoundation.nova.runtime.storage.source.multi.MultiQueryBuilder
|
||||
import io.novafoundation.nova.runtime.storage.source.multi.singleValueOf
|
||||
import io.novafoundation.nova.runtime.storage.source.query.multi
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.math.BigInteger
|
||||
|
||||
class UniquesNftProvider(
|
||||
private val remoteStorage: StorageDataSource,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val nftDao: NftDao,
|
||||
private val ipfsApi: IpfsApi,
|
||||
) : NftProvider {
|
||||
|
||||
override val requireFullChainSync: Boolean = true
|
||||
|
||||
override suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean) {
|
||||
val accountId = metaAccount.accountIdIn(chain) ?: return
|
||||
|
||||
val newNfts = remoteStorage.query(chain.id) {
|
||||
val classesWithInstances = runtime.metadata.uniques().storage("Account").keys(accountId)
|
||||
.map { (_: AccountId, collection: BigInteger, instance: BigInteger) ->
|
||||
listOf(collection, instance)
|
||||
}
|
||||
|
||||
val classesIds = classesWithInstances.map { (collection, _) -> collection }.distinct()
|
||||
|
||||
val classMetadataDescriptor: MultiQueryBuilder.Descriptor<BigInteger, ByteArray?>
|
||||
val totalIssuanceDescriptor: MultiQueryBuilder.Descriptor<BigInteger, BigInteger>
|
||||
val instanceMetadataDescriptor: MultiQueryBuilder.Descriptor<Pair<BigInteger, BigInteger>, ByteArray?>
|
||||
|
||||
val multiQueryResults = multi {
|
||||
classMetadataDescriptor = runtime.metadata.uniques().storage("ClassMetadataOf").querySingleArgKeys(
|
||||
keysArgs = classesIds,
|
||||
keyExtractor = { (classId: BigInteger) -> classId },
|
||||
binding = ::bindMetadata
|
||||
)
|
||||
instanceMetadataDescriptor = runtime.metadata.uniques().storage("InstanceMetadataOf").queryKeys(
|
||||
keysArgs = classesWithInstances,
|
||||
keyExtractor = { (classId: BigInteger, instance: BigInteger) -> classId to instance },
|
||||
binding = ::bindMetadata
|
||||
)
|
||||
totalIssuanceDescriptor = runtime.metadata.uniques().storage("Class").querySingleArgKeys(
|
||||
keysArgs = classesIds,
|
||||
keyExtractor = { (classId: BigInteger) -> classId },
|
||||
binding = { bindNumber(it.castToStruct()["items"]) }
|
||||
)
|
||||
}
|
||||
|
||||
val classMetadatas = multiQueryResults[classMetadataDescriptor]
|
||||
val totalIssuances = multiQueryResults[totalIssuanceDescriptor]
|
||||
val instancesMetadatas = multiQueryResults[instanceMetadataDescriptor]
|
||||
|
||||
classesWithInstances.map { (collectionId, instanceId) ->
|
||||
val instanceKey = collectionId to instanceId
|
||||
|
||||
val metadata = instancesMetadatas[instanceKey] ?: classMetadatas[collectionId]
|
||||
|
||||
NftLocal(
|
||||
identifier = identifier(chain.id, collectionId, instanceId),
|
||||
metaId = metaAccount.id,
|
||||
chainId = chain.id,
|
||||
collectionId = collectionId.toString(),
|
||||
instanceId = instanceId.toString(),
|
||||
metadata = metadata,
|
||||
type = NftLocal.Type.UNIQUES,
|
||||
issuanceTotal = totalIssuances.getValue(collectionId),
|
||||
issuanceMyEdition = instanceId.toString(),
|
||||
issuanceType = NftLocal.IssuanceType.LIMITED,
|
||||
price = null,
|
||||
|
||||
// to load at full sync
|
||||
name = null,
|
||||
label = null,
|
||||
media = null,
|
||||
|
||||
wholeDetailsLoaded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
nftDao.insertNftsDiff(NftLocal.Type.UNIQUES, chain.id, metaAccount.id, newNfts, forceOverwrite)
|
||||
}
|
||||
|
||||
override suspend fun nftFullSync(nft: Nft) {
|
||||
if (nft.metadataRaw == null) {
|
||||
nftDao.markFullSynced(nft.identifier)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val metadataLink = nft.metadataRaw!!.decodeToString().adoptFileStorageLinkToHttps()
|
||||
val metadata = ipfsApi.getIpfsMetadata(metadataLink)
|
||||
|
||||
nftDao.updateNft(nft.identifier) { local ->
|
||||
local.copy(
|
||||
name = metadata.name!!,
|
||||
media = metadata.image?.adoptFileStorageLinkToHttps(),
|
||||
label = metadata.description,
|
||||
wholeDetailsLoaded = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nftDetailsFlow(nftIdentifier: String): Flow<NftDetails> {
|
||||
return flowOf {
|
||||
val nftLocal = nftDao.getNft(nftIdentifier)
|
||||
require(nftLocal.wholeDetailsLoaded) {
|
||||
"Cannot load details of non fully-synced NFT"
|
||||
}
|
||||
|
||||
val chain = chainRegistry.getChain(nftLocal.chainId)
|
||||
val metaAccount = accountRepository.getMetaAccount(nftLocal.metaId)
|
||||
|
||||
val classId = nftLocal.collectionId.toBigInteger()
|
||||
|
||||
remoteStorage.query(chain.id) {
|
||||
var classMetadataDescriptor: MultiQueryBuilder.Descriptor<*, ByteArray?>
|
||||
var classDescriptor: MultiQueryBuilder.Descriptor<*, AccountId>
|
||||
|
||||
val queryResults = multi {
|
||||
classMetadataDescriptor = runtime.metadata.uniques().storage("ClassMetadataOf").queryKey(classId, binding = ::bindMetadata)
|
||||
classDescriptor = runtime.metadata.uniques().storage("Class").queryKey(classId, binding = ::bindIssuer)
|
||||
}
|
||||
|
||||
val classMetadataPointer = queryResults.singleValueOf(classMetadataDescriptor)
|
||||
|
||||
val collection = if (classMetadataPointer == null) {
|
||||
NftDetails.Collection(nftLocal.collectionId)
|
||||
} else {
|
||||
val url = classMetadataPointer.decodeToString().adoptFileStorageLinkToHttps()
|
||||
val classMetadata = ipfsApi.getIpfsMetadata(url)
|
||||
|
||||
NftDetails.Collection(
|
||||
id = nftLocal.collectionId,
|
||||
name = classMetadata.name,
|
||||
media = classMetadata.image?.adoptFileStorageLinkToHttps()
|
||||
)
|
||||
}
|
||||
|
||||
val classIssuer = queryResults.singleValueOf(classDescriptor)
|
||||
|
||||
NftDetails(
|
||||
identifier = nftLocal.identifier,
|
||||
chain = chain,
|
||||
owner = metaAccount.requireAccountIdIn(chain),
|
||||
creator = classIssuer,
|
||||
media = nftLocal.media,
|
||||
name = nftLocal.name ?: nftLocal.instanceId!!,
|
||||
description = nftLocal.label,
|
||||
issuance = nftIssuance(nftLocal),
|
||||
price = nftPrice(nftLocal),
|
||||
collection = collection
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindIssuer(dynamic: Any?): AccountId = bindAccountId(dynamic.castToStruct()["issuer"])
|
||||
|
||||
private fun bindMetadata(dynamic: Any?): ByteArray? = dynamic?.cast<Struct.Instance>()?.getTyped("data")
|
||||
|
||||
private fun identifier(chainId: ChainId, collectionId: BigInteger, instanceId: BigInteger): String {
|
||||
return "$chainId-$collectionId-$instanceId"
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.network
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface IpfsApi {
|
||||
|
||||
@GET
|
||||
suspend fun getIpfsMetadata(@Url url: String): UniquesMetadata
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.network
|
||||
|
||||
class UniquesMetadata(
|
||||
val name: String?,
|
||||
val image: String?,
|
||||
val description: String?
|
||||
)
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import io.novafoundation.nova.common.di.CommonApi
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_impl.NftRouter
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.details.di.NftDetailsComponent
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.list.di.NftListComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
NftFeatureDependencies::class
|
||||
],
|
||||
modules = [
|
||||
NftFeatureModule::class
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface NftFeatureComponent : NftFeatureApi {
|
||||
|
||||
fun nftListComponentFactory(): NftListComponent.Factory
|
||||
|
||||
fun nftDetailsComponentFactory(): NftDetailsComponent.Factory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance router: NftRouter,
|
||||
deps: NftFeatureDependencies
|
||||
): NftFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
DbApi::class,
|
||||
AccountFeatureApi::class,
|
||||
WalletFeatureApi::class,
|
||||
RuntimeApi::class
|
||||
]
|
||||
)
|
||||
interface NftFeatureDependenciesComponent : NftFeatureDependencies
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di
|
||||
|
||||
import coil.ImageLoader
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import javax.inject.Named
|
||||
|
||||
interface NftFeatureDependencies {
|
||||
|
||||
val amountFormatter: AmountFormatter
|
||||
|
||||
fun accountRepository(): AccountRepository
|
||||
|
||||
fun resourceManager(): ResourceManager
|
||||
|
||||
fun selectedAccountUseCase(): SelectedAccountUseCase
|
||||
|
||||
fun addressIconGenerator(): AddressIconGenerator
|
||||
|
||||
fun gson(): Gson
|
||||
|
||||
fun chainRegistry(): ChainRegistry
|
||||
|
||||
fun imageLoader(): ImageLoader
|
||||
|
||||
fun externalAccountActions(): ExternalActions.Presentation
|
||||
|
||||
fun addressDisplayUseCase(): AddressDisplayUseCase
|
||||
|
||||
fun feeLoaderMixinFactory(): FeeLoaderMixin.Factory
|
||||
|
||||
fun extrinsicService(): ExtrinsicService
|
||||
|
||||
fun tokenRepository(): TokenRepository
|
||||
|
||||
fun apiCreator(): NetworkApiCreator
|
||||
|
||||
fun nftDao(): NftDao
|
||||
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
fun remoteStorageSource(): StorageDataSource
|
||||
|
||||
fun exceptionHandler(): HttpExceptionHandler
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureApiHolder
|
||||
import io.novafoundation.nova.common.di.FeatureContainer
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_impl.NftRouter
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class NftFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val router: NftRouter,
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val dApp = DaggerNftFeatureComponent_NftFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.dbApi(getFeature(DbApi::class.java))
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerNftFeatureComponent.factory()
|
||||
.create(router, dApp)
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.data.repository.NftRepositoryImpl
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.JobOrchestrator
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.NftProvidersRegistry
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.KodadotProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.Pdc20Provider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.RmrkV1NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.RmrkV2NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.UniqueNetworkNftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.UniquesNftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.di.modules.KodadotModule
|
||||
import io.novafoundation.nova.feature_nft_impl.di.modules.Pdc20Module
|
||||
import io.novafoundation.nova.feature_nft_impl.di.modules.RmrkV1Module
|
||||
import io.novafoundation.nova.feature_nft_impl.di.modules.RmrkV2Module
|
||||
import io.novafoundation.nova.feature_nft_impl.di.modules.UniquesModule
|
||||
import io.novafoundation.nova.feature_nft_impl.di.modules.UniqueNetworkModule
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(
|
||||
includes = [
|
||||
UniquesModule::class,
|
||||
RmrkV1Module::class,
|
||||
RmrkV2Module::class,
|
||||
Pdc20Module::class,
|
||||
KodadotModule::class,
|
||||
UniqueNetworkModule::class
|
||||
]
|
||||
)
|
||||
class NftFeatureModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideJobOrchestrator() = JobOrchestrator()
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNftProviderRegistry(
|
||||
uniquesNftProvider: UniquesNftProvider,
|
||||
rmrkV1NftProvider: RmrkV1NftProvider,
|
||||
rmrkV2NftProvider: RmrkV2NftProvider,
|
||||
pdc20Provider: Pdc20Provider,
|
||||
kodadotProvider: KodadotProvider,
|
||||
uniqueNetworkProvider: UniqueNetworkNftProvider
|
||||
) = NftProvidersRegistry(uniquesNftProvider, rmrkV1NftProvider, rmrkV2NftProvider, pdc20Provider, kodadotProvider, uniqueNetworkProvider)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNftRepository(
|
||||
nftProvidersRegistry: NftProvidersRegistry,
|
||||
chainRegistry: ChainRegistry,
|
||||
jobOrchestrator: JobOrchestrator,
|
||||
nftDao: NftDao,
|
||||
httpExceptionHandler: HttpExceptionHandler,
|
||||
): NftRepository = NftRepositoryImpl(
|
||||
nftProvidersRegistry = nftProvidersRegistry,
|
||||
chainRegistry = chainRegistry,
|
||||
jobOrchestrator = jobOrchestrator,
|
||||
nftDao = nftDao,
|
||||
exceptionHandler = httpExceptionHandler
|
||||
)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.KodadotProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.kodadot.network.KodadotApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class KodadotModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideApi(networkApiCreator: NetworkApiCreator): KodadotApi {
|
||||
return networkApiCreator.create(KodadotApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideKodadotProvider(
|
||||
api: KodadotApi,
|
||||
nftDao: NftDao,
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
) = KodadotProvider(
|
||||
api = api,
|
||||
nftDao = nftDao,
|
||||
accountRepository = accountRepository,
|
||||
chainRegistry = chainRegistry
|
||||
)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.Pdc20Provider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Api
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class Pdc20Module {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideApi(networkApiCreator: NetworkApiCreator): Pdc20Api {
|
||||
return networkApiCreator.create(Pdc20Api::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNftProvider(
|
||||
api: Pdc20Api,
|
||||
nftDao: NftDao,
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
) = Pdc20Provider(
|
||||
api = api,
|
||||
nftDao = nftDao,
|
||||
accountRepository = accountRepository,
|
||||
chainRegistry = chainRegistry
|
||||
)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.RmrkV1NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.network.RmrkV1Api
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class RmrkV1Module {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideApi(networkApiCreator: NetworkApiCreator): RmrkV1Api {
|
||||
return networkApiCreator.create(RmrkV1Api::class.java, RmrkV1Api.BASE_URL)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNftProvider(
|
||||
chainRegistry: ChainRegistry,
|
||||
accountRepository: AccountRepository,
|
||||
api: RmrkV1Api,
|
||||
nftDao: NftDao
|
||||
) = RmrkV1NftProvider(chainRegistry, accountRepository, api, nftDao)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.RmrkV2NftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.network.singular.SingularV2Api
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class RmrkV2Module {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSingularApi(networkApiCreator: NetworkApiCreator): SingularV2Api {
|
||||
return networkApiCreator.create(SingularV2Api::class.java, SingularV2Api.BASE_URL)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNftProvider(
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
singularV2Api: SingularV2Api,
|
||||
nftDao: NftDao
|
||||
) = RmrkV2NftProvider(chainRegistry, accountRepository, singularV2Api, nftDao)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.UniqueNetworkNftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.unique_network.network.UniqueNetworkApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class UniqueNetworkModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideApi(networkApiCreator: NetworkApiCreator): UniqueNetworkApi {
|
||||
return networkApiCreator.create(UniqueNetworkApi::class.java, UniqueNetworkApi.BASE_URL)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideUniqueNetworkNftProvider(
|
||||
uniqueNetworkApi: UniqueNetworkApi,
|
||||
nftDao: NftDao,
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
) = UniqueNetworkNftProvider(
|
||||
uniqueNetworkApi = uniqueNetworkApi,
|
||||
nftDao = nftDao,
|
||||
accountRepository = accountRepository,
|
||||
chainRegistry = chainRegistry
|
||||
)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.NftDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.UniquesNftProvider
|
||||
import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.network.IpfsApi
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
class UniquesModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideIpfsApi(networkApiCreator: NetworkApiCreator) = networkApiCreator.create(IpfsApi::class.java)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideUniquesNftProvider(
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
@Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource,
|
||||
nftDao: NftDao,
|
||||
ipfsApi: IpfsApi,
|
||||
) = UniquesNftProvider(remoteStorageSource, accountRepository, chainRegistry, nftDao, ipfsApi)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.domain.nft.details
|
||||
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class NftDetailsInteractor(
|
||||
private val nftRepository: NftRepository,
|
||||
private val tokenRepository: TokenRepository
|
||||
) {
|
||||
|
||||
fun nftDetailsFlow(nftIdentifier: String): Flow<PricedNftDetails> {
|
||||
return nftRepository.nftDetails(nftIdentifier).flatMapLatest { nftDetails ->
|
||||
tokenRepository.observeToken(nftDetails.chain.utilityAsset).map { token ->
|
||||
PricedNftDetails(
|
||||
nftDetails = nftDetails,
|
||||
priceToken = token
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.domain.nft.details
|
||||
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.NftDetails
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
|
||||
class PricedNftDetails(
|
||||
val nftDetails: NftDetails,
|
||||
val priceToken: Token
|
||||
)
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.domain.nft.list
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class NftListInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val nftRepository: NftRepository,
|
||||
) {
|
||||
|
||||
fun userNftsFlow(): Flow<List<PricedNft>> {
|
||||
return accountRepository.selectedMetaAccountFlow()
|
||||
.flatMapLatest(nftRepository::allNftFlow)
|
||||
.map { nfts -> nfts.sortedBy { it.identifier } }
|
||||
.flatMapLatest { nfts ->
|
||||
val allUtilityAssets = nfts.map { it.chain.utilityAsset }.distinct()
|
||||
|
||||
tokenRepository.observeTokens(allUtilityAssets).mapLatest { tokensByUtilityAsset ->
|
||||
nfts.map { nft ->
|
||||
PricedNft(
|
||||
nft = nft,
|
||||
nftPriceToken = tokensByUtilityAsset[nft.chain.utilityAsset.fullId]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun syncNftsList() = withContext(Dispatchers.Default) {
|
||||
nftRepository.initialNftSync(accountRepository.getSelectedMetaAccount(), forceOverwrite = true)
|
||||
}
|
||||
|
||||
suspend fun fullSyncNft(nft: Nft) {
|
||||
nftRepository.fullNftSync(nft)
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.domain.nft.list
|
||||
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
|
||||
class PricedNft(
|
||||
val nft: Nft,
|
||||
val nftPriceToken: Token?
|
||||
)
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.common
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.formatting.format
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_impl.R
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
|
||||
fun ResourceManager.formatIssuance(issuance: Nft.Issuance): String {
|
||||
return when (issuance) {
|
||||
is Nft.Issuance.Unlimited -> getString(R.string.nft_issuance_unlimited)
|
||||
|
||||
is Nft.Issuance.Limited -> {
|
||||
getString(
|
||||
R.string.nft_issuance_limited_format,
|
||||
issuance.edition.format(),
|
||||
issuance.max.format()
|
||||
)
|
||||
}
|
||||
|
||||
is Nft.Issuance.Fungible -> {
|
||||
getString(
|
||||
R.string.nft_issuance_fungible_format,
|
||||
issuance.myAmount.format(),
|
||||
issuance.totalSupply.format()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ResourceManager.formatNftPrice(amountFormatter: AmountFormatter, price: Nft.Price?, priceToken: Token?): NftPriceModel? {
|
||||
if (price == null || priceToken == null) return null
|
||||
|
||||
return when (price) {
|
||||
is Nft.Price.Fungible -> {
|
||||
val units = price.units.format()
|
||||
val amountModel = amountFormatter.formatAmountToAmountModel(price.totalPrice, priceToken)
|
||||
|
||||
NftPriceModel(
|
||||
amountInfo = getString(R.string.nft_fungile_price, units, amountModel.token),
|
||||
fiat = amountModel.fiat
|
||||
)
|
||||
}
|
||||
is Nft.Price.NonFungible -> {
|
||||
val amountModel = amountFormatter.formatAmountToAmountModel(price.nftPrice, priceToken)
|
||||
|
||||
NftPriceModel(
|
||||
amountInfo = amountModel.token,
|
||||
fiat = amountModel.fiat
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.common
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import io.novafoundation.nova.common.utils.WithContextExtensions
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.utils.updatePadding
|
||||
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
|
||||
import io.novafoundation.nova.feature_nft_impl.R
|
||||
|
||||
class NftIssuanceView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : AppCompatTextView(context, attrs, defStyleAttr), WithContextExtensions {
|
||||
|
||||
override val providedContext: Context = context
|
||||
|
||||
init {
|
||||
setTextAppearance(R.style.TextAppearance_NovaFoundation_SemiBold_Caps2)
|
||||
setTextColorRes(R.color.chip_text)
|
||||
updatePadding(top = 1.5f.dp, bottom = 1.5f.dp, start = 6.dp, end = 6.dp)
|
||||
background = context.getRoundedCornerDrawable(R.color.chips_background, cornerSizeInDp = 4)
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model
|
||||
|
||||
class NftPriceModel(
|
||||
val amountInfo: CharSequence,
|
||||
val fiat: CharSequence?
|
||||
)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.details
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
import coil.ImageLoader
|
||||
import coil.load
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.letOrHide
|
||||
import io.novafoundation.nova.common.utils.makeGone
|
||||
import io.novafoundation.nova.common.utils.makeVisible
|
||||
import io.novafoundation.nova.common.utils.setTextOrHide
|
||||
import io.novafoundation.nova.common.view.dialog.errorDialog
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.view.showAddress
|
||||
import io.novafoundation.nova.feature_account_api.view.showChain
|
||||
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_impl.R
|
||||
import io.novafoundation.nova.feature_nft_impl.databinding.FragmentNftDetailsBinding
|
||||
import io.novafoundation.nova.feature_nft_impl.di.NftFeatureComponent
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.PriceSectionView
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class NftDetailsFragment : BaseFragment<NftDetailsViewModel, FragmentNftDetailsBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PAYLOAD = "NftDetailsFragment.PAYLOAD"
|
||||
|
||||
fun getBundle(nftId: String) = bundleOf(PAYLOAD to nftId)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentNftDetailsBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val contentViews by lazy(LazyThreadSafetyMode.NONE) {
|
||||
listOf(
|
||||
binder.nftDetailsMedia,
|
||||
binder.nftDetailsTitle,
|
||||
binder.nftDetailsDescription,
|
||||
binder.nftDetailsIssuance,
|
||||
binder.nftDetailsPrice,
|
||||
binder.nftDetailsTable
|
||||
)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.nftDetailsToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
binder.nftDetailsOnwer.setOnClickListener { viewModel.ownerClicked() }
|
||||
binder.nftDetailsCreator.setOnClickListener { viewModel.creatorClicked() }
|
||||
|
||||
binder.nftDetailsCollection.valuePrimary.ellipsize = TextUtils.TruncateAt.END
|
||||
|
||||
binder.nftDetailsProgress.makeVisible()
|
||||
contentViews.forEach(View::makeGone)
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<NftFeatureComponent>(this, NftFeatureApi::class.java)
|
||||
.nftDetailsComponentFactory()
|
||||
.create(this, argument(PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: NftDetailsViewModel) {
|
||||
setupExternalActions(viewModel)
|
||||
|
||||
viewModel.nftDetailsUi.observe {
|
||||
binder.nftDetailsProgress.makeGone()
|
||||
contentViews.forEach(View::makeVisible)
|
||||
|
||||
binder.nftDetailsMedia.load(it.media, imageLoader) {
|
||||
placeholder(R.drawable.nft_media_progress)
|
||||
error(R.drawable.nft_media_progress)
|
||||
}
|
||||
binder.nftDetailsTitle.text = it.name
|
||||
binder.nftDetailsDescription.setTextOrHide(it.description)
|
||||
binder.nftDetailsIssuance.text = it.issuance
|
||||
|
||||
binder.nftDetailsPrice.setPriceOrHide(it.price)
|
||||
|
||||
if (it.collection != null) {
|
||||
binder.nftDetailsCollection.makeVisible()
|
||||
binder.nftDetailsCollection.loadImage(it.collection.media)
|
||||
binder.nftDetailsCollection.showValue(it.collection.name)
|
||||
} else {
|
||||
binder.nftDetailsCollection.makeGone()
|
||||
}
|
||||
|
||||
binder.nftDetailsOnwer.showAddress(it.owner)
|
||||
|
||||
if (it.creator != null) {
|
||||
binder.nftDetailsCreator.makeVisible()
|
||||
binder.nftDetailsCreator.showAddress(it.creator)
|
||||
} else {
|
||||
binder.nftDetailsCreator.makeGone()
|
||||
}
|
||||
|
||||
binder.nftDetailsChain.showChain(it.network)
|
||||
}
|
||||
|
||||
viewModel.exitingErrorLiveData.observeEvent {
|
||||
errorDialog(requireContext(), onConfirm = viewModel::backClicked) {
|
||||
setMessage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PriceSectionView.setPriceOrHide(maybePrice: NftPriceModel?) = letOrHide(maybePrice) { price ->
|
||||
setPrice(price.amountInfo, price.fiat)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.details
|
||||
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel
|
||||
|
||||
class NftDetailsModel(
|
||||
val media: String?,
|
||||
val name: String,
|
||||
val issuance: String,
|
||||
val description: String?,
|
||||
val price: NftPriceModel?,
|
||||
val collection: Collection?,
|
||||
val owner: AddressModel,
|
||||
val creator: AddressModel?,
|
||||
val network: ChainUi
|
||||
) {
|
||||
|
||||
class Collection(
|
||||
val name: String,
|
||||
val media: String?
|
||||
)
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.details
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.event
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAddressModel
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
|
||||
import io.novafoundation.nova.feature_nft_impl.NftRouter
|
||||
import io.novafoundation.nova.feature_nft_impl.domain.nft.details.NftDetailsInteractor
|
||||
import io.novafoundation.nova.feature_nft_impl.domain.nft.details.PricedNftDetails
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatNftPrice
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NftDetailsViewModel(
|
||||
private val router: NftRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val interactor: NftDetailsInteractor,
|
||||
private val nftIdentifier: String,
|
||||
private val externalActionsDelegate: ExternalActions.Presentation,
|
||||
private val addressIconGenerator: AddressIconGenerator,
|
||||
private val addressDisplayUseCase: AddressDisplayUseCase,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) : BaseViewModel(), ExternalActions by externalActionsDelegate {
|
||||
|
||||
private val _exitingErrorLiveData = MutableLiveData<Event<String>>()
|
||||
val exitingErrorLiveData: LiveData<Event<String>> = _exitingErrorLiveData
|
||||
|
||||
private val nftDetailsFlow = interactor.nftDetailsFlow(nftIdentifier)
|
||||
.inBackground()
|
||||
.catch { showExitingError(it) }
|
||||
.share()
|
||||
|
||||
val nftDetailsUi = nftDetailsFlow
|
||||
.map(::mapNftDetailsToUi)
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
fun ownerClicked() = launch {
|
||||
val pricedNftDetails = nftDetailsFlow.first()
|
||||
|
||||
with(pricedNftDetails.nftDetails) {
|
||||
externalActionsDelegate.showAddressActions(owner, chain)
|
||||
}
|
||||
}
|
||||
|
||||
fun creatorClicked() = launch {
|
||||
val pricedNftDetails = nftDetailsFlow.first()
|
||||
|
||||
with(pricedNftDetails.nftDetails) {
|
||||
externalActionsDelegate.showAddressActions(creator!!, chain)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showExitingError(exception: Throwable) {
|
||||
_exitingErrorLiveData.value = exception.message.orEmpty().event()
|
||||
}
|
||||
|
||||
private suspend fun mapNftDetailsToUi(pricedNftDetails: PricedNftDetails): NftDetailsModel {
|
||||
val nftDetails = pricedNftDetails.nftDetails
|
||||
|
||||
return NftDetailsModel(
|
||||
media = nftDetails.media,
|
||||
name = nftDetails.name,
|
||||
issuance = resourceManager.formatIssuance(nftDetails.issuance),
|
||||
description = nftDetails.description,
|
||||
price = resourceManager.formatNftPrice(amountFormatter, pricedNftDetails.nftDetails.price, pricedNftDetails.priceToken),
|
||||
collection = nftDetails.collection?.let {
|
||||
NftDetailsModel.Collection(
|
||||
name = it.name ?: it.id,
|
||||
media = it.media,
|
||||
)
|
||||
},
|
||||
owner = createAddressModel(nftDetails.owner, nftDetails.chain),
|
||||
creator = nftDetails.creator?.let {
|
||||
createAddressModel(it, nftDetails.chain)
|
||||
},
|
||||
network = mapChainToUi(nftDetails.chain)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createAddressModel(accountId: AccountId, chain: Chain) = addressIconGenerator.createAddressModel(
|
||||
chain = chain,
|
||||
accountId = accountId,
|
||||
sizeInDp = AddressIconGenerator.SIZE_MEDIUM,
|
||||
addressDisplayUseCase = addressDisplayUseCase,
|
||||
background = AddressIconGenerator.BACKGROUND_TRANSPARENT
|
||||
)
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.details.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.NftRouter
|
||||
import io.novafoundation.nova.feature_nft_impl.domain.nft.details.NftDetailsInteractor
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.details.NftDetailsViewModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class NfDetailsModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
nftRepository: NftRepository,
|
||||
tokenRepository: TokenRepository
|
||||
) = NftDetailsInteractor(
|
||||
tokenRepository = tokenRepository,
|
||||
nftRepository = nftRepository
|
||||
)
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): NftDetailsViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(NftDetailsViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(NftDetailsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: NftRouter,
|
||||
resourceManager: ResourceManager,
|
||||
interactor: NftDetailsInteractor,
|
||||
nftIdentifier: String,
|
||||
accountExternalActions: ExternalActions.Presentation,
|
||||
addressIconGenerator: AddressIconGenerator,
|
||||
addressDisplayUseCase: AddressDisplayUseCase,
|
||||
amountFormatter: AmountFormatter
|
||||
): ViewModel {
|
||||
return NftDetailsViewModel(
|
||||
router = router,
|
||||
resourceManager = resourceManager,
|
||||
interactor = interactor,
|
||||
nftIdentifier = nftIdentifier,
|
||||
externalActionsDelegate = accountExternalActions,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
addressDisplayUseCase = addressDisplayUseCase,
|
||||
amountFormatter = amountFormatter
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.details.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.details.NftDetailsFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
NfDetailsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface NftDetailsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance nftId: String
|
||||
): NftDetailsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: NftDetailsFragment)
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import coil.clear
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import io.novafoundation.nova.common.presentation.LoadingState
|
||||
import io.novafoundation.nova.common.utils.dpF
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.makeGone
|
||||
import io.novafoundation.nova.common.utils.makeVisible
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.common.view.shape.addRipple
|
||||
import io.novafoundation.nova.common.view.shape.getRippleMask
|
||||
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
|
||||
import io.novafoundation.nova.feature_nft_impl.R
|
||||
import io.novafoundation.nova.feature_nft_impl.databinding.ItemNftBinding
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
|
||||
class NftAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val handler: Handler
|
||||
) : ListAdapter<NftListItem, NftHolder>(DiffCallback) {
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun itemClicked(item: NftListItem)
|
||||
|
||||
fun loadableItemShown(item: NftListItem)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NftHolder {
|
||||
return NftHolder(ItemNftBinding.inflate(parent.inflater(), parent, false), imageLoader, handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NftHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: NftHolder) {
|
||||
holder.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<NftListItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: NftListItem, newItem: NftListItem): Boolean {
|
||||
return oldItem.identifier == newItem.identifier
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: NftListItem, newItem: NftListItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class NftHolder(
|
||||
private val binder: ItemNftBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: NftAdapter.Handler
|
||||
) : RecyclerView.ViewHolder(binder.root), LayoutContainer {
|
||||
|
||||
override val containerView = binder.root
|
||||
|
||||
init {
|
||||
with(containerView) {
|
||||
binder.itemNftContent.background = with(context) {
|
||||
addRipple(getRoundedCornerDrawable(R.color.block_background, cornerSizeInDp = 12), mask = getRippleMask(cornerSizeDp = 12))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
binder.itemNftMedia.clear()
|
||||
}
|
||||
|
||||
fun bind(item: NftListItem) = with(binder) {
|
||||
when (val content = item.content) {
|
||||
is LoadingState.Loading -> {
|
||||
itemNftShimmer.makeVisible()
|
||||
itemNftShimmer.startShimmer()
|
||||
itemNftContent.makeGone()
|
||||
|
||||
itemHandler.loadableItemShown(item)
|
||||
}
|
||||
is LoadingState.Loaded -> {
|
||||
itemNftShimmer.makeGone()
|
||||
itemNftShimmer.stopShimmer()
|
||||
itemNftContent.makeVisible()
|
||||
|
||||
itemNftMedia.load(content.data.media, imageLoader) {
|
||||
transformations(RoundedCornersTransformation(8.dpF(containerView.context)))
|
||||
placeholder(R.drawable.nft_media_progress)
|
||||
error(R.drawable.nft_media_error)
|
||||
fallback(R.drawable.nft_media_error)
|
||||
listener(
|
||||
onError = { _, _ ->
|
||||
// so that placeholder would be able to change aspect ratio and fill ImageView entirely
|
||||
itemNftMedia.scaleType = ImageView.ScaleType.FIT_XY
|
||||
},
|
||||
onSuccess = { _, _ ->
|
||||
// set default scale type back
|
||||
itemNftMedia.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
itemNftIssuance.text = content.data.issuance
|
||||
itemNftTitle.text = content.data.title
|
||||
|
||||
setPrice(content)
|
||||
}
|
||||
}
|
||||
|
||||
containerView.setOnClickListener { itemHandler.itemClicked(item) }
|
||||
}
|
||||
|
||||
private fun setPrice(content: LoadingState.Loaded<NftListItem.Content>) {
|
||||
val price = content.data.price
|
||||
|
||||
binder.itemNftPriceToken.setVisible(price != null)
|
||||
binder.itemNftPriceFiat.setVisible(price != null)
|
||||
binder.itemNftPricePlaceholder.setVisible(price == null)
|
||||
|
||||
if (price != null) {
|
||||
binder.itemNftPriceToken.text = price.amountInfo
|
||||
binder.itemNftPriceFiat.text = price.fiat
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.list
|
||||
|
||||
import android.view.View
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.insets.applyNavigationBarInsets
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.submitListPreservingViewPoint
|
||||
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_impl.databinding.FragmentNftListBinding
|
||||
import io.novafoundation.nova.feature_nft_impl.di.NftFeatureComponent
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class NftListFragment : BaseFragment<NftListViewModel, FragmentNftListBinding>(), NftAdapter.Handler {
|
||||
|
||||
override fun createBinding() = FragmentNftListBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) { NftAdapter(imageLoader, this) }
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.nftListToolbar.applyStatusBarInsets()
|
||||
binder.nftListNfts.applyNavigationBarInsets()
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.nftListBack.setOnClickListener { viewModel.backClicked() }
|
||||
|
||||
binder.nftListNfts.adapter = adapter
|
||||
binder.nftListNfts.itemAnimator = null
|
||||
|
||||
binder.nftListRefresh.setOnRefreshListener { viewModel.syncNfts() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<NftFeatureComponent>(this, NftFeatureApi::class.java)
|
||||
.nftListComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: NftListViewModel) {
|
||||
viewModel.nftListItemsFlow.observe {
|
||||
adapter.submitListPreservingViewPoint(it, binder.nftListNfts)
|
||||
}
|
||||
|
||||
viewModel.hideRefreshEvent.observeEvent {
|
||||
binder.nftListRefresh.isRefreshing = false
|
||||
}
|
||||
|
||||
viewModel.nftCountFlow.observe(binder.nftListCounter::setText)
|
||||
}
|
||||
|
||||
override fun itemClicked(item: NftListItem) {
|
||||
viewModel.nftClicked(item)
|
||||
}
|
||||
|
||||
override fun loadableItemShown(item: NftListItem) {
|
||||
viewModel.loadableNftShown(item)
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.list
|
||||
|
||||
import io.novafoundation.nova.common.presentation.LoadingState
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel
|
||||
|
||||
data class NftListItem(
|
||||
val content: LoadingState<Content>,
|
||||
val identifier: String,
|
||||
) {
|
||||
|
||||
data class Content(
|
||||
val issuance: String,
|
||||
val title: String,
|
||||
val price: NftPriceModel?,
|
||||
val media: String?,
|
||||
)
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.list
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.presentation.LoadingState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.formatting.format
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_nft_impl.NftRouter
|
||||
import io.novafoundation.nova.feature_nft_impl.domain.nft.list.NftListInteractor
|
||||
import io.novafoundation.nova.feature_nft_impl.domain.nft.list.PricedNft
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatIssuance
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatNftPrice
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NftListViewModel(
|
||||
private val router: NftRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val interactor: NftListInteractor,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val nftsFlow = interactor.userNftsFlow()
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val nftCountFlow = nftsFlow.map { it.size.format() }
|
||||
.share()
|
||||
|
||||
val nftListItemsFlow = nftsFlow.mapList(::mapNftToListItem)
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
private val _hideRefreshEvent = MutableLiveData<Event<Unit>>()
|
||||
val hideRefreshEvent: LiveData<Event<Unit>> = _hideRefreshEvent
|
||||
|
||||
fun syncNfts() {
|
||||
viewModelScope.launch {
|
||||
interactor.syncNftsList()
|
||||
|
||||
_hideRefreshEvent.value = Event(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun nftClicked(nftListItem: NftListItem) = launch {
|
||||
if (nftListItem.content is LoadingState.Loaded) {
|
||||
router.openNftDetails(nftListItem.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadableNftShown(nftListItem: NftListItem) = launch(Dispatchers.Default) {
|
||||
val pricedNft = nftsFlow.first().firstOrNull { it.nft.identifier == nftListItem.identifier }
|
||||
?: return@launch
|
||||
|
||||
interactor.fullSyncNft(pricedNft.nft)
|
||||
}
|
||||
|
||||
private fun mapNftToListItem(pricedNft: PricedNft): NftListItem {
|
||||
val content = when (val details = pricedNft.nft.details) {
|
||||
Nft.Details.Loadable -> LoadingState.Loading()
|
||||
|
||||
is Nft.Details.Loaded -> {
|
||||
val issuanceFormatted = resourceManager.formatIssuance(details.issuance)
|
||||
|
||||
val price = resourceManager.formatNftPrice(amountFormatter, details.price, pricedNft.nftPriceToken)
|
||||
|
||||
LoadingState.Loaded(
|
||||
NftListItem.Content(
|
||||
issuance = issuanceFormatted,
|
||||
title = details.name ?: pricedNft.nft.instanceId ?: pricedNft.nft.collectionId,
|
||||
price = price,
|
||||
media = details.media
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return NftListItem(
|
||||
identifier = pricedNft.nft.identifier,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.list.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.list.NftListFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
NftListModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface NftListComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): NftListComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: NftListFragment)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.presentation.nft.list.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_nft_impl.NftRouter
|
||||
import io.novafoundation.nova.feature_nft_impl.domain.nft.list.NftListInteractor
|
||||
import io.novafoundation.nova.feature_nft_impl.presentation.nft.list.NftListViewModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class NftListModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
accountRepository: AccountRepository,
|
||||
nftRepository: NftRepository,
|
||||
tokenRepository: TokenRepository
|
||||
) = NftListInteractor(
|
||||
accountRepository = accountRepository,
|
||||
tokenRepository = tokenRepository,
|
||||
nftRepository = nftRepository
|
||||
)
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): NftListViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(NftListViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(NftListViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: NftRouter,
|
||||
resourceManager: ResourceManager,
|
||||
interactor: NftListInteractor,
|
||||
amountFormatter: AmountFormatter
|
||||
): ViewModel {
|
||||
return NftListViewModel(
|
||||
router = router,
|
||||
resourceManager = resourceManager,
|
||||
interactor = interactor,
|
||||
amountFormatter = amountFormatter
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="150dp"
|
||||
android:height="150dp"
|
||||
android:viewportWidth="154"
|
||||
android:viewportHeight="154">
|
||||
<path
|
||||
android:fillColor="#7A08090E"
|
||||
android:pathData="M8,0L146,0A8,8 0,0 1,154 8L154,146A8,8 0,0 1,146 154L8,154A8,8 0,0 1,0 146L0,8A8,8 0,0 1,8 0z" />
|
||||
<path
|
||||
android:fillColor="#29FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M64.75,69C64.75,67.757 65.757,66.75 67,66.75H87C88.243,66.75 89.25,67.757 89.25,69V82.398L85.883,79.591L85.883,79.59C85.355,79.151 84.682,78.926 83.997,78.957C83.312,78.989 82.663,79.276 82.178,79.762L82.178,79.762L80.501,81.438L74.864,75.801L74.864,75.801C74.368,75.306 73.702,75.018 73.001,74.997C72.3,74.977 71.618,75.224 71.094,75.69L71.093,75.69L64.75,81.33V69ZM79.44,82.499L73.803,76.862L73.803,76.862C73.578,76.637 73.275,76.506 72.957,76.497C72.638,76.487 72.328,76.6 72.09,76.811L72.09,76.811L64.75,83.337V85C64.75,86.243 65.757,87.25 67,87.25H74.69L79.44,82.499ZM74.975,88.75C74.992,88.751 75.008,88.751 75.025,88.75H87C89.071,88.75 90.75,87.071 90.75,85V84.006C90.75,84.003 90.75,83.999 90.75,83.995V69C90.75,66.929 89.071,65.25 87,65.25H67C64.929,65.25 63.25,66.929 63.25,69V82.997V83.004V85C63.25,87.071 64.929,88.75 67,88.75H74.975ZM89.25,84.352V85C89.25,86.243 88.243,87.25 87,87.25H76.811L83.239,80.822L83.24,80.821C83.46,80.601 83.755,80.47 84.066,80.456C84.378,80.441 84.683,80.544 84.923,80.743L84.924,80.743L89.25,84.352ZM80.75,72C80.75,71.31 81.31,70.75 82,70.75C82.69,70.75 83.25,71.31 83.25,72C83.25,72.69 82.69,73.25 82,73.25C81.31,73.25 80.75,72.69 80.75,72ZM82,69.25C80.481,69.25 79.25,70.481 79.25,72C79.25,73.519 80.481,74.75 82,74.75C83.519,74.75 84.75,73.519 84.75,72C84.75,70.481 83.519,69.25 82,69.25Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<solid android:color="@color/block_background" />
|
||||
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:background="@color/secondary_screen_background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/nftDetailsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/nftDetailsProgress"
|
||||
style="@style/Widget.Nova.ProgressBar.Indeterminate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/nftDetailsMedia"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nftDetailsTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Bold.Title1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="Honeybird" />
|
||||
|
||||
<io.novafoundation.nova.feature_nft_impl.presentation.nft.common.NftIssuanceView
|
||||
android:id="@+id/nftDetailsIssuance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:text="#11 Edition of 9978" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nftDetailsDescription"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.SubHeadline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
tools:text="Wayne: Hello sir, what's your name and what brings you to Pastel Beach Mall?" />
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.PriceSectionView
|
||||
android:id="@+id/nftDetailsPrice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:id="@+id/nftDetailsTable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/nftDetailsCollection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/nft_collection" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/nftDetailsOnwer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/nft_owned_by" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/nftDetailsCreator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/nft_created_by" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/nftDetailsChain"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_network" />
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/drawable_background_image"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/nftListToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/blur_navigation_background"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/nftListBack"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
app:tint="@color/actions_color" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/nft_your_nfts"
|
||||
android:textColor="@color/text_primary" />
|
||||
|
||||
<io.novafoundation.nova.common.view.CounterView
|
||||
android:id="@+id/nftListCounter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
tools:text="6" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/nftListRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/nftListNfts"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="5dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
tools:listitem="@layout/item_nft" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/itemNftContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="11dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:shimmer_auto_start="false"
|
||||
tools:background="@color/block_background">
|
||||
|
||||
<com.facebook.shimmer.ShimmerFrameLayout
|
||||
android:id="@+id/itemNftShimmer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="262dp"
|
||||
android:background="@drawable/bg_shimmering" />
|
||||
|
||||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/itemNftContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemNftMedia"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="154dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:maxHeight="154dp"
|
||||
tools:src="@drawable/nft_media_progress" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemNftTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.SubHeadline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="Honeybird" />
|
||||
|
||||
<io.novafoundation.nova.feature_nft_impl.presentation.nft.common.NftIssuanceView
|
||||
android:id="@+id/itemNftIssuance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
tools:text="#11 Edition of 9978" />
|
||||
|
||||
<View
|
||||
android:id="@+id/itemNftPriceDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@color/divider" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/itemNftPrice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemNftPricePlaceholder"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/nft_price_not_listed"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemNftPriceToken"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/text_primary"
|
||||
tools:text="10 DOT" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemNftPriceFiat"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Caption1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text_secondary"
|
||||
tools:text="($865.19)" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Widget.Nova.NftIssuance" parent="TextAppearance.NovaFoundation.SemiBold.Caps2">
|
||||
<item name="android:paddingStart">6dp</item>
|
||||
<item name="android:paddingTop">1.5dp</item>
|
||||
<item name="android:paddingEnd">8dp</item>
|
||||
<item name="android:paddingBottom">1.5dp</item>
|
||||
<item name="android:textColor">@color/text_secondary</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_nft_impl.data.network.distributed
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class FileStorageAdapterTest {
|
||||
|
||||
|
||||
@Test
|
||||
fun `should adapt ipfs to http`() {
|
||||
runTest(
|
||||
initial = "ipfs://ipfs/bafkreig7jn6iwz4fo3mwkl3ndljbrf7ot4mwyvpmzrj4vm2nvwzw6dysb4",
|
||||
expected = "${FileStorage.IPFS.defaultHttpsGateway}bafkreig7jn6iwz4fo3mwkl3ndljbrf7ot4mwyvpmzrj4vm2nvwzw6dysb4"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fallback`() {
|
||||
runTest(
|
||||
initial = "bafkreig7jn6iwz4fo3mwkl3ndljbrf7ot4mwyvpmzrj4vm2nvwzw6dysb4",
|
||||
expected = "${FileStorage.IPFS.defaultHttpsGateway}bafkreig7jn6iwz4fo3mwkl3ndljbrf7ot4mwyvpmzrj4vm2nvwzw6dysb4"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should leave http and https as is`() {
|
||||
runTest(
|
||||
initial = "https://singular.rmrk.app/api/rmrk1/account/EkLXe943A4Ceu8rF4a2Wb8s5BHuTdTcEszJJCgsEPwAiGCi",
|
||||
expected = "https://singular.rmrk.app/api/rmrk1/account/EkLXe943A4Ceu8rF4a2Wb8s5BHuTdTcEszJJCgsEPwAiGCi"
|
||||
)
|
||||
|
||||
runTest(
|
||||
initial = "http://singular.rmrk.app/api/rmrk1/account/EkLXe943A4Ceu8rF4a2Wb8s5BHuTdTcEszJJCgsEPwAiGCi",
|
||||
expected = "http://singular.rmrk.app/api/rmrk1/account/EkLXe943A4Ceu8rF4a2Wb8s5BHuTdTcEszJJCgsEPwAiGCi"
|
||||
)
|
||||
}
|
||||
|
||||
private fun runTest(
|
||||
initial: String,
|
||||
expected: String,
|
||||
) {
|
||||
val actual = FileStorageAdapter.adaptToHttps(initial)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user