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,105 @@
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: '../tests.gradle'
|
||||
apply from: '../scripts/secrets.gradle'
|
||||
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
|
||||
|
||||
|
||||
buildConfigField "String", "PEZKUWI_CARD_WIDGET_ID", "\"4ce98182-ed76-4933-ba1b-b85e4a51d75a\""
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
namespace 'io.novafoundation.nova.feature_assets'
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes.add("META-INF/NOTICE.md")
|
||||
}
|
||||
|
||||
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(':feature-currency-api')
|
||||
implementation project(':feature-crowdloan-api')
|
||||
implementation project(':feature-wallet-connect-api')
|
||||
implementation project(':feature-staking-api')
|
||||
implementation project(':feature-swap-api')
|
||||
implementation project(':web3names')
|
||||
implementation project(':runtime')
|
||||
implementation project(':feature-buy-api')
|
||||
implementation project(':feature-xcm:api')
|
||||
implementation project(':feature-banners-api')
|
||||
implementation project(':feature-deep-linking')
|
||||
implementation project(':feature-ahm-api')
|
||||
implementation project(':feature-gift-api')
|
||||
|
||||
implementation kotlinDep
|
||||
|
||||
implementation androidDep
|
||||
implementation swipeRefershLayout
|
||||
implementation materialDep
|
||||
implementation cardViewDep
|
||||
implementation constraintDep
|
||||
|
||||
implementation permissionsDep
|
||||
|
||||
implementation coroutinesDep
|
||||
implementation coroutinesAndroidDep
|
||||
implementation viewModelKtxDep
|
||||
implementation liveDataKtxDep
|
||||
implementation lifeCycleKtxDep
|
||||
|
||||
implementation daggerDep
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
ksp daggerCompiler
|
||||
|
||||
implementation roomDep
|
||||
ksp roomCompiler
|
||||
|
||||
implementation lifecycleDep
|
||||
ksp lifecycleCompiler
|
||||
|
||||
implementation androidxWebKit
|
||||
|
||||
implementation bouncyCastleDep
|
||||
|
||||
testImplementation jUnitDep
|
||||
testImplementation mockitoDep
|
||||
|
||||
implementation substrateSdkDep
|
||||
|
||||
implementation gsonDep
|
||||
implementation retrofitDep
|
||||
|
||||
implementation wsDep
|
||||
|
||||
implementation zXingCoreDep
|
||||
implementation zXingEmbeddedDep
|
||||
|
||||
implementation insetterDep
|
||||
|
||||
implementation shimmerDep
|
||||
implementation flexBoxDep
|
||||
|
||||
implementation chartsDep
|
||||
|
||||
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>
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
package io.novafoundation.nova.feature_assets.data.mappers
|
||||
|
||||
import io.novafoundation.nova.core_db.model.AssetAndChainId
|
||||
import io.novafoundation.nova.core_db.model.operation.DirectRewardTypeJoin
|
||||
import io.novafoundation.nova.core_db.model.operation.DirectRewardTypeLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.ExtrinsicTypeJoin
|
||||
import io.novafoundation.nova.core_db.model.operation.ExtrinsicTypeLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationBaseLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationJoin
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationTypeLocal.OperationForeignKey
|
||||
import io.novafoundation.nova.core_db.model.operation.PoolRewardTypeJoin
|
||||
import io.novafoundation.nova.core_db.model.operation.PoolRewardTypeLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.RewardTypeLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.SwapTypeJoin
|
||||
import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.TransferTypeJoin
|
||||
import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.mappers.mapAssetWithAmountToLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.mappers.mapOperationStatusToOperationLocalStatus
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation.Type
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation.Type.Extrinsic.Content
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation.Type.Reward.RewardKind
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.convertPlanks
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
private fun mapOperationStatusLocalToOperationStatus(status: OperationBaseLocal.Status) = when (status) {
|
||||
OperationBaseLocal.Status.PENDING -> Operation.Status.PENDING
|
||||
OperationBaseLocal.Status.COMPLETED -> Operation.Status.COMPLETED
|
||||
OperationBaseLocal.Status.FAILED -> Operation.Status.FAILED
|
||||
}
|
||||
|
||||
fun mapOperationToOperationLocalDb(
|
||||
operation: Operation,
|
||||
source: OperationBaseLocal.Source,
|
||||
): OperationLocal = with(operation) {
|
||||
val localAssetId = AssetAndChainId(chainAsset.chainId, chainAsset.id)
|
||||
val foreignKey = OperationForeignKey(id, address, localAssetId)
|
||||
val typeLocal = when (val operationType = operation.type) {
|
||||
is Type.Extrinsic -> mapExtrinsicToLocal(operationType, foreignKey)
|
||||
is Type.Reward -> mapRewardToLocal(operationType, foreignKey)
|
||||
is Type.Swap -> mapSwapToLocal(operationType, foreignKey)
|
||||
is Type.Transfer -> mapTransferToLocal(operationType, foreignKey)
|
||||
}
|
||||
|
||||
val base = OperationBaseLocal(
|
||||
id = id,
|
||||
address = address,
|
||||
time = time,
|
||||
assetId = localAssetId,
|
||||
hash = extrinsicHash,
|
||||
status = mapOperationStatusToOperationLocalStatus(operation.status),
|
||||
source = source
|
||||
)
|
||||
|
||||
OperationLocal(
|
||||
base = base,
|
||||
type = typeLocal
|
||||
)
|
||||
}
|
||||
|
||||
fun mapOperationLocalToOperation(
|
||||
operationLocal: OperationJoin,
|
||||
chainAsset: Chain.Asset,
|
||||
chain: Chain,
|
||||
coinRate: CoinRate?,
|
||||
): Operation? = with(operationLocal) {
|
||||
val operationType = when {
|
||||
operationLocal.transfer != null -> mapTransferFromLocal(operationLocal.transfer!!, chainAsset, coinRate, operationLocal.base.address)
|
||||
operationLocal.directReward != null -> mapDirectRewardFromLocal(operationLocal.directReward!!, chainAsset, coinRate)
|
||||
operationLocal.poolReward != null -> mapPoolRewardFromLocal(operationLocal.poolReward!!, chainAsset, coinRate)
|
||||
operationLocal.extrinsic != null -> mapExtrinsicFromLocal(operationLocal.extrinsic!!, chainAsset, coinRate)
|
||||
operationLocal.swap != null -> mapSwapFromLocal(operationLocal.swap!!, chainAsset, chain, coinRate)
|
||||
else -> null
|
||||
} ?: return@with null
|
||||
|
||||
return Operation(
|
||||
id = base.id,
|
||||
address = base.address,
|
||||
type = operationType,
|
||||
time = base.time,
|
||||
chainAsset = chainAsset,
|
||||
extrinsicHash = base.hash,
|
||||
status = mapOperationStatusLocalToOperationStatus(base.status)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapExtrinsicToLocal(
|
||||
extrinsic: Type.Extrinsic,
|
||||
foreignKey: OperationForeignKey
|
||||
): ExtrinsicTypeLocal {
|
||||
return when (val content = extrinsic.content) {
|
||||
is Content.ContractCall -> ExtrinsicTypeLocal(
|
||||
foreignKey = foreignKey,
|
||||
contentType = ExtrinsicTypeLocal.ContentType.SMART_CONTRACT_CALL,
|
||||
module = content.contractAddress,
|
||||
call = content.function,
|
||||
fee = extrinsic.fee
|
||||
)
|
||||
|
||||
is Content.SubstrateCall -> ExtrinsicTypeLocal(
|
||||
foreignKey = foreignKey,
|
||||
contentType = ExtrinsicTypeLocal.ContentType.SUBSTRATE_CALL,
|
||||
module = content.module,
|
||||
call = content.call,
|
||||
fee = extrinsic.fee
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapTransferToLocal(
|
||||
transfer: Type.Transfer,
|
||||
foreignKey: OperationForeignKey
|
||||
): TransferTypeLocal = with(transfer) {
|
||||
TransferTypeLocal(
|
||||
foreignKey = foreignKey,
|
||||
amount = amount,
|
||||
sender = sender,
|
||||
receiver = receiver,
|
||||
fee = fee
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapRewardToLocal(
|
||||
reward: Type.Reward,
|
||||
foreignKey: OperationForeignKey
|
||||
): RewardTypeLocal = with(reward) {
|
||||
when (val kind = reward.kind) {
|
||||
is RewardKind.Direct -> DirectRewardTypeLocal(
|
||||
foreignKey = foreignKey,
|
||||
isReward = isReward,
|
||||
amount = amount,
|
||||
eventId = eventId,
|
||||
era = kind.era,
|
||||
validator = kind.validator
|
||||
)
|
||||
|
||||
is RewardKind.Pool -> PoolRewardTypeLocal(
|
||||
foreignKey = foreignKey,
|
||||
isReward = isReward,
|
||||
amount = amount,
|
||||
eventId = eventId,
|
||||
poolId = kind.poolId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapSwapToLocal(
|
||||
swap: Type.Swap,
|
||||
foreignKey: OperationForeignKey
|
||||
): SwapTypeLocal = with(swap) {
|
||||
SwapTypeLocal(
|
||||
foreignKey = foreignKey,
|
||||
fee = mapAssetWithAmountToLocal(fee),
|
||||
assetIn = mapAssetWithAmountToLocal(amountIn),
|
||||
assetOut = mapAssetWithAmountToLocal(amountOut),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapExtrinsicFromLocal(
|
||||
local: ExtrinsicTypeJoin,
|
||||
chainAsset: Chain.Asset,
|
||||
coinRate: CoinRate?,
|
||||
): Type.Extrinsic {
|
||||
val content = when (local.contentType) {
|
||||
ExtrinsicTypeLocal.ContentType.SUBSTRATE_CALL -> Content.SubstrateCall(
|
||||
module = local.module,
|
||||
call = local.call.orEmpty()
|
||||
)
|
||||
ExtrinsicTypeLocal.ContentType.SMART_CONTRACT_CALL -> Content.ContractCall(
|
||||
contractAddress = local.module,
|
||||
function = local.call
|
||||
)
|
||||
}
|
||||
|
||||
return Type.Extrinsic(
|
||||
content = content,
|
||||
fee = local.fee,
|
||||
fiatFee = coinRate?.convertPlanks(chainAsset, local.fee)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapDirectRewardFromLocal(
|
||||
local: DirectRewardTypeJoin,
|
||||
chainAsset: Chain.Asset,
|
||||
coinRate: CoinRate?,
|
||||
): Type.Reward {
|
||||
return Type.Reward(
|
||||
amount = local.amount,
|
||||
isReward = local.isReward,
|
||||
eventId = local.eventId,
|
||||
kind = RewardKind.Direct(
|
||||
// For a null value of Int? field, Room inserts zero when this Int? is used in Join
|
||||
era = local.era.takeIf { it != 0 },
|
||||
validator = local.validator
|
||||
),
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, local.amount)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapPoolRewardFromLocal(
|
||||
local: PoolRewardTypeJoin,
|
||||
chainAsset: Chain.Asset,
|
||||
coinRate: CoinRate?,
|
||||
): Type.Reward {
|
||||
return Type.Reward(
|
||||
amount = local.amount,
|
||||
isReward = local.isReward,
|
||||
eventId = local.eventId,
|
||||
kind = RewardKind.Pool(poolId = local.poolId),
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, local.amount)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapTransferFromLocal(
|
||||
local: TransferTypeJoin,
|
||||
chainAsset: Chain.Asset,
|
||||
coinRate: CoinRate?,
|
||||
myAddress: String,
|
||||
): Type.Transfer {
|
||||
return Type.Transfer(
|
||||
amount = local.amount,
|
||||
myAddress = myAddress,
|
||||
receiver = local.receiver,
|
||||
sender = local.sender,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, local.amount),
|
||||
fee = local.fee
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapSwapFromLocal(
|
||||
local: SwapTypeJoin,
|
||||
chainAsset: Chain.Asset,
|
||||
chain: Chain,
|
||||
coinRate: CoinRate?,
|
||||
): Type.Swap? {
|
||||
val amountIn = mapAssetWithAmountFromLocal(chain, local.assetIn) ?: return null
|
||||
val amountOut = mapAssetWithAmountFromLocal(chain, local.assetOut) ?: return null
|
||||
|
||||
val amount = if (amountIn.chainAsset.fullId == chainAsset.fullId) amountIn.amount else amountOut.amount
|
||||
|
||||
return Type.Swap(
|
||||
fee = mapAssetWithAmountFromLocal(chain, local.fee) ?: return null,
|
||||
amountIn = amountIn,
|
||||
amountOut = amountOut,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, amount),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapAssetWithAmountFromLocal(
|
||||
chain: Chain,
|
||||
local: SwapTypeLocal.AssetWithAmount
|
||||
): ChainAssetWithAmount? {
|
||||
val asset = chain.assetsById[local.assetId.assetId] ?: return null
|
||||
|
||||
return ChainAssetWithAmount(
|
||||
chainAsset = asset,
|
||||
amount = local.amount
|
||||
)
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package io.novafoundation.nova.feature_assets.data.network
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.mergeIfMultiple
|
||||
import io.novafoundation.nova.common.utils.transformLatestDiffed
|
||||
import io.novafoundation.nova.core.updater.UpdateSystem
|
||||
import io.novafoundation.nova.core.updater.Updater
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
|
||||
import io.novafoundation.nova.feature_staking_api.data.network.blockhain.updaters.PooledBalanceUpdaterFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.BalanceLocksUpdaterFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.PaymentUpdaterFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.subscribe
|
||||
import io.novafoundation.nova.runtime.ext.isDisabled
|
||||
import io.novafoundation.nova.runtime.ext.isFullSync
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class BalancesUpdateSystem(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val paymentUpdaterFactory: PaymentUpdaterFactory,
|
||||
private val balanceLocksUpdater: BalanceLocksUpdaterFactory,
|
||||
private val pooledBalanceUpdaterFactory: PooledBalanceUpdaterFactory,
|
||||
private val accountUpdateScope: AccountUpdateScope,
|
||||
private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
|
||||
) : UpdateSystem {
|
||||
|
||||
override fun start(): Flow<Updater.SideEffect> {
|
||||
return accountUpdateScope.invalidationFlow().flatMapLatest { metaAccount ->
|
||||
chainRegistry.currentChains.transformLatestDiffed { chain ->
|
||||
emitAll(balancesSync(chain, metaAccount))
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}
|
||||
|
||||
private suspend fun balancesSync(chain: Chain, metaAccount: MetaAccount): Flow<Updater.SideEffect> {
|
||||
return when {
|
||||
!metaAccount.hasAccountIn(chain) -> emptyFlow()
|
||||
chain.connectionState.isDisabled -> emptyFlow()
|
||||
chain.canPerformFullSync() -> fullBalancesSync(chain, metaAccount)
|
||||
else -> lightBalancesSync(chain, metaAccount)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fullBalancesSync(
|
||||
chain: Chain,
|
||||
metaAccount: MetaAccount,
|
||||
): Flow<Updater.SideEffect> {
|
||||
return launchChainUpdaters(
|
||||
chain = chain,
|
||||
metaAccount = metaAccount,
|
||||
createUpdaters = { createFullSyncUpdaters(chain) }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun lightBalancesSync(
|
||||
chain: Chain,
|
||||
metaAccount: MetaAccount,
|
||||
): Flow<Updater.SideEffect> {
|
||||
return launchChainUpdaters(
|
||||
chain = chain,
|
||||
metaAccount = metaAccount,
|
||||
createUpdaters = { createLightSyncUpdaters(chain) }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun launchChainUpdaters(
|
||||
chain: Chain,
|
||||
metaAccount: MetaAccount,
|
||||
createUpdaters: suspend () -> List<Updater<MetaAccount>>
|
||||
): Flow<Updater.SideEffect> {
|
||||
return flow {
|
||||
val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id)
|
||||
|
||||
val updaters = createUpdaters()
|
||||
|
||||
val sideEffectFlows = updaters.map { updater ->
|
||||
try {
|
||||
updater.listenForUpdates(subscriptionBuilder, metaAccount).catch { logError(chain, it) }
|
||||
} catch (e: Exception) {
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
subscriptionBuilder.subscribe(coroutineContext)
|
||||
val resultFlow = sideEffectFlows.mergeIfMultiple()
|
||||
|
||||
emitAll(resultFlow)
|
||||
}.catch { logError(chain, it) }
|
||||
}
|
||||
|
||||
private fun Chain.canPerformFullSync(): Boolean {
|
||||
return connectionState.isFullSync || !hasSubstrateRuntime
|
||||
}
|
||||
|
||||
private fun createFullSyncUpdaters(chain: Chain): List<Updater<MetaAccount>> {
|
||||
return listOf(
|
||||
paymentUpdaterFactory.createFullSync(chain),
|
||||
balanceLocksUpdater.create(chain),
|
||||
pooledBalanceUpdaterFactory.create(chain)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createLightSyncUpdaters(chain: Chain): List<Updater<MetaAccount>> {
|
||||
return listOf(
|
||||
paymentUpdaterFactory.createLightSync(chain),
|
||||
)
|
||||
}
|
||||
|
||||
private fun logError(chain: Chain, error: Throwable) {
|
||||
Log.e(LOG_TAG, "Failed to subscribe to balances in ${chain.name}: ${error.message}", error)
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package io.novafoundation.nova.feature_assets.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.feature_assets.domain.novaCard.NovaCardState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface NovaCardStateRepository {
|
||||
|
||||
fun getNovaCardCreationState(): NovaCardState
|
||||
|
||||
fun setNovaCardCreationState(state: NovaCardState)
|
||||
|
||||
fun observeNovaCardCreationState(): Flow<NovaCardState>
|
||||
|
||||
fun setLastTopUpTime(time: Long)
|
||||
|
||||
fun getLastTopUpTime(): Long
|
||||
|
||||
suspend fun setTopUpFinishedEvent()
|
||||
|
||||
fun observeTopUpFinishedEvent(): Flow<Unit>
|
||||
}
|
||||
|
||||
private const val PREFS_NOVA_CARD_STATE = "PREFS_NOVA_CARD_STATE"
|
||||
private const val PREFS_TIME_CARD_BEING_ISSUED = "PREFS_TIME_CARD_BEING_ISSUED"
|
||||
|
||||
class RealNovaCardStateRepository(
|
||||
private val preferences: Preferences
|
||||
) : NovaCardStateRepository {
|
||||
|
||||
private val topUpFinishedEvent = MutableSharedFlow<Unit>()
|
||||
|
||||
override fun getNovaCardCreationState(): NovaCardState {
|
||||
val novaCardState = preferences.getString(PREFS_NOVA_CARD_STATE, NovaCardState.NONE.toString())
|
||||
return NovaCardState.valueOf(novaCardState)
|
||||
}
|
||||
|
||||
override fun setNovaCardCreationState(state: NovaCardState) {
|
||||
preferences.putString(PREFS_NOVA_CARD_STATE, state.toString())
|
||||
}
|
||||
|
||||
override fun observeNovaCardCreationState(): Flow<NovaCardState> {
|
||||
return preferences.keyFlow(PREFS_NOVA_CARD_STATE)
|
||||
.map { getNovaCardCreationState() }
|
||||
}
|
||||
|
||||
override fun setLastTopUpTime(time: Long) {
|
||||
preferences.putLong(PREFS_TIME_CARD_BEING_ISSUED, time)
|
||||
}
|
||||
|
||||
override fun getLastTopUpTime(): Long {
|
||||
return preferences.getLong(PREFS_TIME_CARD_BEING_ISSUED, 0)
|
||||
}
|
||||
|
||||
override suspend fun setTopUpFinishedEvent() {
|
||||
topUpFinishedEvent.emit(Unit)
|
||||
}
|
||||
|
||||
override fun observeTopUpFinishedEvent(): Flow<Unit> {
|
||||
return topUpFinishedEvent
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package io.novafoundation.nova.feature_assets.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.data.model.DataPage
|
||||
import io.novafoundation.nova.common.data.model.PageOffset
|
||||
import io.novafoundation.nova.common.utils.Filter
|
||||
import io.novafoundation.nova.common.utils.applyFilters
|
||||
import io.novafoundation.nova.core_db.dao.OperationDao
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationBaseLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationJoin
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.system.SystemAccountMatcher
|
||||
import io.novafoundation.nova.feature_assets.data.mappers.mapOperationLocalToOperation
|
||||
import io.novafoundation.nova.feature_assets.data.mappers.mapOperationToOperationLocalDb
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_staking_api.data.mythos.MythosMainPotMatcherFactory
|
||||
import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation
|
||||
import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.poolRewardAccountMatcher
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.AssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.getAllCoinPriceHistory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.findNearestCoinRate
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrNull
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
interface TransactionHistoryRepository {
|
||||
|
||||
suspend fun syncOperationsFirstPage(
|
||||
pageSize: Int,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
)
|
||||
|
||||
suspend fun getOperations(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): DataPage<Operation>
|
||||
|
||||
suspend fun operationsFirstPageFlow(
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): Flow<DataPage<Operation>>
|
||||
}
|
||||
|
||||
class RealTransactionHistoryRepository(
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val operationDao: OperationDao,
|
||||
private val poolAccountDerivation: PoolAccountDerivation,
|
||||
private val mythosMainPotMatcherFactory: MythosMainPotMatcherFactory,
|
||||
private val coinPriceRepository: CoinPriceRepository
|
||||
) : TransactionHistoryRepository {
|
||||
|
||||
override suspend fun syncOperationsFirstPage(
|
||||
pageSize: Int,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
) = withContext(Dispatchers.Default) {
|
||||
val historySource = historySourceFor(chainAsset)
|
||||
val accountAddress = chain.addressOf(accountId)
|
||||
|
||||
val dataPageResult = runCatching {
|
||||
historySource.getFilteredOperations(
|
||||
pageSize,
|
||||
PageOffset.Loadable.FirstPage,
|
||||
filters,
|
||||
accountId,
|
||||
chain,
|
||||
chainAsset,
|
||||
currency
|
||||
)
|
||||
}
|
||||
historySource.additionalFirstPageSync(chain, chainAsset, accountId, dataPageResult)
|
||||
|
||||
val dataPage = dataPageResult.getOrThrow()
|
||||
|
||||
val localOperations = dataPage.map { mapOperationToOperationLocalDb(it, OperationBaseLocal.Source.REMOTE) }
|
||||
|
||||
operationDao.insertFromRemote(accountAddress, chain.id, chainAsset.id, localOperations)
|
||||
}
|
||||
|
||||
override suspend fun getOperations(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): DataPage<Operation> = withContext(Dispatchers.Default) {
|
||||
val historySource = historySourceFor(chainAsset)
|
||||
|
||||
historySource.getFilteredOperations(
|
||||
pageSize = pageSize,
|
||||
pageOffset = pageOffset,
|
||||
filters = filters,
|
||||
accountId = accountId,
|
||||
chain = chain,
|
||||
chainAsset = chainAsset,
|
||||
currency = currency
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun operationsFirstPageFlow(
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): Flow<DataPage<Operation>> {
|
||||
val accountAddress = chain.addressOf(accountId)
|
||||
val historySource = historySourceFor(chainAsset)
|
||||
|
||||
return operationDao.observe(accountAddress, chain.id, chainAsset.id)
|
||||
.transform { operations ->
|
||||
emit(mapOperations(operations, chainAsset, chain, emptyList()))
|
||||
|
||||
runCatching { coinPriceRepository.getAllCoinPriceHistory(chainAsset.priceId!!, currency) }
|
||||
.onSuccess { emit(mapOperations(operations, chainAsset, chain, it)) }
|
||||
}
|
||||
.mapLatest { operations ->
|
||||
val pageOffset = historySource.getSyncedPageOffset(accountId, chain, chainAsset)
|
||||
|
||||
DataPage(pageOffset, operations)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapOperations(
|
||||
operations: List<OperationJoin>,
|
||||
chainAsset: Chain.Asset,
|
||||
chain: Chain,
|
||||
coinPrices: List<HistoricalCoinRate>,
|
||||
): List<Operation> {
|
||||
return operations.mapNotNull { operation ->
|
||||
val operationTimestamp = operation.base.time.milliseconds.inWholeSeconds
|
||||
val coinPrice = coinPrices.findNearestCoinRate(operationTimestamp)
|
||||
mapOperationLocalToOperation(operation, chainAsset, chain, coinPrice)
|
||||
}
|
||||
}
|
||||
|
||||
private fun historySourceFor(chainAsset: Chain.Asset): AssetHistory = assetSourceRegistry.sourceFor(chainAsset).history
|
||||
|
||||
private suspend fun AssetHistory.getFilteredOperations(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): DataPage<Operation> {
|
||||
val nonFiltered = getOperations(pageSize, pageOffset, filters, accountId, chain, chainAsset, currency)
|
||||
|
||||
val pageFilters = createTransactionFilters(chain, chainAsset)
|
||||
val filtered = nonFiltered.applyFilters(pageFilters)
|
||||
|
||||
return DataPage(nonFiltered.nextOffset, items = filtered)
|
||||
}
|
||||
|
||||
private suspend fun AssetHistory.createTransactionFilters(chain: Chain, chainAsset: Chain.Asset): List<Filter<Operation>> {
|
||||
val systemAccountFilterCreator = { matcher: SystemAccountMatcher? ->
|
||||
matcher?.let { IgnoreTransfersFromSystemAccount(it, chain) }
|
||||
}
|
||||
|
||||
return listOfNotNull(
|
||||
IgnoreUnsafeOperations(this),
|
||||
systemAccountFilterCreator(poolAccountDerivation.poolRewardAccountMatcher(chain.id)),
|
||||
systemAccountFilterCreator(mythosMainPotMatcherFactory.create(chainAsset))
|
||||
)
|
||||
}
|
||||
|
||||
private class IgnoreTransfersFromSystemAccount(
|
||||
private val systemAccountMatcher: SystemAccountMatcher,
|
||||
private val chain: Chain
|
||||
) : Filter<Operation> {
|
||||
|
||||
override fun shouldInclude(model: Operation): Boolean {
|
||||
val operationType = model.type as? Operation.Type.Transfer ?: return true
|
||||
val accountId = chain.accountIdOrNull(operationType.sender) ?: return true
|
||||
|
||||
return !systemAccountMatcher.isSystemAccount(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
private class IgnoreUnsafeOperations(private val assetsHistory: AssetHistory) : Filter<Operation> {
|
||||
|
||||
override fun shouldInclude(model: Operation): Boolean {
|
||||
return assetsHistory.isOperationSafe(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.feature_assets.data.repository.assetFilters
|
||||
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.filters.AssetFilter
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.filters.NonZeroBalanceFilter
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface AssetFiltersRepository {
|
||||
|
||||
val allFilters: List<AssetFilter>
|
||||
|
||||
fun assetFiltersFlow(): Flow<List<AssetFilter>>
|
||||
|
||||
fun updateAssetFilters(filters: List<AssetFilter>)
|
||||
}
|
||||
|
||||
private const val PREF_ASSET_FILTERS = "ASSET_FILTERS"
|
||||
|
||||
class PreferencesAssetFiltersRepository(
|
||||
private val preferences: Preferences
|
||||
) : AssetFiltersRepository {
|
||||
|
||||
override val allFilters: List<AssetFilter> = listOf(
|
||||
NonZeroBalanceFilter
|
||||
)
|
||||
|
||||
private val filterFactory = allFilters.associateBy(AssetFilter::name)
|
||||
|
||||
override fun assetFiltersFlow(): Flow<List<AssetFilter>> {
|
||||
return preferences.stringFlow(PREF_ASSET_FILTERS).map { encoded ->
|
||||
encoded?.let {
|
||||
encoded.split(",").mapNotNull(filterFactory::get)
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateAssetFilters(filters: List<AssetFilter>) {
|
||||
val encoded = filters.joinToString(separator = ",") { it.name }
|
||||
|
||||
preferences.putString(PREF_ASSET_FILTERS, encoded)
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_assets.di
|
||||
|
||||
import io.novafoundation.nova.feature_assets.data.network.BalancesUpdateSystem
|
||||
import io.novafoundation.nova.feature_assets.di.modules.deeplinks.AssetDeepLinks
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator
|
||||
|
||||
interface AssetsFeatureApi {
|
||||
|
||||
val updateSystem: BalancesUpdateSystem
|
||||
|
||||
val coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory
|
||||
|
||||
val assetDeepLinks: AssetDeepLinks
|
||||
|
||||
val assetDetailsDeepLinkConfigurator: AssetDetailsDeepLinkConfigurator
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package io.novafoundation.nova.feature_assets.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_account_api.presenatation.mixin.selectAddress.SelectAddressCommunicator
|
||||
import io.novafoundation.nova.feature_ahm_api.di.ChainMigrationFeatureApi
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.detail.di.BalanceDetailComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.di.BalanceListComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.GoToNftsView
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.search.di.AssetSearchComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.gifts.assets.di.AssetGiftsFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.gifts.networks.di.NetworkGiftsFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.novacard.overview.di.NovaCardComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.topup.di.TopUpAddressComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.novacard.waiting.di.WaitingNovaCardTopUpComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.buy.flow.asset.di.AssetBuyFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.buy.flow.network.di.NetworkBuyFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.receive.di.ReceiveComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.receive.flow.asset.di.AssetReceiveFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.receive.flow.network.di.NetworkReceiveFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.amount.di.SelectSendComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.confirm.di.ConfirmSendComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.flow.asset.di.AssetSendFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.flow.network.di.NetworkSendFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.swap.asset.di.AssetSwapFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.swap.network.di.NetworkSwapFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.di.AddTokenEnterInfoComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.add.selectChain.di.AddTokenSelectChainComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.di.ManageChainTokensComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.di.ManageTokensComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.topup.TopUpAddressCommunicator
|
||||
import io.novafoundation.nova.feature_assets.presentation.bridge.di.BridgeComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.sell.flow.asset.di.AssetSellFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.sell.flow.network.di.NetworkSellFlowComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.provider.di.TradeProviderListComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.webInterface.di.TradeWebComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.detail.di.ExtrinsicDetailComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.detail.di.PoolRewardDetailComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.detail.di.RewardDetailComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.detail.di.TransactionDetailComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.detail.swap.di.SwapDetailComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.filter.di.TransactionHistoryFilterComponent
|
||||
import io.novafoundation.nova.feature_banners_api.di.BannersFeatureApi
|
||||
import io.novafoundation.nova.feature_buy_api.di.BuyFeatureApi
|
||||
import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
|
||||
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
|
||||
import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import io.novafoundation.nova.web3names.di.Web3NamesApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
AssetsFeatureDependencies::class
|
||||
],
|
||||
modules = [
|
||||
AssetsFeatureModule::class,
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface AssetsFeatureComponent : AssetsFeatureApi {
|
||||
|
||||
fun balanceListComponentFactory(): BalanceListComponent.Factory
|
||||
|
||||
fun balanceDetailComponentFactory(): BalanceDetailComponent.Factory
|
||||
|
||||
fun chooseAmountComponentFactory(): SelectSendComponent.Factory
|
||||
|
||||
fun confirmTransferComponentFactory(): ConfirmSendComponent.Factory
|
||||
|
||||
fun transactionDetailComponentFactory(): TransactionDetailComponent.Factory
|
||||
|
||||
fun swapDetailComponentFactory(): SwapDetailComponent.Factory
|
||||
|
||||
fun transactionHistoryComponentFactory(): TransactionHistoryFilterComponent.Factory
|
||||
|
||||
fun rewardDetailComponentFactory(): RewardDetailComponent.Factory
|
||||
|
||||
fun poolRewardDetailComponentFactory(): PoolRewardDetailComponent.Factory
|
||||
|
||||
fun extrinsicDetailComponentFactory(): ExtrinsicDetailComponent.Factory
|
||||
|
||||
fun receiveComponentFactory(): ReceiveComponent.Factory
|
||||
|
||||
fun assetSearchComponentFactory(): AssetSearchComponent.Factory
|
||||
|
||||
fun manageTokensComponentFactory(): ManageTokensComponent.Factory
|
||||
|
||||
fun manageChainTokensComponentFactory(): ManageChainTokensComponent.Factory
|
||||
|
||||
fun addTokenSelectChainComponentFactory(): AddTokenSelectChainComponent.Factory
|
||||
|
||||
fun addTokenEnterInfoComponentFactory(): AddTokenEnterInfoComponent.Factory
|
||||
|
||||
fun sendFlowComponent(): AssetSendFlowComponent.Factory
|
||||
|
||||
fun swapFlowComponent(): AssetSwapFlowComponent.Factory
|
||||
|
||||
fun receiveFlowComponent(): AssetReceiveFlowComponent.Factory
|
||||
|
||||
fun buyFlowComponent(): AssetBuyFlowComponent.Factory
|
||||
|
||||
fun sellFlowComponent(): AssetSellFlowComponent.Factory
|
||||
|
||||
fun bridgeComponentFactory(): BridgeComponent.Factory
|
||||
|
||||
fun giftsFlowComponent(): AssetGiftsFlowComponent.Factory
|
||||
|
||||
fun tradeProviderListComponent(): TradeProviderListComponent.Factory
|
||||
|
||||
fun tradeWebComponent(): TradeWebComponent.Factory
|
||||
|
||||
fun networkBuyFlowComponent(): NetworkBuyFlowComponent.Factory
|
||||
|
||||
fun networkSellFlowComponent(): NetworkSellFlowComponent.Factory
|
||||
|
||||
fun networkReceiveFlowComponent(): NetworkReceiveFlowComponent.Factory
|
||||
|
||||
fun networkSendFlowComponent(): NetworkSendFlowComponent.Factory
|
||||
|
||||
fun networkSwapFlowComponent(): NetworkSwapFlowComponent.Factory
|
||||
|
||||
fun topUpCardComponentFactory(): TopUpAddressComponent.Factory
|
||||
|
||||
fun networkGiftsFlowComponent(): NetworkGiftsFlowComponent.Factory
|
||||
|
||||
fun novaCardComponentFactory(): NovaCardComponent.Factory
|
||||
|
||||
fun waitingNovaCardTopUpComponentFactory(): WaitingNovaCardTopUpComponent.Factory
|
||||
|
||||
fun inject(view: GoToNftsView)
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance accountRouter: AssetsRouter,
|
||||
@BindsInstance selectAddressCommunicator: SelectAddressCommunicator,
|
||||
@BindsInstance topUpAddressCommunicator: TopUpAddressCommunicator,
|
||||
deps: AssetsFeatureDependencies
|
||||
): AssetsFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
DbApi::class,
|
||||
RuntimeApi::class,
|
||||
NftFeatureApi::class,
|
||||
WalletFeatureApi::class,
|
||||
AccountFeatureApi::class,
|
||||
CurrencyFeatureApi::class,
|
||||
CrowdloanFeatureApi::class,
|
||||
StakingFeatureApi::class,
|
||||
Web3NamesApi::class,
|
||||
WalletConnectFeatureApi::class,
|
||||
SwapFeatureApi::class,
|
||||
BuyFeatureApi::class,
|
||||
BannersFeatureApi::class,
|
||||
DeepLinkingFeatureApi::class,
|
||||
ChainMigrationFeatureApi::class,
|
||||
GiftFeatureApi::class
|
||||
]
|
||||
)
|
||||
interface AssetsFeatureDependenciesComponent : AssetsFeatureDependencies
|
||||
}
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
package io.novafoundation.nova.feature_assets.di
|
||||
|
||||
import android.content.ContentResolver
|
||||
import coil.ImageLoader
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.address.format.EthereumAddressFormat
|
||||
import io.novafoundation.nova.common.data.memory.ComputationalCache
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.data.network.HttpExceptionHandler
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository
|
||||
import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository
|
||||
import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences
|
||||
import io.novafoundation.nova.common.domain.interactor.AssetViewModeInteractor
|
||||
import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.hints.ResourcesHintsMixinFactory
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ClipboardManager
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.QrCodeGenerator
|
||||
import io.novafoundation.nova.common.utils.browser.fileChoosing.WebViewFileChooserFactory
|
||||
import io.novafoundation.nova.common.utils.browser.permissions.WebViewPermissionAskerFactory
|
||||
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.utils.systemCall.SystemCallExecutor
|
||||
import io.novafoundation.nova.common.utils.webView.InterceptingWebViewClientFactory
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncher
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncherFactory
|
||||
import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBottomSheetLauncher
|
||||
import io.novafoundation.nova.core_db.dao.HoldsDao
|
||||
import io.novafoundation.nova.core_db.dao.LockDao
|
||||
import io.novafoundation.nova.core_db.dao.OperationDao
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.AddressDisplayUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.copyAddress.CopyAddressMixin
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.watchOnly.WatchOnlyMissingKeysPresenter
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.AddressInputMixinFactory
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectAddress.SelectAddressMixin
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.mixin.TradeMixin
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.interceptors.mercuryo.MercuryoSellRequestInterceptorFactory
|
||||
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
|
||||
import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsInteractor
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_staking_api.data.mythos.MythosMainPotMatcherFactory
|
||||
import io.novafoundation.nova.feature_staking_api.data.network.blockhain.updaters.PooledBalanceUpdaterFactory
|
||||
import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation
|
||||
import io.novafoundation.nova.feature_staking_api.presentation.nominationPools.display.PoolDisplayUseCase
|
||||
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
|
||||
import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.navigation.SwapFlowScopeAggregator
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.BalanceLocksUpdaterFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.PaymentUpdaterFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainValidationSystemProvider
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.priceApi.CoingeckoApi
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.priceApi.ProxyPriceApi
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryTokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstants
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.FiatFormatter
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterFactory
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade
|
||||
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationInfoUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.maxAction.MaxActionProviderFactory
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.runtime.di.LOCAL_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.qr.MultiChainQrSharingFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainStateRepository
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novafoundation.nova.web3names.domain.networking.Web3NamesInteractor
|
||||
import io.novasama.substrate_sdk_android.encrypt.Signer
|
||||
import io.novasama.substrate_sdk_android.icon.IconGenerator
|
||||
import io.novasama.substrate_sdk_android.wsrpc.logging.Logger
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Named
|
||||
|
||||
interface AssetsFeatureDependencies {
|
||||
|
||||
val maskingModeUseCase: MaskingModeUseCase
|
||||
|
||||
val maskableValueFormatterFactory: MaskableValueFormatterFactory
|
||||
|
||||
val amountFormatterProvider: MaskableValueFormatterProvider
|
||||
|
||||
val fiatFormatter: FiatFormatter
|
||||
|
||||
val amountFormatter: AmountFormatter
|
||||
|
||||
val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory
|
||||
|
||||
val assetsSourceRegistry: AssetSourceRegistry
|
||||
|
||||
val addressInputMixinFactory: AddressInputMixinFactory
|
||||
|
||||
val multiChainQrSharingFactory: MultiChainQrSharingFactory
|
||||
|
||||
val walletUiUseCase: WalletUiUseCase
|
||||
|
||||
val computationalCache: ComputationalCache
|
||||
|
||||
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
|
||||
val crossChainTraRepository: CrossChainTransfersRepository
|
||||
|
||||
val crossChainWeigher: CrossChainWeigher
|
||||
|
||||
val crossChainTransactor: CrossChainTransactor
|
||||
|
||||
val crossChainValidationSystemProvider: CrossChainValidationSystemProvider
|
||||
|
||||
val resourcesHintsMixinFactory: ResourcesHintsMixinFactory
|
||||
|
||||
val parachainInfoRepository: ParachainInfoRepository
|
||||
|
||||
val watchOnlyMissingKeysPresenter: WatchOnlyMissingKeysPresenter
|
||||
|
||||
val balanceLocksRepository: BalanceLocksRepository
|
||||
|
||||
val chainAssetRepository: ChainAssetRepository
|
||||
|
||||
val erc20Standard: Erc20Standard
|
||||
|
||||
val externalBalanceRepository: ExternalBalanceRepository
|
||||
|
||||
val pooledBalanceUpdaterFactory: PooledBalanceUpdaterFactory
|
||||
|
||||
val paymentUpdaterFactory: PaymentUpdaterFactory
|
||||
|
||||
val locksUpdaterFactory: BalanceLocksUpdaterFactory
|
||||
|
||||
val accountUpdateScope: AccountUpdateScope
|
||||
|
||||
val storageSharedRequestBuilderFactory: StorageSharedRequestsBuilderFactory
|
||||
|
||||
val poolDisplayUseCase: PoolDisplayUseCase
|
||||
|
||||
val poolAccountDerivation: PoolAccountDerivation
|
||||
|
||||
val operationDao: OperationDao
|
||||
|
||||
val coinPriceRepository: CoinPriceRepository
|
||||
|
||||
val swapSettingsStateProvider: SwapSettingsStateProvider
|
||||
|
||||
val swapService: SwapService
|
||||
|
||||
val swapAvailabilityInteractor: SwapAvailabilityInteractor
|
||||
|
||||
val bannerVisibilityRepository: BannerVisibilityRepository
|
||||
|
||||
val tradeMixinFactory: TradeMixin.Factory
|
||||
|
||||
val crossChainTransfersUseCase: CrossChainTransfersUseCase
|
||||
|
||||
val arbitraryTokenUseCase: ArbitraryTokenUseCase
|
||||
|
||||
val swapRateFormatter: SwapRateFormatter
|
||||
|
||||
val bottomSheetLauncher: DescriptionBottomSheetLauncher
|
||||
|
||||
val selectAddressMixinFactory: SelectAddressMixin.Factory
|
||||
|
||||
val chainStateRepository: ChainStateRepository
|
||||
|
||||
val holdsRepository: BalanceHoldsRepository
|
||||
|
||||
val holdsDao: HoldsDao
|
||||
|
||||
val coinGeckoLinkParser: CoinGeckoLinkParser
|
||||
|
||||
val assetIconProvider: AssetIconProvider
|
||||
|
||||
val swapFlowScopeAggregator: SwapFlowScopeAggregator
|
||||
|
||||
val okHttpClient: OkHttpClient
|
||||
|
||||
val mythosMainPotMatcherFactory: MythosMainPotMatcherFactory
|
||||
|
||||
val bannerSourceFactory: BannersSourceFactory
|
||||
|
||||
val bannersMixinFactory: PromotionBannersMixinFactory
|
||||
|
||||
val webViewPermissionAskerFactory: WebViewPermissionAskerFactory
|
||||
|
||||
val webViewFileChooserFactory: WebViewFileChooserFactory
|
||||
|
||||
val tradeTokenRegistry: TradeTokenRegistry
|
||||
|
||||
val interceptingWebViewClientFactory: InterceptingWebViewClientFactory
|
||||
|
||||
val mercuryoSellRequestInterceptorFactory: MercuryoSellRequestInterceptorFactory
|
||||
|
||||
val multisigPendingOperationsService: MultisigPendingOperationsService
|
||||
|
||||
val automaticInteractionGate: AutomaticInteractionGate
|
||||
|
||||
val linkBuilderFactory: LinkBuilderFactory
|
||||
|
||||
val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper
|
||||
|
||||
val actionBottomSheetLauncherFactory: ActionBottomSheetLauncherFactory
|
||||
|
||||
val actionBottomSheetLauncher: ActionBottomSheetLauncher
|
||||
|
||||
val chainMigrationInfoUseCase: ChainMigrationInfoUseCase
|
||||
|
||||
val sendUseCase: SendUseCase
|
||||
|
||||
val feePaymentProviderRegistry: FeePaymentProviderRegistry
|
||||
|
||||
val customFeeCapabilityFacade: CustomFeeCapabilityFacade
|
||||
|
||||
val availableGiftAssetsUseCase: AvailableGiftAssetsUseCase
|
||||
|
||||
val giftsAccountSupportedUseCase: GiftsAccountSupportedUseCase
|
||||
|
||||
fun web3NamesInteractor(): Web3NamesInteractor
|
||||
|
||||
fun contributionsInteractor(): ContributionsInteractor
|
||||
|
||||
fun contributionsRepository(): ContributionsRepository
|
||||
|
||||
fun locksDao(): LockDao
|
||||
|
||||
fun currencyInteractor(): CurrencyInteractor
|
||||
|
||||
fun currencyRepository(): CurrencyRepository
|
||||
|
||||
fun metaAccountGroupingInteractor(): MetaAccountGroupingInteractor
|
||||
|
||||
fun accountInteractor(): AccountInteractor
|
||||
|
||||
fun preferences(): Preferences
|
||||
|
||||
fun encryptedPreferences(): EncryptedPreferences
|
||||
|
||||
fun resourceManager(): ResourceManager
|
||||
|
||||
fun iconGenerator(): IconGenerator
|
||||
|
||||
fun clipboardManager(): ClipboardManager
|
||||
|
||||
fun contentResolver(): ContentResolver
|
||||
|
||||
fun accountRepository(): AccountRepository
|
||||
|
||||
fun networkCreator(): NetworkApiCreator
|
||||
|
||||
fun signer(): Signer
|
||||
|
||||
fun logger(): Logger
|
||||
|
||||
fun jsonMapper(): Gson
|
||||
|
||||
fun addressIconGenerator(): AddressIconGenerator
|
||||
|
||||
fun appLinksProvider(): AppLinksProvider
|
||||
|
||||
fun qrCodeGenerator(): QrCodeGenerator
|
||||
|
||||
fun fileProvider(): FileProvider
|
||||
|
||||
fun externalAccountActions(): ExternalActions.Presentation
|
||||
|
||||
fun httpExceptionHandler(): HttpExceptionHandler
|
||||
|
||||
fun addressDisplayUseCase(): AddressDisplayUseCase
|
||||
|
||||
fun chainRegistry(): ChainRegistry
|
||||
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
fun remoteStorageSource(): StorageDataSource
|
||||
|
||||
@Named(LOCAL_STORAGE_SOURCE)
|
||||
fun localStorageSource(): StorageDataSource
|
||||
|
||||
fun extrinsicService(): ExtrinsicService
|
||||
|
||||
fun imageLoader(): ImageLoader
|
||||
|
||||
fun selectedAccountUseCase(): SelectedAccountUseCase
|
||||
|
||||
fun validationExecutor(): ValidationExecutor
|
||||
|
||||
fun eventsRepository(): EventsRepository
|
||||
|
||||
fun walletRepository(): WalletRepository
|
||||
|
||||
fun feeLoaderMixinFactory(): FeeLoaderMixin.Factory
|
||||
|
||||
fun amountChooserFactory(): AmountChooserMixin.Factory
|
||||
|
||||
fun walletConstants(): WalletConstants
|
||||
|
||||
fun ethereumAddressFormat(): EthereumAddressFormat
|
||||
|
||||
fun proxyPriceApi(): ProxyPriceApi
|
||||
|
||||
fun coingeckoApi(): CoingeckoApi
|
||||
|
||||
fun assetsViewModeRepository(): AssetsViewModeRepository
|
||||
|
||||
fun walletConnectSessionsUseCase(): WalletConnectSessionsUseCase
|
||||
|
||||
fun assetsIconModeRepository(): AssetsIconModeRepository
|
||||
|
||||
fun nftRepository(): NftRepository
|
||||
|
||||
fun systemCallExecutor(): SystemCallExecutor
|
||||
|
||||
fun permissionsAskerFactory(): PermissionsAskerFactory
|
||||
|
||||
fun assetViewModeInteractor(): AssetViewModeInteractor
|
||||
|
||||
fun maxActionProviderFactory(): MaxActionProviderFactory
|
||||
|
||||
fun copyAddressMixin(): CopyAddressMixin
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.feature_assets.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_account_api.presenatation.mixin.selectAddress.SelectAddressCommunicator
|
||||
import io.novafoundation.nova.feature_ahm_api.di.ChainMigrationFeatureApi
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.topup.TopUpAddressCommunicator
|
||||
import io.novafoundation.nova.feature_banners_api.di.BannersFeatureApi
|
||||
import io.novafoundation.nova.feature_buy_api.di.BuyFeatureApi
|
||||
import io.novafoundation.nova.feature_crowdloan_api.di.CrowdloanFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_nft_api.NftFeatureApi
|
||||
import io.novafoundation.nova.feature_staking_api.di.StakingFeatureApi
|
||||
import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import io.novafoundation.nova.web3names.di.Web3NamesApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class AssetsFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val selectAddressCommunicator: SelectAddressCommunicator,
|
||||
private val topUpAddressCommunicator: TopUpAddressCommunicator,
|
||||
private val router: AssetsRouter
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val dependencies = DaggerAssetsFeatureComponent_AssetsFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.dbApi(getFeature(DbApi::class.java))
|
||||
.nftFeatureApi(getFeature(NftFeatureApi::class.java))
|
||||
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.currencyFeatureApi(getFeature(CurrencyFeatureApi::class.java))
|
||||
.crowdloanFeatureApi(getFeature(CrowdloanFeatureApi::class.java))
|
||||
.web3NamesApi(getFeature(Web3NamesApi::class.java))
|
||||
.walletConnectFeatureApi(getFeature(WalletConnectFeatureApi::class.java))
|
||||
.stakingFeatureApi(getFeature(StakingFeatureApi::class.java))
|
||||
.swapFeatureApi(getFeature(SwapFeatureApi::class.java))
|
||||
.buyFeatureApi(getFeature(BuyFeatureApi::class.java))
|
||||
.bannersFeatureApi(getFeature(BannersFeatureApi::class.java))
|
||||
.deepLinkingFeatureApi(getFeature(DeepLinkingFeatureApi::class.java))
|
||||
.chainMigrationFeatureApi(getFeature(ChainMigrationFeatureApi::class.java))
|
||||
.giftFeatureApi(getFeature(GiftFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerAssetsFeatureComponent.factory()
|
||||
.create(router, selectAddressCommunicator, topUpAddressCommunicator, dependencies)
|
||||
}
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
package io.novafoundation.nova.feature_assets.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.memory.ComputationalCache
|
||||
import io.novafoundation.nova.common.data.model.MaskingMode
|
||||
import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterFactory
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncher
|
||||
import io.novafoundation.nova.core_db.dao.OperationDao
|
||||
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.domain.updaters.AccountUpdateScope
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.watchOnly.WatchOnlyMissingKeysPresenter
|
||||
import io.novafoundation.nova.feature_assets.data.network.BalancesUpdateSystem
|
||||
import io.novafoundation.nova.feature_assets.data.repository.NovaCardStateRepository
|
||||
import io.novafoundation.nova.feature_assets.data.repository.RealNovaCardStateRepository
|
||||
import io.novafoundation.nova.feature_assets.data.repository.RealTransactionHistoryRepository
|
||||
import io.novafoundation.nova.feature_assets.data.repository.TransactionHistoryRepository
|
||||
import io.novafoundation.nova.feature_assets.data.repository.assetFilters.AssetFiltersRepository
|
||||
import io.novafoundation.nova.feature_assets.data.repository.assetFilters.PreferencesAssetFiltersRepository
|
||||
import io.novafoundation.nova.feature_assets.di.modules.AddTokenModule
|
||||
import io.novafoundation.nova.feature_assets.di.modules.ManageTokensCommonModule
|
||||
import io.novafoundation.nova.feature_assets.di.modules.SendModule
|
||||
import io.novafoundation.nova.feature_assets.di.modules.deeplinks.DeepLinkModule
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractorImpl
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.RealExternalBalancesInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchInteractorFactory
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchUseCase
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.search.AssetViewModeAssetSearchInteractorFactory
|
||||
import io.novafoundation.nova.feature_assets.domain.networks.AssetNetworksInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.novaCard.NovaCardInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.novaCard.RealNovaCardInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.price.ChartsInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.price.RealChartsInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.ExpandableAssetsMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.gifts.GiftsRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.NetworkAssetFormatter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.NetworkAssetFormatterFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.TokenAssetFormatter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.TokenAssetFormatterFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.novacard.common.NovaCardRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.swap.executor.InitialSwapFlowExecutor
|
||||
import io.novafoundation.nova.feature_assets.presentation.swap.executor.SwapFlowExecutorFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.filter.HistoryFiltersProviderFactory
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_staking_api.data.mythos.MythosMainPotMatcherFactory
|
||||
import io.novafoundation.nova.feature_staking_api.data.network.blockhain.updaters.PooledBalanceUpdaterFactory
|
||||
import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation
|
||||
import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.BalanceLocksUpdaterFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.PaymentUpdaterFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.FiatFormatter
|
||||
import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(
|
||||
includes = [
|
||||
SendModule::class,
|
||||
ManageTokensCommonModule::class,
|
||||
AddTokenModule::class,
|
||||
DeepLinkModule::class
|
||||
]
|
||||
)
|
||||
class AssetsFeatureModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideExternalBalancesInteractor(
|
||||
accountRepository: AccountRepository,
|
||||
externalBalanceRepository: ExternalBalanceRepository
|
||||
): ExternalBalancesInteractor = RealExternalBalancesInteractor(accountRepository, externalBalanceRepository)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetSearchUseCase(
|
||||
walletRepository: WalletRepository,
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
swapService: SwapService
|
||||
) = AssetSearchUseCase(
|
||||
walletRepository = walletRepository,
|
||||
accountRepository = accountRepository,
|
||||
chainRegistry = chainRegistry,
|
||||
swapService = swapService
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSearchInteractorFactory(
|
||||
assetViewModeRepository: AssetsViewModeRepository,
|
||||
assetSearchUseCase: AssetSearchUseCase,
|
||||
chainRegistry: ChainRegistry,
|
||||
tradeTokenRegistry: TradeTokenRegistry,
|
||||
availableGiftAssetsUseCase: AvailableGiftAssetsUseCase
|
||||
): AssetSearchInteractorFactory = AssetViewModeAssetSearchInteractorFactory(
|
||||
assetViewModeRepository,
|
||||
assetSearchUseCase,
|
||||
chainRegistry,
|
||||
tradeTokenRegistry,
|
||||
availableGiftAssetsUseCase
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetNetworksInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
assetSearchUseCase: AssetSearchUseCase,
|
||||
tradeTokenRegistry: TradeTokenRegistry,
|
||||
giftAssetsUseCase: AvailableGiftAssetsUseCase
|
||||
) = AssetNetworksInteractor(chainRegistry, assetSearchUseCase, tradeTokenRegistry, giftAssetsUseCase)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetFiltersRepository(preferences: Preferences): AssetFiltersRepository {
|
||||
return PreferencesAssetFiltersRepository(preferences)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideWalletInteractor(
|
||||
walletRepository: WalletRepository,
|
||||
accountRepository: AccountRepository,
|
||||
assetFiltersRepository: AssetFiltersRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
nftRepository: NftRepository,
|
||||
transactionHistoryRepository: TransactionHistoryRepository,
|
||||
currencyRepository: CurrencyRepository
|
||||
): WalletInteractor = WalletInteractorImpl(
|
||||
walletRepository = walletRepository,
|
||||
accountRepository = accountRepository,
|
||||
assetFiltersRepository = assetFiltersRepository,
|
||||
chainRegistry = chainRegistry,
|
||||
nftRepository = nftRepository,
|
||||
transactionHistoryRepository = transactionHistoryRepository,
|
||||
currencyRepository = currencyRepository
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideHistoryFiltersProviderFactory(
|
||||
computationalCache: ComputationalCache,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
chainRegistry: ChainRegistry,
|
||||
) = HistoryFiltersProviderFactory(computationalCache, assetSourceRegistry, chainRegistry)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideControllableAssetCheckMixin(
|
||||
missingKeysPresenter: WatchOnlyMissingKeysPresenter,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
resourceManager: ResourceManager
|
||||
): ControllableAssetCheckMixin {
|
||||
return ControllableAssetCheckMixin(
|
||||
missingKeysPresenter,
|
||||
actionAwaitableMixinFactory,
|
||||
resourceManager
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideBalancesUpdateSystem(
|
||||
chainRegistry: ChainRegistry,
|
||||
paymentUpdaterFactory: PaymentUpdaterFactory,
|
||||
balanceLocksUpdater: BalanceLocksUpdaterFactory,
|
||||
pooledBalanceUpdaterFactory: PooledBalanceUpdaterFactory,
|
||||
accountUpdateScope: AccountUpdateScope,
|
||||
storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
|
||||
): BalancesUpdateSystem {
|
||||
return BalancesUpdateSystem(
|
||||
chainRegistry = chainRegistry,
|
||||
paymentUpdaterFactory = paymentUpdaterFactory,
|
||||
balanceLocksUpdater = balanceLocksUpdater,
|
||||
pooledBalanceUpdaterFactory = pooledBalanceUpdaterFactory,
|
||||
accountUpdateScope = accountUpdateScope,
|
||||
storageSharedRequestsBuilderFactory = storageSharedRequestsBuilderFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTransactionHistoryRepository(
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
operationsDao: OperationDao,
|
||||
coinPriceRepository: CoinPriceRepository,
|
||||
poolAccountDerivation: PoolAccountDerivation,
|
||||
mythosMainPotMatcherFactory: MythosMainPotMatcherFactory,
|
||||
): TransactionHistoryRepository = RealTransactionHistoryRepository(
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
operationDao = operationsDao,
|
||||
coinPriceRepository = coinPriceRepository,
|
||||
poolAccountDerivation = poolAccountDerivation,
|
||||
mythosMainPotMatcherFactory = mythosMainPotMatcherFactory
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNovaCardRepository(preferences: Preferences): NovaCardStateRepository {
|
||||
return RealNovaCardStateRepository(preferences)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNovaCardInteractor(repository: NovaCardStateRepository): NovaCardInteractor {
|
||||
return RealNovaCardInteractor(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideInitialSwapFlowExecutor(
|
||||
assetsRouter: AssetsRouter
|
||||
): InitialSwapFlowExecutor {
|
||||
return InitialSwapFlowExecutor(assetsRouter)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSwapExecutor(
|
||||
initialSwapFlowExecutor: InitialSwapFlowExecutor,
|
||||
assetsRouter: AssetsRouter,
|
||||
swapSettingsStateProvider: SwapSettingsStateProvider
|
||||
): SwapFlowExecutorFactory {
|
||||
return SwapFlowExecutorFactory(initialSwapFlowExecutor, assetsRouter, swapSettingsStateProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNetworkAssetMapperFactory(
|
||||
fiatFormatter: FiatFormatter,
|
||||
amountFormatter: AmountFormatter
|
||||
): NetworkAssetFormatterFactory {
|
||||
return NetworkAssetFormatterFactory(
|
||||
fiatFormatter,
|
||||
amountFormatter
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTokenAssetMapperFactory(amountFormatter: AmountFormatter): TokenAssetFormatterFactory {
|
||||
return TokenAssetFormatterFactory(amountFormatter)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNotMaskingNetworkAssetMapper(
|
||||
networkAssetFormatterFactory: NetworkAssetFormatterFactory,
|
||||
maskableValueFormatterFactory: MaskableValueFormatterFactory
|
||||
): NetworkAssetFormatter {
|
||||
return networkAssetFormatterFactory.create(maskableValueFormatterFactory.create(MaskingMode.DISABLED))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNotMaskingTokenAssetMapper(
|
||||
tokenAssetFormatterFactory: TokenAssetFormatterFactory,
|
||||
maskableValueFormatterFactory: MaskableValueFormatterFactory
|
||||
): TokenAssetFormatter {
|
||||
return tokenAssetFormatterFactory.create(maskableValueFormatterFactory.create(MaskingMode.DISABLED))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideExpandableAssetsMixinFactory(
|
||||
assetIconProvider: AssetIconProvider,
|
||||
currencyInteractor: CurrencyInteractor,
|
||||
assetsViewModeRepository: AssetsViewModeRepository,
|
||||
amountFormatterProvider: MaskableValueFormatterProvider,
|
||||
networkAssetFormatterFactory: NetworkAssetFormatterFactory,
|
||||
tokenAssetFormatterFactory: TokenAssetFormatterFactory,
|
||||
): ExpandableAssetsMixinFactory {
|
||||
return ExpandableAssetsMixinFactory(
|
||||
assetIconProvider,
|
||||
currencyInteractor,
|
||||
assetsViewModeRepository,
|
||||
amountFormatterProvider,
|
||||
networkAssetFormatterFactory,
|
||||
tokenAssetFormatterFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideChartsInteractor(
|
||||
coinPriceRepository: CoinPriceRepository,
|
||||
currencyRepository: CurrencyRepository
|
||||
): ChartsInteractor {
|
||||
return RealChartsInteractor(coinPriceRepository, currencyRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideBuySellRestrictionCheckMixin(
|
||||
accountUseCase: SelectedAccountUseCase,
|
||||
actionLauncher: ActionBottomSheetLauncher,
|
||||
resourceManager: ResourceManager
|
||||
): BuySellRestrictionCheckMixin {
|
||||
return BuySellRestrictionCheckMixin(
|
||||
accountUseCase,
|
||||
resourceManager,
|
||||
actionLauncher
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNovaCardRestrictionCheckMixin(
|
||||
accountUseCase: SelectedAccountUseCase,
|
||||
actionLauncher: ActionBottomSheetLauncher,
|
||||
resourceManager: ResourceManager,
|
||||
chainRegistry: ChainRegistry
|
||||
): NovaCardRestrictionCheckMixin {
|
||||
return NovaCardRestrictionCheckMixin(
|
||||
accountUseCase,
|
||||
resourceManager,
|
||||
actionLauncher,
|
||||
chainRegistry
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideBuySellMixinFactory(
|
||||
router: AssetsRouter,
|
||||
tradeTokenRegistry: TradeTokenRegistry,
|
||||
chainRegistry: ChainRegistry,
|
||||
resourceManager: ResourceManager,
|
||||
buySellRestrictionCheckMixin: BuySellRestrictionCheckMixin
|
||||
): BuySellSelectorMixinFactory {
|
||||
return BuySellSelectorMixinFactory(
|
||||
router,
|
||||
tradeTokenRegistry,
|
||||
chainRegistry,
|
||||
resourceManager,
|
||||
buySellRestrictionCheckMixin
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideGiftsRestrictionCheckMixin(
|
||||
accountSupportedUseCase: GiftsAccountSupportedUseCase,
|
||||
resourceManager: ResourceManager,
|
||||
actionLauncher: ActionBottomSheetLauncher,
|
||||
): GiftsRestrictionCheckMixin {
|
||||
return GiftsRestrictionCheckMixin(
|
||||
accountSupportedUseCase,
|
||||
resourceManager,
|
||||
actionLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.novafoundation.nova.feature_assets.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.address.format.EthereumAddressFormat
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.AddTokensInteractor
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.RealAddTokensInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.priceApi.CoingeckoApi
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class AddTokenModule {
|
||||
|
||||
@Provides
|
||||
fun coinGeckoLinkValidationFactory(
|
||||
coingeckoApi: CoingeckoApi,
|
||||
coinGeckoLinkParser: CoinGeckoLinkParser
|
||||
): CoinGeckoLinkValidationFactory {
|
||||
return CoinGeckoLinkValidationFactory(coingeckoApi, coinGeckoLinkParser)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
erc20Standard: Erc20Standard,
|
||||
chainAssetRepository: ChainAssetRepository,
|
||||
coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
ethereumAddressFormat: EthereumAddressFormat,
|
||||
currencyRepository: CurrencyRepository,
|
||||
walletRepository: WalletRepository,
|
||||
coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory
|
||||
): AddTokensInteractor {
|
||||
return RealAddTokensInteractor(
|
||||
chainRegistry,
|
||||
erc20Standard,
|
||||
chainAssetRepository,
|
||||
coinGeckoLinkParser,
|
||||
ethereumAddressFormat,
|
||||
currencyRepository,
|
||||
walletRepository,
|
||||
coinGeckoLinkValidationFactory
|
||||
)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_assets.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.AssetsDataCleaner
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.RealAssetsDataCleaner
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.manage.ManageTokenInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.manage.RealManageTokenInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.model.MultiChainTokenMapper
|
||||
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class ManageTokensCommonModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideMultiChainTokenUiMapper(
|
||||
assetIconProvider: AssetIconProvider,
|
||||
resourceManager: ResourceManager
|
||||
) = MultiChainTokenMapper(assetIconProvider, resourceManager)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetDataCleaner(
|
||||
externalBalanceRepository: ExternalBalanceRepository,
|
||||
contributionsRepository: ContributionsRepository,
|
||||
walletRepository: WalletRepository,
|
||||
): AssetsDataCleaner {
|
||||
return RealAssetsDataCleaner(externalBalanceRepository, contributionsRepository, walletRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
chainAssetRepository: ChainAssetRepository,
|
||||
assetsDataCleaner: AssetsDataCleaner
|
||||
): ManageTokenInteractor = RealManageTokenInteractor(
|
||||
chainRegistry = chainRegistry,
|
||||
chainAssetRepository = chainAssetRepository,
|
||||
assetsDataCleaner = assetsDataCleaner
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_assets.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_assets.domain.send.SendInteractor
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainValidationSystemProvider
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
|
||||
@Module
|
||||
class SendModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSendInteractor(
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
crossChainTransfersRepository: CrossChainTransfersRepository,
|
||||
crossChainTransactor: CrossChainTransactor,
|
||||
parachainInfoRepository: ParachainInfoRepository,
|
||||
extrinsicService: ExtrinsicService,
|
||||
sendUseCase: SendUseCase,
|
||||
crossChainTransfersUseCase: CrossChainTransfersUseCase,
|
||||
crossChainValidationProvider: CrossChainValidationSystemProvider
|
||||
) = SendInteractor(
|
||||
assetSourceRegistry,
|
||||
crossChainTransactor,
|
||||
crossChainTransfersRepository,
|
||||
parachainInfoRepository,
|
||||
crossChainTransfersUseCase,
|
||||
extrinsicService,
|
||||
sendUseCase,
|
||||
crossChainValidationProvider
|
||||
)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_assets.di.modules.deeplinks
|
||||
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
|
||||
class AssetDeepLinks(val deepLinkHandlers: List<DeepLinkHandler>)
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package io.novafoundation.nova.feature_assets.di.modules.deeplinks
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink.AssetDetailsDeepLinkHandler
|
||||
import io.novafoundation.nova.feature_assets.presentation.novacard.common.NovaCardRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.novacard.overview.deeplink.NovaCardDeepLinkHandler
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class DeepLinkModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinkConfigurator(
|
||||
linkBuilderFactory: LinkBuilderFactory
|
||||
): AssetDetailsDeepLinkConfigurator {
|
||||
return AssetDetailsDeepLinkConfigurator(linkBuilderFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetDetailsDeepLinkHandler(
|
||||
router: AssetsRouter,
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
automaticInteractionGate: AutomaticInteractionGate,
|
||||
assetDetailsDeepLinkConfigurator: AssetDetailsDeepLinkConfigurator
|
||||
): AssetDetailsDeepLinkHandler {
|
||||
return AssetDetailsDeepLinkHandler(
|
||||
router,
|
||||
accountRepository,
|
||||
chainRegistry,
|
||||
automaticInteractionGate,
|
||||
assetDetailsDeepLinkConfigurator
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNovaCardDeepLinkHandler(
|
||||
router: AssetsRouter,
|
||||
automaticInteractionGate: AutomaticInteractionGate,
|
||||
novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin
|
||||
): NovaCardDeepLinkHandler {
|
||||
return NovaCardDeepLinkHandler(
|
||||
router,
|
||||
automaticInteractionGate,
|
||||
novaCardRestrictionCheckMixin
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinks(
|
||||
assetDetails: AssetDetailsDeepLinkHandler,
|
||||
novaCardDeepLink: NovaCardDeepLinkHandler
|
||||
): AssetDeepLinks {
|
||||
return AssetDeepLinks(listOf(assetDetails, novaCardDeepLink))
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.novafoundation.nova.feature_assets.domain
|
||||
|
||||
import io.novafoundation.nova.common.data.model.DataPage
|
||||
import io.novafoundation.nova.common.data.model.PageOffset
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork
|
||||
import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance
|
||||
import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftSyncTrigger
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.OperationsPageChange
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletInteractor {
|
||||
|
||||
fun isFiltersEnabledFlow(): Flow<Boolean>
|
||||
|
||||
fun filterAssets(assetsFlow: Flow<List<Asset>>): Flow<List<Asset>>
|
||||
|
||||
fun assetsFlow(): Flow<List<Asset>>
|
||||
|
||||
suspend fun syncAssetsRates(currency: Currency)
|
||||
|
||||
fun nftSyncTrigger(): Flow<NftSyncTrigger>
|
||||
|
||||
suspend fun syncAllNfts(metaAccount: MetaAccount)
|
||||
|
||||
suspend fun syncChainNfts(metaAccount: MetaAccount, chain: Chain)
|
||||
|
||||
fun chainFlow(chainId: ChainId): Flow<Chain>
|
||||
|
||||
fun assetFlow(chainId: ChainId, chainAssetId: Int): Flow<Asset>
|
||||
|
||||
fun assetFlow(chainAsset: Chain.Asset): Flow<Asset>
|
||||
|
||||
fun commissionAssetFlow(chainId: ChainId): Flow<Asset>
|
||||
|
||||
fun commissionAssetFlow(chain: Chain): Flow<Asset>
|
||||
|
||||
fun operationsFirstPageFlow(chainId: ChainId, chainAssetId: Int): Flow<OperationsPageChange>
|
||||
|
||||
suspend fun syncOperationsFirstPage(
|
||||
chainId: ChainId,
|
||||
chainAssetId: Int,
|
||||
pageSize: Int,
|
||||
filters: Set<TransactionFilter>,
|
||||
): Result<*>
|
||||
|
||||
suspend fun getOperations(
|
||||
chainId: ChainId,
|
||||
chainAssetId: Int,
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>
|
||||
): Result<DataPage<Operation>>
|
||||
|
||||
suspend fun groupAssetsByNetwork(
|
||||
assets: List<Asset>,
|
||||
externalBalances: List<ExternalBalance>
|
||||
): Map<NetworkAssetGroup, List<AssetWithOffChainBalance>>
|
||||
|
||||
suspend fun groupAssetsByToken(
|
||||
assets: List<Asset>,
|
||||
externalBalances: List<ExternalBalance>
|
||||
): Map<TokenAssetGroup, List<AssetWithNetwork>>
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package io.novafoundation.nova.feature_assets.domain
|
||||
|
||||
import io.novafoundation.nova.common.data.model.DataPage
|
||||
import io.novafoundation.nova.common.data.model.PageOffset
|
||||
import io.novafoundation.nova.common.utils.applyFilters
|
||||
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_assets.data.repository.TransactionHistoryRepository
|
||||
import io.novafoundation.nova.feature_assets.data.repository.assetFilters.AssetFiltersRepository
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork
|
||||
import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance
|
||||
import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByNetwork
|
||||
import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByToken
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
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_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.OperationsPageChange
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset
|
||||
import io.novafoundation.nova.runtime.ext.commissionAsset
|
||||
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.multiNetwork.chainWithAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainByIdFlow
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChains
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class WalletInteractorImpl(
|
||||
private val walletRepository: WalletRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val assetFiltersRepository: AssetFiltersRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val nftRepository: NftRepository,
|
||||
private val transactionHistoryRepository: TransactionHistoryRepository,
|
||||
private val currencyRepository: CurrencyRepository
|
||||
) : WalletInteractor {
|
||||
|
||||
override fun isFiltersEnabledFlow(): Flow<Boolean> {
|
||||
return assetFiltersRepository.assetFiltersFlow()
|
||||
.map { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
override fun filterAssets(assetsFlow: Flow<List<Asset>>): Flow<List<Asset>> {
|
||||
return combine(assetsFlow, assetFiltersRepository.assetFiltersFlow()) { assets, filters ->
|
||||
assets.applyFilters(filters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun assetsFlow(): Flow<List<Asset>> {
|
||||
val assetsFlow = accountRepository.selectedMetaAccountFlow()
|
||||
.flatMapLatest { walletRepository.syncedAssetsFlow(it.id) }
|
||||
|
||||
val enabledChains = chainRegistry.enabledChainByIdFlow()
|
||||
|
||||
return combine(assetsFlow, enabledChains) { assets, chainsById ->
|
||||
assets.filter { chainsById.containsKey(it.token.configuration.chainId) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun syncAssetsRates(currency: Currency) {
|
||||
runCatching {
|
||||
walletRepository.syncAssetsRates(currency)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nftSyncTrigger(): Flow<NftSyncTrigger> {
|
||||
return nftRepository.initialNftSyncTrigger()
|
||||
}
|
||||
|
||||
override suspend fun syncAllNfts(metaAccount: MetaAccount) {
|
||||
nftRepository.initialNftSync(metaAccount, forceOverwrite = false)
|
||||
}
|
||||
|
||||
override suspend fun syncChainNfts(metaAccount: MetaAccount, chain: Chain) {
|
||||
nftRepository.initialNftSync(metaAccount, chain)
|
||||
}
|
||||
|
||||
override fun chainFlow(chainId: ChainId): Flow<Chain> {
|
||||
return chainRegistry.enabledChainByIdFlow()
|
||||
.map { it.getValue(chainId) }
|
||||
}
|
||||
|
||||
override fun assetFlow(chainId: ChainId, chainAssetId: Int): Flow<Asset> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
val (_, chainAsset) = chainRegistry.chainWithAsset(chainId, chainAssetId)
|
||||
|
||||
walletRepository.assetFlow(metaAccount.id, chainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun assetFlow(chainAsset: Chain.Asset): Flow<Asset> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
walletRepository.assetFlow(metaAccount.id, chainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun commissionAssetFlow(chainId: ChainId): Flow<Asset> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
|
||||
walletRepository.assetFlow(metaAccount.id, chain.commissionAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun commissionAssetFlow(chain: Chain): Flow<Asset> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
walletRepository.assetFlow(metaAccount.id, chain.commissionAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun operationsFirstPageFlow(chainId: ChainId, chainAssetId: Int): Flow<OperationsPageChange> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
val (chain, chainAsset) = chainRegistry.chainWithAsset(chainId, chainAssetId)
|
||||
val accountId = metaAccount.accountIdIn(chain)!!
|
||||
val currency = currencyRepository.getSelectedCurrency()
|
||||
transactionHistoryRepository.operationsFirstPageFlow(accountId, chain, chainAsset, currency)
|
||||
.withIndex()
|
||||
.map { (index, cursorPage) -> OperationsPageChange(cursorPage, accountChanged = index == 0) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun syncOperationsFirstPage(
|
||||
chainId: ChainId,
|
||||
chainAssetId: Int,
|
||||
pageSize: Int,
|
||||
filters: Set<TransactionFilter>,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
val (chain, chainAsset) = chainRegistry.chainWithAsset(chainId, chainAssetId)
|
||||
val accountId = metaAccount.accountIdIn(chain)!!
|
||||
val currency = currencyRepository.getSelectedCurrency()
|
||||
|
||||
transactionHistoryRepository.syncOperationsFirstPage(pageSize, filters, accountId, chain, chainAsset, currency)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getOperations(
|
||||
chainId: ChainId,
|
||||
chainAssetId: Int,
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
): Result<DataPage<Operation>> {
|
||||
return runCatching {
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
val (chain, chainAsset) = chainRegistry.chainWithAsset(chainId, chainAssetId)
|
||||
val accountId = metaAccount.requireAccountIdIn(chain)
|
||||
val currency = currencyRepository.getSelectedCurrency()
|
||||
|
||||
transactionHistoryRepository.getOperations(
|
||||
pageSize = pageSize,
|
||||
pageOffset = pageOffset,
|
||||
filters = filters,
|
||||
accountId = accountId,
|
||||
chain = chain,
|
||||
chainAsset = chainAsset,
|
||||
currency = currency
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun groupAssetsByNetwork(
|
||||
assets: List<Asset>,
|
||||
externalBalances: List<ExternalBalance>
|
||||
): Map<NetworkAssetGroup, List<AssetWithOffChainBalance>> {
|
||||
val chains = chainRegistry.enabledChainByIdFlow().first()
|
||||
|
||||
return groupAndSortAssetsByNetwork(assets, externalBalances.aggregatedBalanceByAsset(), chains)
|
||||
}
|
||||
|
||||
override suspend fun groupAssetsByToken(
|
||||
assets: List<Asset>,
|
||||
externalBalances: List<ExternalBalance>
|
||||
): Map<TokenAssetGroup, List<AssetWithNetwork>> {
|
||||
val chains = chainRegistry.enabledChainByIdFlow().first()
|
||||
|
||||
return groupAndSortAssetsByToken(assets, externalBalances.aggregatedBalanceByAsset(), chains)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
|
||||
interface ExternalBalancesInteractor {
|
||||
|
||||
fun observeExternalBalances(): Flow<List<ExternalBalance>>
|
||||
|
||||
fun observeExternalBalances(assetId: FullChainAssetId): Flow<List<ExternalBalance>>
|
||||
}
|
||||
|
||||
class RealExternalBalancesInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val externalBalanceRepository: ExternalBalanceRepository,
|
||||
) : ExternalBalancesInteractor {
|
||||
|
||||
override fun observeExternalBalances(): Flow<List<ExternalBalance>> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
externalBalanceRepository.observeAccountExternalBalances(metaAccount.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeExternalBalances(assetId: FullChainAssetId): Flow<List<ExternalBalance>> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
externalBalanceRepository.observeAccountChainExternalBalances(metaAccount.id, assetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.filters
|
||||
|
||||
import io.novafoundation.nova.common.utils.NamedFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
|
||||
typealias AssetFilter = NamedFilter<Asset>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.filters
|
||||
|
||||
import io.novafoundation.nova.feature_assets.data.repository.assetFilters.AssetFiltersRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class AssetFiltersInteractor(
|
||||
private val assetFiltersRepository: AssetFiltersRepository
|
||||
) {
|
||||
|
||||
val allFilters = assetFiltersRepository.allFilters
|
||||
|
||||
fun updateFilters(filters: List<AssetFilter>) {
|
||||
assetFiltersRepository.updateAssetFilters(filters)
|
||||
}
|
||||
|
||||
suspend fun currentFilters(): Set<AssetFilter> = assetFiltersRepository.assetFiltersFlow().first().toSet()
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.filters
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import java.math.BigDecimal
|
||||
|
||||
object NonZeroBalanceFilter : AssetFilter {
|
||||
|
||||
override val name: String = "NonZeroBalance"
|
||||
|
||||
override fun shouldInclude(model: Asset) = model.total > BigDecimal.ZERO
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.list
|
||||
|
||||
import io.novafoundation.nova.common.data.model.AssetViewMode
|
||||
import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository
|
||||
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.model.isFullySynced
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val PREVIEW_COUNT = 3
|
||||
|
||||
class AssetsListInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val nftRepository: NftRepository,
|
||||
private val assetsViewModeRepository: AssetsViewModeRepository
|
||||
) {
|
||||
|
||||
fun assetsViewModeFlow() = assetsViewModeRepository.assetsViewModeFlow()
|
||||
|
||||
suspend fun setAssetViewMode(assetViewModel: AssetViewMode) {
|
||||
assetsViewModeRepository.setAssetsViewMode(assetViewModel)
|
||||
}
|
||||
|
||||
suspend fun fullSyncNft(nft: Nft) = nftRepository.fullNftSync(nft)
|
||||
|
||||
fun observeNftPreviews(): Flow<NftPreviews> {
|
||||
return accountRepository.selectedMetaAccountFlow()
|
||||
.flatMapLatest(nftRepository::allNftFlow)
|
||||
.map { nfts ->
|
||||
NftPreviews(
|
||||
totalNftsCount = nfts.size,
|
||||
nftPreviews = nfts.sortedBy { it.isFullySynced }.take(PREVIEW_COUNT)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.list
|
||||
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
|
||||
class NftPreviews(
|
||||
val totalNftsCount: Int,
|
||||
val nftPreviews: List<Nft>
|
||||
)
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.models
|
||||
|
||||
import io.novafoundation.nova.common.utils.MultiMapList
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance
|
||||
import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup
|
||||
|
||||
sealed interface AssetsByViewModeResult {
|
||||
|
||||
class ByNetworks(val assets: MultiMapList<NetworkAssetGroup, AssetWithOffChainBalance>) : AssetsByViewModeResult
|
||||
|
||||
class ByTokens(val tokens: MultiMapList<TokenAssetGroup, AssetWithNetwork>) : AssetsByViewModeResult
|
||||
}
|
||||
|
||||
fun AssetsByViewModeResult.groupList(): List<Any> {
|
||||
return when (this) {
|
||||
is AssetsByViewModeResult.ByNetworks -> assets.keys.toList()
|
||||
is AssetsByViewModeResult.ByTokens -> tokens.keys.toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun MultiMapList<NetworkAssetGroup, AssetWithOffChainBalance>.byNetworks(): AssetsByViewModeResult {
|
||||
return AssetsByViewModeResult.ByNetworks(this)
|
||||
}
|
||||
|
||||
fun MultiMapList<TokenAssetGroup, AssetWithNetwork>.byTokens(): AssetsByViewModeResult {
|
||||
return AssetsByViewModeResult.ByTokens(this)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.search
|
||||
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface AssetSearchInteractorFactory {
|
||||
|
||||
fun createByAssetViewMode(): AssetSearchInteractor
|
||||
}
|
||||
|
||||
typealias AssetSearchFilter = suspend (Asset) -> Boolean
|
||||
|
||||
interface AssetSearchInteractor {
|
||||
|
||||
fun tradeAssetSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
tradeType: TradeTokenRegistry.TradeType
|
||||
): Flow<AssetsByViewModeResult>
|
||||
|
||||
fun sendAssetSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult>
|
||||
|
||||
fun searchSwapAssetsFlow(
|
||||
forAsset: FullChainAssetId?,
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<AssetsByViewModeResult>
|
||||
|
||||
fun searchReceiveAssetsFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult>
|
||||
|
||||
fun giftAssetsSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<AssetsByViewModeResult>
|
||||
|
||||
fun searchAssetsFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult>
|
||||
}
|
||||
|
||||
fun Flow<Set<FullChainAssetId>>.mapToAssetSearchFilter(): Flow<AssetSearchFilter> {
|
||||
return map { assetsSet ->
|
||||
{ asset ->
|
||||
val chainAsset = asset.token.configuration
|
||||
|
||||
chainAsset.fullId in assetsSet
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.search
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOfAll
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_assets.domain.common.searchTokens
|
||||
import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainsById
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
|
||||
class AssetSearchUseCase(
|
||||
private val walletRepository: WalletRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val swapService: SwapService
|
||||
) {
|
||||
|
||||
fun filteredAssetFlow(filterFlow: Flow<AssetSearchFilter?>): Flow<List<Asset>> {
|
||||
val assetsFlow = accountRepository.selectedMetaAccountFlow()
|
||||
.flatMapLatest { walletRepository.syncedAssetsFlow(it.id) }
|
||||
|
||||
return combine(assetsFlow, filterFlow) { assets, filter ->
|
||||
if (filter == null) {
|
||||
assets
|
||||
} else {
|
||||
assets.filter { filter(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filterAssetsByQuery(query: String, assets: List<Asset>, chainsById: ChainsById): List<Asset> {
|
||||
return assets.searchTokens(
|
||||
query = query,
|
||||
chainsById = chainsById,
|
||||
tokenSymbol = { it.token.configuration.symbol.value },
|
||||
relevantToChains = { asset, chainIds -> asset.token.configuration.chainId in chainIds }
|
||||
)
|
||||
}
|
||||
|
||||
fun getAvailableSwapAssets(asset: FullChainAssetId?, coroutineScope: CoroutineScope): Flow<Set<FullChainAssetId>> {
|
||||
return flowOfAll {
|
||||
val chainAsset = asset?.let { chainRegistry.asset(it) }
|
||||
|
||||
if (chainAsset == null) {
|
||||
swapService.assetsAvailableForSwap(coroutineScope)
|
||||
} else {
|
||||
swapService.availableSwapDirectionsFor(chainAsset, coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.search
|
||||
|
||||
import io.novafoundation.nova.common.data.model.AssetViewMode
|
||||
import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
class AssetViewModeAssetSearchInteractorFactory(
|
||||
private val assetViewModeRepository: AssetsViewModeRepository,
|
||||
private val assetSearchUseCase: AssetSearchUseCase,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val tradeTokenRegistry: TradeTokenRegistry,
|
||||
private val availableGiftAssetsUseCase: AvailableGiftAssetsUseCase
|
||||
) : AssetSearchInteractorFactory {
|
||||
|
||||
override fun createByAssetViewMode(): AssetSearchInteractor {
|
||||
return when (assetViewModeRepository.getAssetViewMode()) {
|
||||
AssetViewMode.TOKENS -> ByTokensAssetSearchInteractor(assetSearchUseCase, chainRegistry, tradeTokenRegistry, availableGiftAssetsUseCase)
|
||||
AssetViewMode.NETWORKS -> ByNetworkAssetSearchInteractor(assetSearchUseCase, chainRegistry, tradeTokenRegistry, availableGiftAssetsUseCase)
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.search
|
||||
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance
|
||||
import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.getAssetBaseComparator
|
||||
import io.novafoundation.nova.feature_assets.domain.common.getAssetGroupBaseComparator
|
||||
import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByNetwork
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainById
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ByNetworkAssetSearchInteractor(
|
||||
private val assetSearchUseCase: AssetSearchUseCase,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val tradeTokenRegistry: TradeTokenRegistry,
|
||||
private val availableGiftAssetsUseCase: AvailableGiftAssetsUseCase
|
||||
) : AssetSearchInteractor {
|
||||
|
||||
override fun tradeAssetSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
tradeType: TradeTokenRegistry.TradeType
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filter = { asset: Asset -> tradeTokenRegistry.hasProvider(asset.token.configuration, tradeType) }
|
||||
|
||||
return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filter = filter)
|
||||
}
|
||||
|
||||
override fun sendAssetSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filter = { asset: Asset -> asset.transferableInPlanks.isPositive() }
|
||||
|
||||
return searchAssetsByNetworksInternalFlow(
|
||||
queryFlow,
|
||||
externalBalancesFlow,
|
||||
assetGroupComparator = getAssetGroupBaseComparator { it.groupTransferableBalanceFiat },
|
||||
assetsComparator = getAssetBaseComparator { it.balanceWithOffchain.transferable.fiat },
|
||||
filter = filter
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchSwapAssetsFlow(
|
||||
forAsset: FullChainAssetId?,
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filterFlow = assetSearchUseCase.getAvailableSwapAssets(forAsset, coroutineScope).mapToAssetSearchFilter()
|
||||
return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filterFlow = filterFlow)
|
||||
}
|
||||
|
||||
override fun searchReceiveAssetsFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filter = null)
|
||||
}
|
||||
|
||||
override fun giftAssetsSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filterFlow = availableGiftAssetsUseCase.getAvailableGiftAssets(coroutineScope).mapToAssetSearchFilter()
|
||||
return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filterFlow = filterFlow)
|
||||
}
|
||||
|
||||
override fun searchAssetsFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, filter = null)
|
||||
}
|
||||
|
||||
private fun ByNetworkAssetSearchInteractor.searchAssetsByNetworksInternalFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
assetGroupComparator: Comparator<NetworkAssetGroup> = getAssetGroupBaseComparator(),
|
||||
assetsComparator: Comparator<AssetWithOffChainBalance> = getAssetBaseComparator(),
|
||||
filter: AssetSearchFilter?,
|
||||
): Flow<AssetsByViewModeResult.ByNetworks> {
|
||||
val filterFlow = flowOf(filter)
|
||||
|
||||
return searchAssetsByNetworksInternalFlow(queryFlow, externalBalancesFlow, assetGroupComparator, assetsComparator, filterFlow)
|
||||
}
|
||||
|
||||
private fun searchAssetsByNetworksInternalFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
assetGroupComparator: Comparator<NetworkAssetGroup> = getAssetGroupBaseComparator(),
|
||||
assetsComparator: Comparator<AssetWithOffChainBalance> = getAssetBaseComparator(),
|
||||
filterFlow: Flow<AssetSearchFilter?>,
|
||||
): Flow<AssetsByViewModeResult.ByNetworks> {
|
||||
val assetsFlow = assetSearchUseCase.filteredAssetFlow(filterFlow)
|
||||
|
||||
val aggregatedExternalBalances = externalBalancesFlow.map { it.aggregatedBalanceByAsset() }
|
||||
|
||||
return combine(assetsFlow, aggregatedExternalBalances, queryFlow) { assets, externalBalances, query ->
|
||||
val chainsById = chainRegistry.enabledChainById()
|
||||
val filtered = assetSearchUseCase.filterAssetsByQuery(query, assets, chainsById)
|
||||
|
||||
val assetGroups = groupAndSortAssetsByNetwork(filtered, externalBalances, chainsById, assetGroupComparator, assetsComparator)
|
||||
AssetsByViewModeResult.ByNetworks(assetGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.assets.search
|
||||
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork
|
||||
import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetBaseComparator
|
||||
import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetGroupBaseComparator
|
||||
import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByToken
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainById
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ByTokensAssetSearchInteractor(
|
||||
private val assetSearchUseCase: AssetSearchUseCase,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val tradeTokenRegistry: TradeTokenRegistry,
|
||||
private val availableGiftAssetsUseCase: AvailableGiftAssetsUseCase
|
||||
) : AssetSearchInteractor {
|
||||
|
||||
override fun tradeAssetSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
tradeType: TradeTokenRegistry.TradeType
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filter = { asset: Asset -> tradeTokenRegistry.hasProvider(asset.token.configuration, tradeType) }
|
||||
|
||||
return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filter = filter)
|
||||
}
|
||||
|
||||
override fun sendAssetSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filter = { asset: Asset -> asset.transferableInPlanks.isPositive() }
|
||||
|
||||
return searchAssetsByTokensInternalFlow(
|
||||
queryFlow,
|
||||
externalBalancesFlow,
|
||||
assetGroupComparator = getTokenAssetGroupBaseComparator { it.groupBalance.transferable.fiat },
|
||||
assetsComparator = getTokenAssetBaseComparator { it.balanceWithOffChain.transferable.fiat },
|
||||
filter = filter
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchSwapAssetsFlow(
|
||||
forAsset: FullChainAssetId?,
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filterFlow = assetSearchUseCase.getAvailableSwapAssets(forAsset, coroutineScope).mapToAssetSearchFilter()
|
||||
return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filterFlow = filterFlow)
|
||||
}
|
||||
|
||||
override fun searchReceiveAssetsFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filter = null)
|
||||
}
|
||||
|
||||
override fun giftAssetsSearch(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<AssetsByViewModeResult> {
|
||||
val filterFlow = availableGiftAssetsUseCase.getAvailableGiftAssets(coroutineScope).mapToAssetSearchFilter()
|
||||
return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filterFlow = filterFlow)
|
||||
}
|
||||
|
||||
override fun searchAssetsFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<AssetsByViewModeResult.ByTokens> {
|
||||
return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, filter = null)
|
||||
}
|
||||
|
||||
private fun searchAssetsByTokensInternalFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
assetGroupComparator: Comparator<TokenAssetGroup> = getTokenAssetGroupBaseComparator(),
|
||||
assetsComparator: Comparator<AssetWithNetwork> = getTokenAssetBaseComparator(),
|
||||
filter: AssetSearchFilter?,
|
||||
): Flow<AssetsByViewModeResult.ByTokens> {
|
||||
val filterFlow = flowOf(filter)
|
||||
|
||||
return searchAssetsByTokensInternalFlow(queryFlow, externalBalancesFlow, assetGroupComparator, assetsComparator, filterFlow)
|
||||
}
|
||||
|
||||
private fun searchAssetsByTokensInternalFlow(
|
||||
queryFlow: Flow<String>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
assetGroupComparator: Comparator<TokenAssetGroup> = getTokenAssetGroupBaseComparator(),
|
||||
assetsComparator: Comparator<AssetWithNetwork> = getTokenAssetBaseComparator(),
|
||||
filterFlow: Flow<AssetSearchFilter?>,
|
||||
): Flow<AssetsByViewModeResult.ByTokens> {
|
||||
val assetsFlow = assetSearchUseCase.filteredAssetFlow(filterFlow)
|
||||
|
||||
val aggregatedExternalBalances = externalBalancesFlow.map { it.aggregatedBalanceByAsset() }
|
||||
|
||||
return combine(assetsFlow, aggregatedExternalBalances, queryFlow) { assets, externalBalances, query ->
|
||||
val chainsById = chainRegistry.enabledChainById()
|
||||
val filtered = assetSearchUseCase.filterAssetsByQuery(query, assets, chainsById)
|
||||
|
||||
val assetGroups = groupAndSortAssetsByToken(filtered, externalBalances, chainsById, assetGroupComparator, assetsComparator)
|
||||
|
||||
AssetsByViewModeResult.ByTokens(assetGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.breakdown
|
||||
|
||||
import io.novafoundation.nova.common.utils.formatting.ABBREVIATED_SCALE
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.utils.percentage
|
||||
import io.novafoundation.nova.common.utils.sumByBigInteger
|
||||
import io.novafoundation.nova.common.utils.unite
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdown.PercentageAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceBreakdownIds
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.unlabeledReserves
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.balanceId
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
class BalanceBreakdown(
|
||||
val total: BigDecimal,
|
||||
val transferableTotal: PercentageAmount,
|
||||
val locksTotal: PercentageAmount,
|
||||
val breakdown: List<BreakdownItem>
|
||||
) {
|
||||
companion object {
|
||||
fun empty(): BalanceBreakdown {
|
||||
return BalanceBreakdown(
|
||||
total = BigDecimal.ZERO,
|
||||
transferableTotal = PercentageAmount(amount = BigDecimal.ZERO, percentage = BigDecimal.ZERO),
|
||||
locksTotal = PercentageAmount(amount = BigDecimal.ZERO, percentage = BigDecimal.ZERO),
|
||||
breakdown = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class PercentageAmount(val amount: BigDecimal, val percentage: BigDecimal)
|
||||
|
||||
class BreakdownItem(val id: String, val token: Token, val amountInPlanks: BigInteger) {
|
||||
val tokenAmount by lazy { token.amountFromPlanks(amountInPlanks) }
|
||||
|
||||
val fiatAmount by lazy { token.amountToFiat(tokenAmount) }
|
||||
}
|
||||
}
|
||||
|
||||
class BalanceBreakdownInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val balanceLocksRepository: BalanceLocksRepository,
|
||||
private val balanceHoldsRepository: BalanceHoldsRepository,
|
||||
) {
|
||||
|
||||
private class TotalAmount(
|
||||
val totalFiat: BigDecimal,
|
||||
val transferableFiat: BigDecimal,
|
||||
val locksFiat: BigDecimal,
|
||||
)
|
||||
|
||||
fun balanceBreakdownFlow(
|
||||
assetsFlow: Flow<List<Asset>>,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>
|
||||
): Flow<BalanceBreakdown> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
unite(
|
||||
assetsFlow,
|
||||
balanceLocksRepository.observeLocksForMetaAccount(metaAccount),
|
||||
balanceHoldsRepository.observeHoldsForMetaAccount(metaAccount.id),
|
||||
externalBalancesFlow
|
||||
) { assets, locks, holds, externalBalances ->
|
||||
if (assets == null) {
|
||||
BalanceBreakdown.empty()
|
||||
} else {
|
||||
val assetsByChainId = assets.associateBy { it.token.configuration.fullId }
|
||||
val locksItems = mapLocks(assetsByChainId, locks.orEmpty())
|
||||
val holdsItems = mapHolds(assetsByChainId, holds.orEmpty())
|
||||
val externalBalancesItems = mapExternalBalances(assetsByChainId, externalBalances.orEmpty())
|
||||
|
||||
val holdsByAsset = holds.orEmpty()
|
||||
.groupBy { it.chainAsset.fullId }
|
||||
.mapValues { (_, holds) -> holds.sumByBigInteger { it.amountInPlanks } }
|
||||
val reserved = getReservedBreakdown(assets, holdsByAsset)
|
||||
|
||||
val breakdown = locksItems + holdsItems + externalBalancesItems + reserved
|
||||
|
||||
val totalAmount = calculateTotalBalance(assets, externalBalancesItems)
|
||||
val (transferablePercentage, locksPercentage) = percentage(
|
||||
scale = ABBREVIATED_SCALE,
|
||||
totalAmount.transferableFiat,
|
||||
totalAmount.locksFiat
|
||||
)
|
||||
BalanceBreakdown(
|
||||
total = totalAmount.totalFiat,
|
||||
transferableTotal = PercentageAmount(totalAmount.transferableFiat, transferablePercentage),
|
||||
locksTotal = PercentageAmount(totalAmount.locksFiat, locksPercentage),
|
||||
breakdown = breakdown.sortedByDescending { it.fiatAmount }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapLocks(
|
||||
assetsByChainId: Map<FullChainAssetId, Asset>,
|
||||
locks: List<BalanceLock>
|
||||
): List<BalanceBreakdown.BreakdownItem> {
|
||||
return locks.mapNotNull { lock ->
|
||||
assetsByChainId[lock.chainAsset.fullId]?.let { asset ->
|
||||
BalanceBreakdown.BreakdownItem(
|
||||
id = lock.id.value,
|
||||
token = asset.token,
|
||||
amountInPlanks = lock.amountInPlanks,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapHolds(
|
||||
assetsByChainId: Map<FullChainAssetId, Asset>,
|
||||
holds: List<BalanceHold>
|
||||
): List<BalanceBreakdown.BreakdownItem> {
|
||||
return holds.mapNotNull { hold ->
|
||||
assetsByChainId[hold.chainAsset.fullId]?.let { asset ->
|
||||
BalanceBreakdown.BreakdownItem(
|
||||
id = hold.identifier,
|
||||
token = asset.token,
|
||||
amountInPlanks = hold.amountInPlanks,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapExternalBalances(
|
||||
assetsByChainId: Map<FullChainAssetId, Asset>,
|
||||
externalBalances: List<ExternalBalance>
|
||||
): List<BalanceBreakdown.BreakdownItem> {
|
||||
return externalBalances.mapNotNull { externalBalance ->
|
||||
assetsByChainId[externalBalance.chainAssetId]?.let { asset ->
|
||||
BalanceBreakdown.BreakdownItem(
|
||||
id = externalBalance.type.balanceId,
|
||||
token = asset.token,
|
||||
amountInPlanks = externalBalance.amount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateTotalBalance(
|
||||
assets: List<Asset>,
|
||||
externalBalancesItems: List<BalanceBreakdown.BreakdownItem>
|
||||
): TotalAmount {
|
||||
val externalBalancesTotal = externalBalancesItems.sumOf { it.fiatAmount }
|
||||
var total = externalBalancesTotal
|
||||
var transferable = BigDecimal.ZERO
|
||||
var locks = externalBalancesTotal
|
||||
|
||||
assets.forEach { asset ->
|
||||
total += asset.token.amountToFiat(asset.total)
|
||||
transferable += asset.token.amountToFiat(asset.transferable)
|
||||
locks += asset.token.amountToFiat(asset.locked)
|
||||
}
|
||||
|
||||
return TotalAmount(total, transferable, locks)
|
||||
}
|
||||
|
||||
private fun getReservedBreakdown(assets: List<Asset>, holds: Map<FullChainAssetId, Balance>): List<BalanceBreakdown.BreakdownItem> {
|
||||
return assets
|
||||
.filter { it.reservedInPlanks > BigInteger.ZERO }
|
||||
.mapNotNull {
|
||||
val labeledReserves = holds[it.token.configuration.fullId].orZero()
|
||||
val unlabeledReserves = it.unlabeledReserves(labeledReserves)
|
||||
if (unlabeledReserves <= BigInteger.ZERO) return@mapNotNull null
|
||||
|
||||
BalanceBreakdown.BreakdownItem(
|
||||
id = BalanceBreakdownIds.RESERVED,
|
||||
token = it.token,
|
||||
amountInPlanks = unlabeledReserves
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.common
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
class AssetBalance(
|
||||
val total: Amount,
|
||||
val transferable: Amount
|
||||
) {
|
||||
|
||||
class Amount(
|
||||
val amount: BigDecimal,
|
||||
val fiat: BigDecimal
|
||||
) {
|
||||
|
||||
operator fun plus(other: Amount): Amount {
|
||||
return Amount(
|
||||
amount + other.amount,
|
||||
fiat + other.fiat
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ZERO = AssetBalance(Amount(BigDecimal.ZERO, BigDecimal.ZERO), Amount(BigDecimal.ZERO, BigDecimal.ZERO))
|
||||
}
|
||||
|
||||
operator fun plus(other: AssetBalance): AssetBalance {
|
||||
return AssetBalance(
|
||||
total + other.total,
|
||||
transferable + other.transferable
|
||||
)
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.common
|
||||
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.utils.sumByBigDecimal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.ext.defaultComparatorFrom
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import java.math.BigDecimal
|
||||
|
||||
class NetworkAssetGroup(
|
||||
val chain: Chain,
|
||||
val groupTotalBalanceFiat: BigDecimal,
|
||||
val groupTransferableBalanceFiat: BigDecimal,
|
||||
val zeroBalance: Boolean
|
||||
)
|
||||
|
||||
class AssetWithOffChainBalance(
|
||||
val asset: Asset,
|
||||
val balanceWithOffchain: AssetBalance,
|
||||
)
|
||||
|
||||
fun groupAndSortAssetsByNetwork(
|
||||
assets: List<Asset>,
|
||||
externalBalances: Map<FullChainAssetId, Balance>,
|
||||
chainsById: Map<String, Chain>,
|
||||
assetGroupComparator: Comparator<NetworkAssetGroup> = getAssetGroupBaseComparator(),
|
||||
assetComparator: Comparator<AssetWithOffChainBalance> = getAssetBaseComparator()
|
||||
): Map<NetworkAssetGroup, List<AssetWithOffChainBalance>> {
|
||||
return assets
|
||||
.map { asset -> AssetWithOffChainBalance(asset, asset.totalWithOffChain(externalBalances)) }
|
||||
.filter { chainsById.containsKey(it.asset.token.configuration.chainId) }
|
||||
.groupBy { chainsById.getValue(it.asset.token.configuration.chainId) }
|
||||
.mapValues { (_, assets) -> assets.sortedWith(assetComparator) }
|
||||
.mapKeys { (chain, assets) ->
|
||||
NetworkAssetGroup(
|
||||
chain = chain,
|
||||
groupTotalBalanceFiat = assets.sumByBigDecimal { it.balanceWithOffchain.total.fiat },
|
||||
groupTransferableBalanceFiat = assets.sumByBigDecimal { it.balanceWithOffchain.transferable.fiat },
|
||||
zeroBalance = assets.any { it.balanceWithOffchain.total.amount > BigDecimal.ZERO }
|
||||
)
|
||||
}.toSortedMap(assetGroupComparator)
|
||||
}
|
||||
|
||||
fun getAssetBaseComparator(
|
||||
comparing: (AssetWithOffChainBalance) -> Comparable<*> = { it.balanceWithOffchain.total.fiat }
|
||||
): Comparator<AssetWithOffChainBalance> {
|
||||
return compareByDescending(comparing)
|
||||
.thenByDescending { it.balanceWithOffchain.total.amount }
|
||||
.thenByDescending { it.asset.token.configuration.isUtilityAsset } // utility assets first
|
||||
.thenBy { it.asset.token.configuration.symbol.value }
|
||||
}
|
||||
|
||||
fun getAssetGroupBaseComparator(
|
||||
comparing: (NetworkAssetGroup) -> Comparable<*> = NetworkAssetGroup::groupTotalBalanceFiat
|
||||
): Comparator<NetworkAssetGroup> {
|
||||
return compareByDescending(comparing)
|
||||
.thenByDescending { it.zeroBalance } // non-zero balances first
|
||||
.then(Chain.defaultComparatorFrom(NetworkAssetGroup::chain))
|
||||
}
|
||||
|
||||
fun Asset.totalWithOffChain(externalBalances: Map<FullChainAssetId, Balance>): AssetBalance {
|
||||
val onChainTotal = total
|
||||
val offChainTotal = externalBalances[token.configuration.fullId]
|
||||
?.let(token::amountFromPlanks)
|
||||
.orZero()
|
||||
|
||||
val overallTotal = onChainTotal + offChainTotal
|
||||
val overallFiat = token.amountToFiat(overallTotal)
|
||||
|
||||
return AssetBalance(
|
||||
AssetBalance.Amount(overallTotal, overallFiat),
|
||||
AssetBalance.Amount(transferable, token.amountToFiat(transferable))
|
||||
)
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.common
|
||||
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.runtime.ext.defaultComparatorFrom
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.ext.normalize
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import java.math.BigDecimal
|
||||
|
||||
class TokenAssetGroup(
|
||||
val tokenInfo: TokenInfo,
|
||||
val groupBalance: AssetBalance,
|
||||
val itemsCount: Int
|
||||
) {
|
||||
|
||||
val groupId: String = tokenInfo.symbol.value
|
||||
|
||||
data class TokenInfo(
|
||||
val icon: String?,
|
||||
val token: Token
|
||||
) {
|
||||
val symbol = token.configuration.symbol.normalize()
|
||||
val currency = token.currency
|
||||
val coinRate = token.coinRate
|
||||
}
|
||||
}
|
||||
|
||||
class AssetWithNetwork(
|
||||
val chain: Chain,
|
||||
val asset: Asset,
|
||||
val balanceWithOffChain: AssetBalance,
|
||||
)
|
||||
|
||||
fun groupAndSortAssetsByToken(
|
||||
assets: List<Asset>,
|
||||
externalBalances: Map<FullChainAssetId, Balance>,
|
||||
chainsById: Map<String, Chain>,
|
||||
assetGroupComparator: Comparator<TokenAssetGroup> = getTokenAssetGroupBaseComparator(),
|
||||
assetComparator: Comparator<AssetWithNetwork> = getTokenAssetBaseComparator()
|
||||
): Map<TokenAssetGroup, List<AssetWithNetwork>> {
|
||||
return assets
|
||||
.filter { chainsById.containsKey(it.token.configuration.chainId) }
|
||||
.map { asset -> AssetWithNetwork(chainsById.getValue(asset.token.configuration.chainId), asset, asset.totalWithOffChain(externalBalances)) }
|
||||
.groupBy { mapToTokenGroup(it) }
|
||||
.mapValues { (_, assets) -> assets.sortedWith(assetComparator) }
|
||||
.mapKeys { (tokenWrapper, assets) ->
|
||||
TokenAssetGroup(
|
||||
tokenInfo = tokenWrapper.tokenInfo,
|
||||
groupBalance = assets.fold(AssetBalance.ZERO) { acc, element -> acc + element.balanceWithOffChain },
|
||||
itemsCount = assets.size
|
||||
)
|
||||
}.toSortedMap(assetGroupComparator)
|
||||
}
|
||||
|
||||
fun getTokenAssetBaseComparator(
|
||||
comparing: (AssetWithNetwork) -> Comparable<*> = { it.balanceWithOffChain.total.amount }
|
||||
): Comparator<AssetWithNetwork> {
|
||||
return compareByDescending(comparing)
|
||||
.thenByDescending { it.asset.token.configuration.isUtilityAsset } // utility assets first
|
||||
.thenBy { it.asset.token.configuration.symbol.value }
|
||||
.then(Chain.defaultComparatorFrom(AssetWithNetwork::chain))
|
||||
}
|
||||
|
||||
fun getTokenAssetGroupBaseComparator(
|
||||
comparing: (TokenAssetGroup) -> Comparable<*> = { it.groupBalance.total.fiat }
|
||||
): Comparator<TokenAssetGroup> {
|
||||
return compareByDescending(comparing)
|
||||
.thenByDescending { it.groupBalance.total.amount > BigDecimal.ZERO } // non-zero balances first
|
||||
.then(TokenSymbol.defaultComparatorFrom { it.tokenInfo.symbol })
|
||||
}
|
||||
|
||||
private fun mapToTokenGroup(it: AssetWithNetwork) = TokenGroupWrapper(
|
||||
TokenAssetGroup.TokenInfo(
|
||||
it.asset.token.configuration.icon,
|
||||
it.asset.token
|
||||
)
|
||||
)
|
||||
|
||||
// Helper class to group items by symbol only
|
||||
private class TokenGroupWrapper(val tokenInfo: TokenAssetGroup.TokenInfo) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is TokenGroupWrapper && tokenInfo.symbol == other.tokenInfo.symbol
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return tokenInfo.symbol.hashCode()
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.common
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapNotNullToSet
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainsById
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
private class SearchResult<T>(
|
||||
val item: T,
|
||||
val match: Match
|
||||
)
|
||||
|
||||
private enum class Match {
|
||||
NONE, INCLUSION, PREFIX, FULL;
|
||||
}
|
||||
|
||||
private val Match.matchFound
|
||||
get() = this != Match.NONE
|
||||
private val Match.isFullMatch
|
||||
get() = this == Match.FULL
|
||||
|
||||
// O(N * logN)
|
||||
fun <T> List<T>.searchTokens(
|
||||
query: String,
|
||||
chainsById: ChainsById,
|
||||
tokenSymbol: (T) -> String,
|
||||
relevantToChains: (T, Set<ChainId>) -> Boolean,
|
||||
): List<T> {
|
||||
if (query.isEmpty()) return this
|
||||
|
||||
val searchResultsFromTokens = map {
|
||||
SearchResult(
|
||||
item = it,
|
||||
match = tokenSymbol(it) match query
|
||||
)
|
||||
}
|
||||
val anyMatchFromTokens = searchResultsFromTokens.mapNotNull { searchResult ->
|
||||
searchResult.item.takeIf { searchResult.match.matchFound }
|
||||
}
|
||||
|
||||
val allFullMatchesFromTokens = searchResultsFromTokens.filter { it.match.isFullMatch }
|
||||
if (allFullMatchesFromTokens.isNotEmpty()) {
|
||||
return anyMatchFromTokens
|
||||
}
|
||||
|
||||
val foundChainIds = chainsById.values.mapNotNullToSet { chain ->
|
||||
chain.id.takeIf { chain.name inclusionMatch query }
|
||||
}
|
||||
|
||||
val fromChainSearch = filter { relevantToChains(it, foundChainIds) }
|
||||
|
||||
return (anyMatchFromTokens + fromChainSearch).distinct()
|
||||
}
|
||||
|
||||
private infix fun String.match(query: String): Match = when {
|
||||
fullMatch(query) -> Match.FULL
|
||||
prefixMatch(prefix = query) -> Match.PREFIX
|
||||
inclusionMatch(inclusion = query) -> Match.INCLUSION
|
||||
else -> Match.NONE
|
||||
}
|
||||
|
||||
private infix fun String.fullMatch(other: String) = lowercase() == other.lowercase()
|
||||
|
||||
private infix fun String.prefixMatch(prefix: String) = lowercase().startsWith(prefix.lowercase())
|
||||
|
||||
private infix fun String.inclusionMatch(inclusion: String) = inclusion.lowercase() in lowercase()
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.locks
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface BalanceLocksInteractor {
|
||||
|
||||
fun balanceLocksFlow(chainId: ChainId, chainAssetId: Int): Flow<List<BalanceLock>>
|
||||
|
||||
fun balanceHoldsFlow(chainId: ChainId, chainAssetId: Int): Flow<List<BalanceHold>>
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.locks
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOfAll
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class BalanceLocksInteractorImpl(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val balanceLocksRepository: BalanceLocksRepository,
|
||||
private val balanceHoldsRepository: BalanceHoldsRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
) : BalanceLocksInteractor {
|
||||
|
||||
override fun balanceLocksFlow(chainId: ChainId, chainAssetId: Int): Flow<List<BalanceLock>> {
|
||||
return flowOfAll {
|
||||
val (chain, chainAsset) = chainRegistry.chainWithAsset(chainId, chainAssetId)
|
||||
val selectedAccount = accountRepository.getSelectedMetaAccount()
|
||||
balanceLocksRepository.observeBalanceLocks(selectedAccount.id, chain, chainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun balanceHoldsFlow(chainId: ChainId, chainAssetId: Int): Flow<List<BalanceHold>> {
|
||||
return flowOfAll {
|
||||
val chainAsset = chainRegistry.asset(chainId, chainAssetId)
|
||||
val selectedAccount = accountRepository.getSelectedMetaAccount()
|
||||
balanceHoldsRepository.observeBalanceHolds(selectedAccount.id, chainAsset)
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.networks
|
||||
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
import io.novafoundation.nova.common.utils.filterList
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchFilter
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.search.AssetSearchUseCase
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.search.mapToAssetSearchFilter
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork
|
||||
import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetBaseComparator
|
||||
import io.novafoundation.nova.feature_assets.domain.common.getTokenAssetGroupBaseComparator
|
||||
import io.novafoundation.nova.feature_assets.domain.common.groupAndSortAssetsByToken
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.aggregatedBalanceByAsset
|
||||
import io.novafoundation.nova.runtime.ext.normalize
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainById
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AssetNetworksInteractor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetSearchUseCase: AssetSearchUseCase,
|
||||
private val tradeTokenRegistry: TradeTokenRegistry,
|
||||
private val giftAssetsUseCase: AvailableGiftAssetsUseCase
|
||||
) {
|
||||
|
||||
fun tradeAssetFlow(
|
||||
tokenSymbol: TokenSymbol,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
tradeType: TradeTokenRegistry.TradeType
|
||||
): Flow<List<AssetWithNetwork>> {
|
||||
val filter = { asset: Asset -> tradeTokenRegistry.hasProvider(asset.token.configuration, tradeType) }
|
||||
|
||||
return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, filter = filter)
|
||||
}
|
||||
|
||||
fun sendAssetFlow(
|
||||
tokenSymbol: TokenSymbol,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<List<AssetWithNetwork>> {
|
||||
val filter = { asset: Asset -> asset.transferableInPlanks.isPositive() }
|
||||
|
||||
return searchAssetsByTokenSymbolInternalFlow(
|
||||
tokenSymbol,
|
||||
externalBalancesFlow,
|
||||
assetGroupComparator = getTokenAssetGroupBaseComparator { it.groupBalance.transferable.fiat },
|
||||
assetsComparator = getTokenAssetBaseComparator { it.balanceWithOffChain.transferable.fiat },
|
||||
filter = filter
|
||||
)
|
||||
}
|
||||
|
||||
fun swapAssetsFlow(
|
||||
forAssetId: FullChainAssetId?,
|
||||
tokenSymbol: TokenSymbol,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<List<AssetWithNetwork>> {
|
||||
val filterFlow = assetSearchUseCase.getAvailableSwapAssets(forAssetId, coroutineScope).mapToAssetSearchFilter()
|
||||
return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, filterFlow = filterFlow)
|
||||
}
|
||||
|
||||
fun receiveAssetFlow(
|
||||
tokenSymbol: TokenSymbol,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
): Flow<List<AssetWithNetwork>> {
|
||||
return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, filter = null)
|
||||
}
|
||||
|
||||
fun giftsAssetFlow(
|
||||
tokenSymbol: TokenSymbol,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
coroutineScope: CoroutineScope
|
||||
): Flow<List<AssetWithNetwork>> {
|
||||
val filterFlow = giftAssetsUseCase.getAvailableGiftAssets(coroutineScope).mapToAssetSearchFilter()
|
||||
return searchAssetsByTokenSymbolInternalFlow(
|
||||
tokenSymbol,
|
||||
externalBalancesFlow,
|
||||
assetGroupComparator = getTokenAssetGroupBaseComparator { it.groupBalance.transferable.fiat },
|
||||
assetsComparator = getTokenAssetBaseComparator { it.balanceWithOffChain.transferable.fiat },
|
||||
filterFlow = filterFlow
|
||||
)
|
||||
}
|
||||
|
||||
fun searchAssetsByTokenSymbolInternalFlow(
|
||||
tokenSymbol: TokenSymbol,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
assetGroupComparator: Comparator<TokenAssetGroup> = getTokenAssetGroupBaseComparator(),
|
||||
assetsComparator: Comparator<AssetWithNetwork> = getTokenAssetBaseComparator(),
|
||||
filterFlow: Flow<AssetSearchFilter?>,
|
||||
): Flow<List<AssetWithNetwork>> {
|
||||
val assetsFlow = assetSearchUseCase.filteredAssetFlow(filterFlow)
|
||||
.filterList { it.token.configuration.symbol.normalize() == tokenSymbol }
|
||||
|
||||
val aggregatedExternalBalances = externalBalancesFlow.map { it.aggregatedBalanceByAsset() }
|
||||
|
||||
return combine(assetsFlow, aggregatedExternalBalances) { assets, externalBalances ->
|
||||
val chainsById = chainRegistry.enabledChainById()
|
||||
|
||||
groupAndSortAssetsByToken(assets, externalBalances, chainsById, assetGroupComparator, assetsComparator)
|
||||
.flatMap { it.value }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSwapAssetsFilter(sourceAsset: FullChainAssetId?, coroutineScope: CoroutineScope): Flow<AssetSearchFilter> {
|
||||
return assetSearchUseCase.getAvailableSwapAssets(sourceAsset, coroutineScope).mapToAssetSearchFilter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun AssetNetworksInteractor.searchAssetsByTokenSymbolInternalFlow(
|
||||
tokenSymbol: TokenSymbol,
|
||||
externalBalancesFlow: Flow<List<ExternalBalance>>,
|
||||
assetGroupComparator: Comparator<TokenAssetGroup> = getTokenAssetGroupBaseComparator(),
|
||||
assetsComparator: Comparator<AssetWithNetwork> = getTokenAssetBaseComparator(),
|
||||
filter: AssetSearchFilter?,
|
||||
): Flow<List<AssetWithNetwork>> {
|
||||
val filterFlow = flowOf(filter)
|
||||
|
||||
return searchAssetsByTokenSymbolInternalFlow(tokenSymbol, externalBalancesFlow, assetGroupComparator, assetsComparator, filterFlow)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.novaCard
|
||||
|
||||
import io.novafoundation.nova.feature_assets.data.repository.NovaCardStateRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
interface NovaCardInteractor {
|
||||
|
||||
fun isNovaCardCreated(): Boolean
|
||||
|
||||
fun getNovaCardState(): NovaCardState
|
||||
|
||||
fun setNovaCardState(state: NovaCardState)
|
||||
|
||||
suspend fun setTopUpFinishedEvent()
|
||||
|
||||
fun observeTopUpFinishedEvent(): Flow<Unit>
|
||||
|
||||
fun observeNovaCardState(): Flow<NovaCardState>
|
||||
|
||||
fun setLastTopUpTime(time: Long)
|
||||
|
||||
fun getEstimatedTopUpDuration(): Long
|
||||
}
|
||||
|
||||
const val TIMER_MINUTES = 5
|
||||
|
||||
class RealNovaCardInteractor(
|
||||
private val novaCardStateRepository: NovaCardStateRepository
|
||||
) : NovaCardInteractor {
|
||||
|
||||
override fun isNovaCardCreated(): Boolean {
|
||||
return novaCardStateRepository.getNovaCardCreationState() == NovaCardState.CREATED
|
||||
}
|
||||
|
||||
override fun getNovaCardState(): NovaCardState {
|
||||
return novaCardStateRepository.getNovaCardCreationState()
|
||||
}
|
||||
|
||||
override fun setNovaCardState(state: NovaCardState) {
|
||||
return novaCardStateRepository.setNovaCardCreationState(state)
|
||||
}
|
||||
|
||||
override suspend fun setTopUpFinishedEvent() {
|
||||
novaCardStateRepository.setTopUpFinishedEvent()
|
||||
}
|
||||
|
||||
override fun observeTopUpFinishedEvent(): Flow<Unit> {
|
||||
return novaCardStateRepository.observeTopUpFinishedEvent()
|
||||
}
|
||||
|
||||
override fun observeNovaCardState(): Flow<NovaCardState> {
|
||||
return novaCardStateRepository.observeNovaCardCreationState()
|
||||
}
|
||||
|
||||
override fun setLastTopUpTime(time: Long) {
|
||||
novaCardStateRepository.setLastTopUpTime(time)
|
||||
}
|
||||
|
||||
override fun getEstimatedTopUpDuration(): Long {
|
||||
val lastTopUpTime = novaCardStateRepository.getLastTopUpTime()
|
||||
val onTopUpFinishTime = lastTopUpTime + TIMER_MINUTES.minutes.inWholeMilliseconds
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val estimatedDurationToFinishTopUp = onTopUpFinishTime - currentTime
|
||||
|
||||
return estimatedDurationToFinishTopUp.coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.novaCard
|
||||
|
||||
enum class NovaCardState {
|
||||
NONE,
|
||||
CREATION,
|
||||
CREATED
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.price
|
||||
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.PricePeriod
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
|
||||
class AssetPriceChart(val range: PricePeriod, val chart: ExtendedLoadingState<List<HistoricalCoinRate>>)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.price
|
||||
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.common.domain.map
|
||||
import io.novafoundation.nova.common.utils.combine
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.PricePeriod
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.duration
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface ChartsInteractor {
|
||||
|
||||
fun chartsFlow(priceId: String): Flow<List<AssetPriceChart>>
|
||||
}
|
||||
|
||||
class RealChartsInteractor(
|
||||
private val coinPriceRepository: CoinPriceRepository,
|
||||
private val currencyRepository: CurrencyRepository
|
||||
) : ChartsInteractor {
|
||||
|
||||
override fun chartsFlow(priceId: String): Flow<List<AssetPriceChart>> {
|
||||
val dayChart = getChartFor(priceId, PricePeriod.DAY)
|
||||
val monthChart = getChartFor(priceId, PricePeriod.MONTH)
|
||||
val maxChart = getChartFor(priceId, PricePeriod.MAX)
|
||||
|
||||
val weekChart = monthChart.map { it.subChartForPeriod(PricePeriod.WEEK) }
|
||||
val yearChart = maxChart.map { it.subChartForPeriod(PricePeriod.YEAR) }
|
||||
|
||||
return listOf(dayChart, weekChart, monthChart, yearChart, maxChart).combine()
|
||||
}
|
||||
|
||||
private fun getChartFor(priceId: String, range: PricePeriod): Flow<AssetPriceChart> {
|
||||
return flow {
|
||||
emit(AssetPriceChart(range, ExtendedLoadingState.Loading))
|
||||
val currency = currencyRepository.getSelectedCurrency()
|
||||
|
||||
runCatching { coinPriceRepository.getLastHistoryForPeriod(priceId, currency, range) }
|
||||
.onSuccess { emit(AssetPriceChart(range, ExtendedLoadingState.Loaded(it))) }
|
||||
.onFailure { emit(AssetPriceChart(range, ExtendedLoadingState.Error(it))) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun AssetPriceChart.subChartForPeriod(period: PricePeriod): AssetPriceChart {
|
||||
val subChartDuration = period.duration()
|
||||
val chart = chart.map { historicalPoints ->
|
||||
val lastPoint = historicalPoints.lastOrNull() ?: return@map emptyList()
|
||||
val fromDate = lastPoint.timestamp - subChartDuration.inWholeSeconds
|
||||
historicalPoints.filter { it.timestamp > fromDate }
|
||||
}
|
||||
return AssetPriceChart(period, chart)
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.receive
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.common.data.model.AssetIconMode
|
||||
import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.common.utils.write
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val QR_FILE_NAME = "share-qr-address.png"
|
||||
|
||||
class ReceiveInteractor(
|
||||
private val fileProvider: FileProvider,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val assetsIconModeRepository: AssetsIconModeRepository
|
||||
) {
|
||||
|
||||
suspend fun getQrCodeSharingString(chainId: ChainId): String = withContext(Dispatchers.Default) {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val account = accountRepository.getSelectedMetaAccount()
|
||||
|
||||
accountRepository.createQrAccountContent(chain, account)
|
||||
}
|
||||
|
||||
fun getAssetIconMode(): AssetIconMode = assetsIconModeRepository.getIconMode()
|
||||
|
||||
suspend fun generateTempQrFile(qrCode: Bitmap): Result<Uri> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val file = fileProvider.generateTempFile(fixedName = QR_FILE_NAME)
|
||||
file.write(qrCode)
|
||||
|
||||
fileProvider.uriOf(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.send
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_assets.domain.send.model.TransferFee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.isCrossChain
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainValidationSystemProvider
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.getXcmChain
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransfersConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.transferConfiguration
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SendInteractor(
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val crossChainTransactor: CrossChainTransactor,
|
||||
private val crossChainTransfersRepository: CrossChainTransfersRepository,
|
||||
private val parachainInfoRepository: ParachainInfoRepository,
|
||||
private val crossChainTransfersUseCase: CrossChainTransfersUseCase,
|
||||
private val extrinsicService: ExtrinsicService,
|
||||
private val sendUseCase: SendUseCase,
|
||||
private val crossChainValidationProvider: CrossChainValidationSystemProvider
|
||||
) {
|
||||
|
||||
suspend fun getFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): TransferFee = withContext(Dispatchers.Default) {
|
||||
if (transfer.isCrossChain) {
|
||||
val fees = with(crossChainTransfersUseCase) {
|
||||
extrinsicService.estimateFee(transfer, cachingScope = null)
|
||||
}
|
||||
|
||||
val originFee = OriginFee(
|
||||
submissionFee = fees.submissionFee,
|
||||
deliveryFee = fees.postSubmissionByAccount,
|
||||
)
|
||||
|
||||
TransferFee(originFee, fees.postSubmissionFromAmount)
|
||||
} else {
|
||||
TransferFee(
|
||||
originFee = getOriginFee(transfer, coroutineScope),
|
||||
crossChainFee = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getOriginFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): OriginFee = withContext(Dispatchers.Default) {
|
||||
OriginFee(getSubmissionFee(transfer, coroutineScope), null)
|
||||
}
|
||||
|
||||
suspend fun getSubmissionFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): SubmissionFee = withContext(Dispatchers.Default) {
|
||||
getAssetTransfers(transfer).calculateFee(transfer, coroutineScope = coroutineScope)
|
||||
}
|
||||
|
||||
suspend fun performTransfer(
|
||||
transfer: WeightedAssetTransfer,
|
||||
originFee: OriginFee,
|
||||
crossChainFee: FeeBase?,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<ExtrinsicSubmission> = withContext(Dispatchers.Default) {
|
||||
if (transfer.isCrossChain) {
|
||||
val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!!
|
||||
|
||||
with(extrinsicService) {
|
||||
crossChainTransactor.performTransfer(config, transfer, crossChainFee!!.amount)
|
||||
}
|
||||
} else {
|
||||
sendUseCase.performOnChainTransfer(transfer, originFee.submissionFee, coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
fun validationSystemFor(transfer: AssetTransfer, coroutineScope: CoroutineScope) = if (transfer.isCrossChain) {
|
||||
crossChainValidationProvider.createValidationSystem()
|
||||
} else {
|
||||
assetSourceRegistry.sourceFor(transfer.originChainAsset).transfers.getValidationSystem(coroutineScope)
|
||||
}
|
||||
|
||||
suspend fun areTransfersEnabled(asset: Chain.Asset) = assetSourceRegistry.sourceFor(asset).transfers.areTransfersEnabled(asset)
|
||||
|
||||
private fun getAssetTransfers(transfer: AssetTransfer) = assetSourceRegistry.sourceFor(transfer.originChainAsset).transfers
|
||||
|
||||
private suspend fun CrossChainTransfersConfiguration.configurationFor(transfer: AssetTransfer) = transferConfiguration(
|
||||
originChain = parachainInfoRepository.getXcmChain(transfer.originChain),
|
||||
originAsset = transfer.originChainAsset,
|
||||
destinationChain = parachainInfoRepository.getXcmChain(transfer.destinationChain),
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.send.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_account_api.data.model.getAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.maxAction.MaxAvailableDeduction
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
data class TransferFee(
|
||||
val originFee: OriginFee,
|
||||
val crossChainFee: FeeBase?
|
||||
) : MaxAvailableDeduction {
|
||||
|
||||
fun totalFeeByExecutingAccount(chainAsset: Chain.Asset): Balance {
|
||||
val accountThatPaysFees = originFee.submissionFee.submissionOrigin.executingAccount
|
||||
|
||||
val submission = originFee.submissionFee.getAmount(chainAsset, accountThatPaysFees)
|
||||
val delivery = originFee.deliveryFee?.getAmount(chainAsset, accountThatPaysFees).orZero()
|
||||
|
||||
return submission + delivery
|
||||
}
|
||||
|
||||
fun replaceSubmission(newSubmissionFee: SubmissionFee): TransferFee {
|
||||
return copy(originFee = originFee.copy(newSubmissionFee))
|
||||
}
|
||||
|
||||
override fun maxAmountDeductionFor(amountAsset: Chain.Asset): Balance {
|
||||
// Delegate submission calculation to submission fee itself
|
||||
val submission = originFee.submissionFee.maxAmountDeductionFor(amountAsset)
|
||||
// Delivery is always paid from executing account
|
||||
val delivery = originFee.deliveryFee?.getAmount(amountAsset).orZero()
|
||||
// Execution is paid from the sending amount itself, so we subtract it as well since we later add it on top of sending amount
|
||||
val execution = crossChainFee?.getAmount(amountAsset).orZero()
|
||||
|
||||
return submission + delivery + execution
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens
|
||||
|
||||
import io.novafoundation.nova.feature_crowdloan_api.data.repository.ContributionsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.ExternalBalanceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
interface AssetsDataCleaner {
|
||||
|
||||
suspend fun clearAssetsData(assetIds: List<FullChainAssetId>)
|
||||
}
|
||||
|
||||
class RealAssetsDataCleaner(
|
||||
private val externalBalanceRepository: ExternalBalanceRepository,
|
||||
private val contributionsRepository: ContributionsRepository,
|
||||
private val walletRepository: WalletRepository,
|
||||
) : AssetsDataCleaner {
|
||||
|
||||
override suspend fun clearAssetsData(assetIds: List<FullChainAssetId>) {
|
||||
contributionsRepository.deleteContributions(assetIds)
|
||||
externalBalanceRepository.deleteExternalBalances(assetIds)
|
||||
walletRepository.clearAssets(assetIds)
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens.add
|
||||
|
||||
import io.novafoundation.nova.common.address.format.EthereumAddressFormat
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.utils.asPrecision
|
||||
import io.novafoundation.nova.common.utils.asTokenSymbol
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.AddEvmTokenValidationSystem
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.evmAssetNotExists
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.validCoinGeckoLink
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.validErc20Contract
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.validTokenDecimals
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.querySingle
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Queries
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
|
||||
import io.novafoundation.nova.runtime.ext.defaultComparator
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.chainAssetIdOfErc20Token
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainsFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface AddTokensInteractor {
|
||||
|
||||
fun availableChainsToAddTokenFlow(): Flow<List<Chain>>
|
||||
|
||||
suspend fun retrieveContractMetadata(
|
||||
chainId: ChainId,
|
||||
contractAddress: String
|
||||
): Erc20ContractMetadata?
|
||||
|
||||
suspend fun addCustomTokenAndSync(customErc20Token: CustomErc20Token): Result<*>
|
||||
|
||||
fun getValidationSystem(): AddEvmTokenValidationSystem
|
||||
}
|
||||
|
||||
class RealAddTokensInteractor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val erc20Standard: Erc20Standard,
|
||||
private val chainAssetRepository: ChainAssetRepository,
|
||||
private val coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
private val ethereumAddressFormat: EthereumAddressFormat,
|
||||
private val currencyRepository: CurrencyRepository,
|
||||
private val walletRepository: WalletRepository,
|
||||
private val coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory
|
||||
) : AddTokensInteractor {
|
||||
|
||||
override fun availableChainsToAddTokenFlow(): Flow<List<Chain>> {
|
||||
return chainRegistry.enabledChainsFlow().map { chains ->
|
||||
chains.filter { it.isEthereumBased }
|
||||
.sortedWith(Chain.defaultComparator())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun retrieveContractMetadata(
|
||||
chainId: ChainId,
|
||||
contractAddress: String,
|
||||
): Erc20ContractMetadata? {
|
||||
return runCatching {
|
||||
queryErc20Contract(chainId, contractAddress) {
|
||||
Erc20ContractMetadata(
|
||||
decimals = executeOrNull { decimals().toInt() },
|
||||
symbol = executeOrNull { symbol() }
|
||||
)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override suspend fun addCustomTokenAndSync(customErc20Token: CustomErc20Token): Result<*> = runCatching {
|
||||
val priceId = coinGeckoLinkParser.parse(customErc20Token.priceLink).getOrNull()?.priceId
|
||||
|
||||
val asset = Chain.Asset(
|
||||
icon = null,
|
||||
id = chainAssetIdOfErc20Token(customErc20Token.contract),
|
||||
priceId = priceId,
|
||||
chainId = customErc20Token.chainId,
|
||||
symbol = customErc20Token.symbol.asTokenSymbol(),
|
||||
precision = customErc20Token.decimals.asPrecision(),
|
||||
buyProviders = emptyMap(),
|
||||
sellProviders = emptyMap(),
|
||||
staking = emptyList(),
|
||||
type = Chain.Asset.Type.EvmErc20(customErc20Token.contract),
|
||||
source = Chain.Asset.Source.MANUAL,
|
||||
name = customErc20Token.symbol,
|
||||
enabled = true
|
||||
)
|
||||
|
||||
chainAssetRepository.insertCustomAsset(asset)
|
||||
|
||||
syncTokenPrice(asset)
|
||||
}
|
||||
|
||||
override fun getValidationSystem(): AddEvmTokenValidationSystem {
|
||||
return ValidationSystem {
|
||||
evmAssetNotExists(chainRegistry)
|
||||
validErc20Contract(ethereumAddressFormat, erc20Standard, chainRegistry)
|
||||
validTokenDecimals()
|
||||
validCoinGeckoLink(coinGeckoLinkValidationFactory)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <R> queryErc20Contract(
|
||||
chainId: ChainId,
|
||||
contractAddress: String,
|
||||
query: suspend Erc20Queries.() -> R
|
||||
): R {
|
||||
val ethereumApi = chainRegistry.getCallEthereumApiOrThrow(chainId)
|
||||
val erc20Queries = erc20Standard.querySingle(contractAddress, ethereumApi)
|
||||
|
||||
return query(erc20Queries)
|
||||
}
|
||||
|
||||
private suspend fun <R> executeOrNull(action: suspend () -> R): R? = runCatching { action() }.getOrNull()
|
||||
|
||||
private suspend fun syncTokenPrice(asset: Chain.Asset) {
|
||||
val currency = currencyRepository.getSelectedCurrency()
|
||||
walletRepository.syncAssetRates(asset, currency)
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens.add
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
class CustomErc20Token(
|
||||
val contract: String,
|
||||
val decimals: Int,
|
||||
val symbol: String,
|
||||
val priceLink: String,
|
||||
val chainId: ChainId,
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens.add
|
||||
|
||||
class Erc20ContractMetadata(
|
||||
val decimals: Int?,
|
||||
val symbol: String?,
|
||||
)
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens.add.validations
|
||||
|
||||
import io.novafoundation.nova.common.address.format.EthereumAddressFormat
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.CustomErc20Token
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.evmAssetNotExists
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.validErc20Contract
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.validTokenDecimals
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
typealias AddEvmTokenValidationSystem = ValidationSystem<AddEvmTokenPayload, AddEvmTokensValidationFailure>
|
||||
typealias AddEvmTokenValidationSystemBuilder = ValidationSystemBuilder<AddEvmTokenPayload, AddEvmTokensValidationFailure>
|
||||
|
||||
sealed interface AddEvmTokensValidationFailure {
|
||||
class InvalidTokenContractAddress(val chainName: String) : AddEvmTokensValidationFailure
|
||||
|
||||
class AssetExist(val alreadyExistingSymbol: String, val canModify: Boolean) : AddEvmTokensValidationFailure
|
||||
|
||||
object InvalidDecimals : AddEvmTokensValidationFailure
|
||||
|
||||
object InvalidCoinGeckoLink : AddEvmTokensValidationFailure
|
||||
}
|
||||
|
||||
fun AddEvmTokenValidationSystemBuilder.validErc20Contract(
|
||||
ethereumAddressFormat: EthereumAddressFormat,
|
||||
erc20Standard: Erc20Standard,
|
||||
chainRegistry: ChainRegistry,
|
||||
) = validErc20Contract(
|
||||
ethereumAddressFormat = ethereumAddressFormat,
|
||||
erc20Standard = erc20Standard,
|
||||
chainRegistry = chainRegistry,
|
||||
chain = { it.chain },
|
||||
address = { it.customErc20Token.contract },
|
||||
error = { AddEvmTokensValidationFailure.InvalidTokenContractAddress(it.chain.name) }
|
||||
)
|
||||
|
||||
fun AddEvmTokenValidationSystemBuilder.evmAssetNotExists(chainRegistry: ChainRegistry) = evmAssetNotExists(
|
||||
chainRegistry = chainRegistry,
|
||||
chain = { it.chain },
|
||||
address = { it.customErc20Token.contract },
|
||||
assetNotExistError = AddEvmTokensValidationFailure::AssetExist,
|
||||
addressMappingError = { AddEvmTokensValidationFailure.InvalidTokenContractAddress(it.chain.name) }
|
||||
)
|
||||
|
||||
fun AddEvmTokenValidationSystemBuilder.validTokenDecimals() = validTokenDecimals(
|
||||
decimals = { it.customErc20Token.decimals },
|
||||
error = { AddEvmTokensValidationFailure.InvalidDecimals }
|
||||
)
|
||||
|
||||
fun AddEvmTokenValidationSystemBuilder.validCoinGeckoLink(
|
||||
coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory
|
||||
) = validCoinGeckoLink(
|
||||
coinGeckoLinkValidationFactory = coinGeckoLinkValidationFactory,
|
||||
optional = true,
|
||||
link = { it.customErc20Token.priceLink },
|
||||
error = { AddEvmTokensValidationFailure.InvalidCoinGeckoLink }
|
||||
)
|
||||
|
||||
class AddEvmTokenPayload(
|
||||
val customErc20Token: CustomErc20Token,
|
||||
val chain: Chain
|
||||
)
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens.add.validations
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.novafoundation.nova.common.utils.asQueryParam
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.common.validation.isTrueOrError
|
||||
import io.novafoundation.nova.common.validation.valid
|
||||
import io.novafoundation.nova.common.validation.validationError
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.priceApi.CoingeckoApi
|
||||
|
||||
class CoinGeckoLinkValidationFactory(
|
||||
private val coingeckoApi: CoingeckoApi,
|
||||
private val coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
) {
|
||||
fun <P, E> create(
|
||||
optional: Boolean,
|
||||
link: (P) -> String?,
|
||||
error: (P) -> E,
|
||||
): CoinGeckoLinkValidation<P, E> {
|
||||
return CoinGeckoLinkValidation(
|
||||
coingeckoApi,
|
||||
coinGeckoLinkParser,
|
||||
optional,
|
||||
link,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CoinGeckoLinkValidation<P, E>(
|
||||
private val coinGeckoApi: CoingeckoApi,
|
||||
private val coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
private val optional: Boolean,
|
||||
private val link: (P) -> String?,
|
||||
private val error: (P) -> E,
|
||||
) : Validation<P, E> {
|
||||
|
||||
override suspend fun validate(value: P): ValidationStatus<E> {
|
||||
if (optional && TextUtils.isEmpty(link(value))) {
|
||||
return valid()
|
||||
}
|
||||
|
||||
return try {
|
||||
val link = link(value)!!
|
||||
val coinGeckoContent = coinGeckoLinkParser.parse(link).getOrThrow()
|
||||
val priceId = coinGeckoContent.priceId
|
||||
val result = coinGeckoApi.getAssetPrice(setOf(priceId).asQueryParam(), "usd", false)
|
||||
result.isNotEmpty().isTrueOrError { error(value) }
|
||||
} catch (e: Exception) {
|
||||
validationError(error(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <P, E> ValidationSystemBuilder<P, E>.validCoinGeckoLink(
|
||||
coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory,
|
||||
optional: Boolean,
|
||||
link: (P) -> String?,
|
||||
error: (P) -> E,
|
||||
) = validate(
|
||||
coinGeckoLinkValidationFactory.create(
|
||||
optional,
|
||||
link,
|
||||
error,
|
||||
)
|
||||
)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens.manage
|
||||
|
||||
import io.novafoundation.nova.common.utils.isSubsetOf
|
||||
import io.novafoundation.nova.feature_assets.domain.common.searchTokens
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.AssetsDataCleaner
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository
|
||||
import io.novafoundation.nova.runtime.ext.defaultComparator
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.normalizeSymbol
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chainsById
|
||||
import io.novafoundation.nova.runtime.multiNetwork.enabledChainsFlow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface ManageTokenInteractor {
|
||||
|
||||
fun multiChainTokensFlow(queryFlow: Flow<String>): Flow<List<MultiChainToken>>
|
||||
|
||||
fun multiChainTokenFlow(id: String): Flow<MultiChainToken>
|
||||
|
||||
suspend fun updateEnabledState(enabled: Boolean, assetIds: List<FullChainAssetId>)
|
||||
}
|
||||
|
||||
class RealManageTokenInteractor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val chainAssetRepository: ChainAssetRepository,
|
||||
private val assetsDataCleaner: AssetsDataCleaner,
|
||||
) : ManageTokenInteractor {
|
||||
|
||||
private val changeTokensMutex = Mutex(false)
|
||||
|
||||
override fun multiChainTokensFlow(
|
||||
queryFlow: Flow<String>
|
||||
): Flow<List<MultiChainToken>> {
|
||||
return combine(multiChainTokensFlow(), queryFlow) { tokens, query ->
|
||||
tokens.searchTokens(
|
||||
query = query,
|
||||
chainsById = chainRegistry.chainsById(),
|
||||
tokenSymbol = MultiChainToken::symbol,
|
||||
relevantToChains = { multiChainToken, chainIds ->
|
||||
multiChainToken.instances.any { it.chain.id in chainIds }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun multiChainTokensFlow() = chainRegistry.enabledChainsFlow().map { chains ->
|
||||
constructMultiChainTokens(chains)
|
||||
}
|
||||
|
||||
override fun multiChainTokenFlow(id: String): Flow<MultiChainToken> {
|
||||
return multiChainTokensFlow().map { multiChainTokens ->
|
||||
multiChainTokens.first { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateEnabledState(enabled: Boolean, assetIds: List<FullChainAssetId>) = withContext(Dispatchers.IO) {
|
||||
changeTokensMutex.withLock {
|
||||
if (!enabled && canNotDisableAssets(assetIds)) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
chainAssetRepository.setAssetsEnabled(enabled, assetIds)
|
||||
|
||||
if (!enabled) {
|
||||
assetsDataCleaner.clearAssetsData(assetIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun canNotDisableAssets(assetIds: List<FullChainAssetId>): Boolean {
|
||||
val enabledAssets = chainAssetRepository.getEnabledAssets()
|
||||
.map { it.fullId }
|
||||
return assetIds.containsAll(enabledAssets)
|
||||
}
|
||||
|
||||
private fun constructMultiChainTokens(chains: List<Chain>): List<MultiChainToken> {
|
||||
val chainComparator = Chain.defaultComparator()
|
||||
val assetsWithChains = chains.sortedWith(chainComparator).flatMap { chain ->
|
||||
chain.assets.map { asset -> ChainWithAsset(chain, asset) }
|
||||
}
|
||||
|
||||
val enabledAssets = assetsWithChains.filter { it.asset.enabled }
|
||||
.map { it.asset.fullId }
|
||||
|
||||
return assetsWithChains.groupBy { (_, asset) -> asset.normalizeSymbol() }
|
||||
.map { (symbol, chainsWithAssets) ->
|
||||
val (_, firstAsset) = chainsWithAssets.first()
|
||||
val tokenAssets = chainsWithAssets.filter { it.asset.enabled }
|
||||
.map { it.asset.fullId }
|
||||
val isLastTokenEnabled = enabledAssets.isSubsetOf(tokenAssets)
|
||||
val isLastAssetEnabled = isLastTokenEnabled && tokenAssets.size == 1
|
||||
|
||||
MultiChainToken(
|
||||
id = symbol,
|
||||
symbol = symbol,
|
||||
icon = firstAsset.icon,
|
||||
isSwitchable = !isLastTokenEnabled,
|
||||
instances = chainsWithAssets.map { (chain, asset) ->
|
||||
MultiChainToken.ChainTokenInstance(
|
||||
chain = chain,
|
||||
chainAssetId = asset.id,
|
||||
isEnabled = asset.enabled,
|
||||
isSwitchable = !asset.enabled || !isLastAssetEnabled
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_assets.domain.tokens.manage
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
class MultiChainToken(
|
||||
val id: String,
|
||||
val symbol: String,
|
||||
val icon: String?,
|
||||
val isSwitchable: Boolean,
|
||||
val instances: List<ChainTokenInstance>
|
||||
) {
|
||||
|
||||
class ChainTokenInstance(
|
||||
val chain: Chain,
|
||||
val chainAssetId: ChainAssetId,
|
||||
val isEnabled: Boolean,
|
||||
val isSwitchable: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
fun MultiChainToken.isEnabled(): Boolean {
|
||||
return instances.any { it.isEnabled }
|
||||
}
|
||||
|
||||
fun MultiChainToken.allChainAssetIds(): List<FullChainAssetId> {
|
||||
return instances.map {
|
||||
FullChainAssetId(it.chain.id, it.chainAssetId)
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import io.novafoundation.nova.feature_assets.presentation.flow.network.NetworkFlowPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.OperationParcelizeModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.swap.network.NetworkSwapFlowPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.add.enterInfo.AddTokenEnterInfoPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.tokens.manage.chain.ManageChainTokensPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.trade.webInterface.TradeWebPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.filter.TransactionHistoryFilterPayload
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
|
||||
interface AssetsRouter {
|
||||
|
||||
fun openAssetDetails(assetPayload: AssetPayload)
|
||||
|
||||
fun finishTradeOperation()
|
||||
|
||||
fun back()
|
||||
|
||||
fun openFilter(payload: TransactionHistoryFilterPayload)
|
||||
|
||||
fun openSend(payload: SendPayload, initialRecipientAddress: String? = null, initialAmount: Double? = null)
|
||||
|
||||
fun openConfirmTransfer(transferDraft: TransferDraft)
|
||||
|
||||
fun openTransferDetail(transaction: OperationParcelizeModel.Transfer)
|
||||
|
||||
fun openExtrinsicDetail(extrinsic: OperationParcelizeModel.Extrinsic)
|
||||
|
||||
fun openRewardDetail(reward: OperationParcelizeModel.Reward)
|
||||
|
||||
fun openPoolRewardDetail(reward: OperationParcelizeModel.PoolReward)
|
||||
|
||||
fun openSwapDetail(swap: OperationParcelizeModel.Swap)
|
||||
|
||||
fun openSwitchWallet()
|
||||
|
||||
fun openSelectAddress(arguments: Bundle)
|
||||
|
||||
fun openSelectSingleWallet(arguments: Bundle)
|
||||
|
||||
fun openSelectMultipleWallets(arguments: Bundle)
|
||||
|
||||
fun openReceive(assetPayload: AssetPayload)
|
||||
|
||||
fun openAssetSearch()
|
||||
|
||||
fun openManageTokens()
|
||||
|
||||
fun openManageChainTokens(payload: ManageChainTokensPayload)
|
||||
|
||||
fun openAddTokenEnterInfo(payload: AddTokenEnterInfoPayload)
|
||||
|
||||
fun openAddTokenSelectChain()
|
||||
|
||||
fun openSendFlow()
|
||||
|
||||
fun openReceiveFlow()
|
||||
|
||||
fun openBuyFlow()
|
||||
|
||||
fun openSellFlow()
|
||||
|
||||
fun openBridgeFlow()
|
||||
|
||||
fun openBuyFlowFromSendFlow()
|
||||
|
||||
fun openNfts()
|
||||
|
||||
fun finishAddTokenFlow()
|
||||
|
||||
fun openWalletConnectSessions(metaId: Long)
|
||||
|
||||
fun openWalletConnectScan()
|
||||
|
||||
fun openSwapFlow()
|
||||
|
||||
fun openSwapSetupAmount(swapSettingsPayload: SwapSettingsPayload)
|
||||
|
||||
fun returnToMainScreen()
|
||||
|
||||
fun finishSelectAndOpenSwapSetupAmount(swapSettingsPayload: SwapSettingsPayload)
|
||||
|
||||
fun closeSendFlow()
|
||||
|
||||
fun openNovaCard()
|
||||
|
||||
fun openAwaitingCardCreation()
|
||||
|
||||
fun closeNovaCard()
|
||||
|
||||
fun openSendNetworks(payload: NetworkFlowPayload)
|
||||
|
||||
fun openReceiveNetworks(payload: NetworkFlowPayload)
|
||||
|
||||
fun openSwapNetworks(payload: NetworkSwapFlowPayload)
|
||||
|
||||
fun returnToMainSwapScreen()
|
||||
|
||||
fun openBuyNetworks(payload: NetworkFlowPayload)
|
||||
|
||||
fun openSellNetworks(payload: NetworkFlowPayload)
|
||||
|
||||
fun openGiftsNetworks(payload: NetworkFlowPayload)
|
||||
|
||||
fun openBuyProviders(chainId: String, chainAssetId: Int)
|
||||
|
||||
fun openSellProviders(chainId: String, chainAssetId: Int)
|
||||
|
||||
fun openTradeWebInterface(payload: TradeWebPayload)
|
||||
|
||||
fun finishTopUp()
|
||||
|
||||
fun openPendingMultisigOperations()
|
||||
|
||||
fun openAssetDetailsFromDeepLink(payload: AssetPayload)
|
||||
|
||||
fun openGifts()
|
||||
|
||||
fun openGiftsByAsset(assetPayload: AssetPayload)
|
||||
|
||||
fun openSelectGiftAmount(assetPayload: AssetPayload)
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.assetActions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import io.novafoundation.nova.common.utils.dp
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.updatePadding
|
||||
import io.novafoundation.nova.common.view.shape.getBlockDrawable
|
||||
import io.novafoundation.nova.feature_assets.databinding.ViewAssetActionsBinding
|
||||
|
||||
class AssetActionsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyle) {
|
||||
|
||||
private val binder = ViewAssetActionsBinding.inflate(inflater(), this)
|
||||
|
||||
init {
|
||||
orientation = HORIZONTAL
|
||||
|
||||
background = context.getBlockDrawable()
|
||||
|
||||
updatePadding(top = 4.dp(context), bottom = 4.dp(context))
|
||||
}
|
||||
|
||||
val send: TextView
|
||||
get() = binder.assetActionsSend
|
||||
|
||||
val receive: TextView
|
||||
get() = binder.assetActionsReceive
|
||||
|
||||
val swap: TextView
|
||||
get() = binder.assetActionsSwap
|
||||
|
||||
val buySell: TextView
|
||||
get() = binder.assetActionsBuy
|
||||
|
||||
val gift: TextView
|
||||
get() = binder.assetActionsGift
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.breakdown
|
||||
|
||||
import android.view.ViewGroup
|
||||
import io.novafoundation.nova.common.list.BaseGroupedDiffCallback
|
||||
import io.novafoundation.nova.common.list.GroupedListAdapter
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemBalanceBreakdownAmountBinding
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemBalanceBreakdownTotalBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.BalanceBreakdownAmount
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.BalanceBreakdownTotal
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.showAmount
|
||||
|
||||
class BalanceBreakdownAdapter : GroupedListAdapter<BalanceBreakdownTotal, BalanceBreakdownAmount>(DiffCallback) {
|
||||
|
||||
override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return BalanceTotalHolder(ItemBalanceBreakdownTotalBinding.inflate(parent.inflater(), parent, false))
|
||||
}
|
||||
|
||||
override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return BalanceAmountHolder(ItemBalanceBreakdownAmountBinding.inflate(parent.inflater(), parent, false))
|
||||
}
|
||||
|
||||
override fun bindGroup(holder: GroupedListHolder, group: BalanceBreakdownTotal) {
|
||||
require(holder is BalanceTotalHolder)
|
||||
holder.bind(group)
|
||||
}
|
||||
|
||||
override fun bindChild(holder: GroupedListHolder, child: BalanceBreakdownAmount) {
|
||||
require(holder is BalanceAmountHolder)
|
||||
holder.bind(child)
|
||||
}
|
||||
}
|
||||
|
||||
class BalanceTotalHolder(
|
||||
private val binder: ItemBalanceBreakdownTotalBinding,
|
||||
) : GroupedListHolder(binder.root) {
|
||||
|
||||
fun bind(item: BalanceBreakdownTotal) {
|
||||
binder.itemBreakdownTotalIcon.setImageResource(item.iconRes)
|
||||
binder.itemBreakdownTotalName.text = item.name
|
||||
binder.itemBreakdownTotalPercentage.text = item.percentage
|
||||
binder.itemBreakdownTotal.text = item.fiatAmount
|
||||
}
|
||||
}
|
||||
|
||||
class BalanceAmountHolder(
|
||||
private val binder: ItemBalanceBreakdownAmountBinding,
|
||||
) : GroupedListHolder(binder.root) {
|
||||
|
||||
fun bind(item: BalanceBreakdownAmount) {
|
||||
binder.balanceBreakdownItemDetail.setTitle(item.name)
|
||||
binder.balanceBreakdownItemDetail.showAmount(item.amount)
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : BaseGroupedDiffCallback<BalanceBreakdownTotal, BalanceBreakdownAmount>(BalanceBreakdownTotal::class.java) {
|
||||
|
||||
override fun areGroupItemsTheSame(oldItem: BalanceBreakdownTotal, newItem: BalanceBreakdownTotal): Boolean {
|
||||
return oldItem.name == newItem.name
|
||||
}
|
||||
|
||||
override fun areGroupContentsTheSame(oldItem: BalanceBreakdownTotal, newItem: BalanceBreakdownTotal): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areChildItemsTheSame(oldItem: BalanceBreakdownAmount, newItem: BalanceBreakdownAmount): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areChildContentsTheSame(oldItem: BalanceBreakdownAmount, newItem: BalanceBreakdownAmount): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getGroupChangePayload(oldItem: BalanceBreakdownTotal, newItem: BalanceBreakdownTotal): Any? {
|
||||
return true
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.breakdown
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import io.novafoundation.nova.common.view.bottomSheet.BaseBottomSheet
|
||||
import io.novafoundation.nova.feature_assets.databinding.FragmentBalanceBreakdownBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.TotalBalanceBreakdownModel
|
||||
|
||||
class BalanceBreakdownBottomSheet(context: Context) : BaseBottomSheet<FragmentBalanceBreakdownBinding>(context) {
|
||||
|
||||
override val binder: FragmentBalanceBreakdownBinding = FragmentBalanceBreakdownBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
private var totalBreakdown: TotalBalanceBreakdownModel? = null
|
||||
|
||||
private val adapter = BalanceBreakdownAdapter()
|
||||
|
||||
init {
|
||||
binder.balanceBreakdownList.adapter = adapter
|
||||
}
|
||||
|
||||
fun setBalanceBreakdown(totalBreakdown: TotalBalanceBreakdownModel) {
|
||||
this.totalBreakdown = totalBreakdown
|
||||
binder.balanceBreakdownTotal.text = totalBreakdown.totalFiat
|
||||
adapter.submitList(totalBreakdown.breakdown)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel
|
||||
|
||||
interface BalanceBreakdownItem {
|
||||
val name: String
|
||||
}
|
||||
|
||||
data class BalanceBreakdownAmount(
|
||||
override val name: String,
|
||||
val amount: AmountModel
|
||||
) : BalanceBreakdownItem
|
||||
|
||||
data class BalanceBreakdownTotal(
|
||||
override val name: String,
|
||||
val fiatAmount: String,
|
||||
@DrawableRes val iconRes: Int,
|
||||
val percentage: String
|
||||
) : BalanceBreakdownItem
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model
|
||||
|
||||
class TotalBalanceBreakdownModel(
|
||||
val totalFiat: String,
|
||||
val breakdown: List<BalanceBreakdownItem>
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common
|
||||
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings
|
||||
|
||||
fun ExpandableAnimationSettings.Companion.createForAssets() = ExpandableAnimationSettings(400, AccelerateDecelerateInterpolator())
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common
|
||||
|
||||
import io.novafoundation.nova.common.data.model.AssetViewMode
|
||||
import io.novafoundation.nova.common.utils.combineToPair
|
||||
import io.novafoundation.nova.common.utils.shareInBackground
|
||||
import io.novafoundation.nova.common.utils.throttleLast
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.models.byNetworks
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.models.byTokens
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class AssetListMixinFactory(
|
||||
private val walletInteractor: WalletInteractor,
|
||||
private val assetsListInteractor: AssetsListInteractor,
|
||||
private val externalBalancesInteractor: ExternalBalancesInteractor,
|
||||
private val expandableAssetsMixinFactory: ExpandableAssetsMixinFactory
|
||||
) {
|
||||
|
||||
fun create(coroutineScope: CoroutineScope): AssetListMixin = RealAssetListMixin(
|
||||
walletInteractor,
|
||||
assetsListInteractor,
|
||||
externalBalancesInteractor,
|
||||
expandableAssetsMixinFactory,
|
||||
coroutineScope
|
||||
)
|
||||
}
|
||||
|
||||
interface AssetListMixin {
|
||||
|
||||
val assetsViewModeFlow: Flow<AssetViewMode>
|
||||
|
||||
val externalBalancesFlow: SharedFlow<List<ExternalBalance>>
|
||||
|
||||
val assetsFlow: Flow<List<Asset>>
|
||||
|
||||
val assetModelsFlow: Flow<List<BalanceListRvItem>>
|
||||
|
||||
fun expandToken(tokenGroupUi: TokenGroupUi)
|
||||
|
||||
suspend fun switchViewMode()
|
||||
}
|
||||
|
||||
class RealAssetListMixin(
|
||||
private val walletInteractor: WalletInteractor,
|
||||
private val assetsListInteractor: AssetsListInteractor,
|
||||
private val externalBalancesInteractor: ExternalBalancesInteractor,
|
||||
private val expandableAssetsMixinFactory: ExpandableAssetsMixinFactory,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) : AssetListMixin, CoroutineScope by coroutineScope {
|
||||
|
||||
override val assetsFlow = walletInteractor.assetsFlow()
|
||||
.shareInBackground()
|
||||
|
||||
private val filteredAssetsFlow = walletInteractor.filterAssets(assetsFlow)
|
||||
.shareInBackground()
|
||||
|
||||
override val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances()
|
||||
.shareInBackground()
|
||||
|
||||
override val assetsViewModeFlow = assetsListInteractor.assetsViewModeFlow()
|
||||
.shareInBackground()
|
||||
|
||||
private val throttledBalance = combineToPair(filteredAssetsFlow, externalBalancesFlow)
|
||||
.throttleLast(300.milliseconds)
|
||||
|
||||
private val assetsByViewMode = combine(
|
||||
throttledBalance,
|
||||
assetsViewModeFlow
|
||||
) { (assets, externalBalances), viewMode ->
|
||||
when (viewMode) {
|
||||
AssetViewMode.NETWORKS -> walletInteractor.groupAssetsByNetwork(assets, externalBalances).byNetworks()
|
||||
AssetViewMode.TOKENS -> walletInteractor.groupAssetsByToken(assets, externalBalances).byTokens()
|
||||
}
|
||||
}.shareInBackground()
|
||||
|
||||
private val expandableAssetsMixin = expandableAssetsMixinFactory.create(assetsByViewMode)
|
||||
|
||||
override val assetModelsFlow = expandableAssetsMixin.assetModelsFlow
|
||||
.shareInBackground()
|
||||
|
||||
override fun expandToken(tokenGroupUi: TokenGroupUi) {
|
||||
expandableAssetsMixin.expandToken(tokenGroupUi)
|
||||
}
|
||||
|
||||
override suspend fun switchViewMode() {
|
||||
expandableAssetsMixin.switchViewMode()
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.view.View
|
||||
import androidx.core.graphics.toRect
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.utils.dp
|
||||
import io.novafoundation.nova.common.utils.dpF
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAdapter
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableItemDecoration
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimationItemState
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.expandingFraction
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.flippedFraction
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetGroupViewHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AssetTokensDecoration(
|
||||
private val context: Context,
|
||||
private val adapter: ExpandableAdapter,
|
||||
animator: ExpandableAnimator
|
||||
) : ExpandableItemDecoration(
|
||||
adapter,
|
||||
animator
|
||||
) {
|
||||
private val argbEvaluator = ArgbEvaluator()
|
||||
|
||||
private val childrenBlockCollapsedHorizontalMargin = 16.dp(context)
|
||||
private val childrenBlockCollapsedHeight = 4.dp(context)
|
||||
|
||||
private val blockRadiusCollapsed = 4.dpF(context)
|
||||
private val blockRadiusExpanded = 12.dpF(context)
|
||||
private val blockRadiusDelta = blockRadiusExpanded - blockRadiusCollapsed
|
||||
|
||||
private val blockColor = context.getColor(R.color.block_background)
|
||||
private val hidedBlockColor = context.getColor(R.color.hided_networks_block_background)
|
||||
private val transparentColor = Color.TRANSPARENT
|
||||
private val dividerColor = context.getColor(R.color.divider)
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private var drawingPath = Path()
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val viewHolder = parent.getChildViewHolder(view)
|
||||
|
||||
if (viewHolder.bindingAdapterPosition == 0) return
|
||||
|
||||
if (viewHolder is TokenAssetGroupViewHolder) {
|
||||
if (viewHolder.bindingAdapterPosition == adapter.getItems().size - 1) {
|
||||
outRect.set(0, 12.dp(context), 0, 12.dp(context))
|
||||
} else {
|
||||
outRect.set(0, 12.dp(context), 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDrawGroup(
|
||||
canvas: Canvas,
|
||||
animationState: ExpandableAnimationItemState,
|
||||
recyclerView: RecyclerView,
|
||||
parentItem: ExpandableParentItem,
|
||||
parent: RecyclerView.ViewHolder?,
|
||||
children: List<RecyclerView.ViewHolder>
|
||||
) {
|
||||
val expandingFraction = animationState.expandingFraction()
|
||||
|
||||
val parentBounds = parentBounds(parent)
|
||||
if (parentBounds != null) {
|
||||
drawParentBlock(parentBounds, canvas, expandingFraction)
|
||||
}
|
||||
|
||||
// Don't draw children background if it's a single item
|
||||
if (parentItem is TokenGroupUi && parentItem.singleItemGroup) return
|
||||
|
||||
val childrenBlockBounds = getChildrenBlockBounds(animationState, recyclerView, parent, children)
|
||||
drawChildrenBlock(expandingFraction, childrenBlockBounds, canvas)
|
||||
clipChildren(children, childrenBlockBounds)
|
||||
}
|
||||
|
||||
private fun clipChildren(children: List<RecyclerView.ViewHolder>, childrenBlockBounds: RectF) {
|
||||
val childrenBlock = childrenBlockBounds.toRect()
|
||||
children.forEach {
|
||||
val childrenBottomClipInset = (it.itemView.bottom + it.itemView.translationY.roundToInt()) - childrenBlock.bottom
|
||||
val childrenTopClipInset = childrenBlock.top - (it.itemView.top + it.itemView.translationY.roundToInt())
|
||||
if (childrenTopClipInset > 0 || childrenBottomClipInset > 0) {
|
||||
it.itemView.clipBounds = Rect(
|
||||
0,
|
||||
childrenTopClipInset,
|
||||
it.itemView.width,
|
||||
it.itemView.height - childrenBottomClipInset
|
||||
)
|
||||
} else {
|
||||
it.itemView.clipBounds = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawChildrenBlock(expandingFraction: Float, childrenBlockBounds: RectF, canvas: Canvas) {
|
||||
val animatedBlockRadius = blockRadiusDelta * expandingFraction
|
||||
childrenBlockBounds.toPath(drawingPath, topRadius = 0f, bottomRadius = blockRadiusCollapsed + animatedBlockRadius * expandingFraction)
|
||||
paint.color = argbEvaluator.evaluate(expandingFraction, hidedBlockColor, blockColor) as Int
|
||||
canvas.drawPath(drawingPath, paint)
|
||||
}
|
||||
|
||||
private fun drawParentBlock(
|
||||
parentBounds: RectF,
|
||||
canvas: Canvas,
|
||||
expandingFraction: Float
|
||||
) {
|
||||
val path = Path()
|
||||
val bottomRadius = blockRadiusExpanded * expandingFraction.flippedFraction()
|
||||
parentBounds.toPath(path, topRadius = blockRadiusExpanded, bottomRadius = bottomRadius)
|
||||
paint.color = blockColor
|
||||
canvas.drawPath(path, paint)
|
||||
|
||||
drawParentDivider(expandingFraction, bottomRadius, canvas, parentBounds)
|
||||
}
|
||||
|
||||
private fun drawParentDivider(
|
||||
expandingFraction: Float,
|
||||
dividerHorizontalMargin: Float,
|
||||
canvas: Canvas,
|
||||
parentBounds: RectF
|
||||
) {
|
||||
linePaint.color = argbEvaluator.evaluate(expandingFraction, transparentColor, dividerColor) as Int
|
||||
canvas.drawLine(
|
||||
parentBounds.left + dividerHorizontalMargin,
|
||||
parentBounds.bottom,
|
||||
parentBounds.right - dividerHorizontalMargin,
|
||||
parentBounds.bottom,
|
||||
linePaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun parentBounds(parent: RecyclerView.ViewHolder?): RectF? {
|
||||
if (parent == null) return null
|
||||
|
||||
return parent.itemView.let {
|
||||
RectF(
|
||||
it.left.toFloat(),
|
||||
it.top.toFloat() + it.translationY,
|
||||
it.right.toFloat(),
|
||||
it.bottom.toFloat() + it.translationY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChildrenBlockBounds(
|
||||
animationState: ExpandableAnimationItemState,
|
||||
recyclerView: RecyclerView,
|
||||
parent: RecyclerView.ViewHolder?,
|
||||
children: List<RecyclerView.ViewHolder>
|
||||
): RectF {
|
||||
val lastChild = children.maxByOrNull { it.itemView.bottom }
|
||||
|
||||
val parentTranslationY = parent?.itemView?.translationY ?: 0f
|
||||
val childTranslationY = lastChild?.itemView?.translationY ?: 0f
|
||||
|
||||
val top = (parent?.itemView?.bottom ?: 0) + parentTranslationY
|
||||
val bottom = (lastChild?.itemView?.bottom?.toFloat() ?: top).coerceAtLeast(top)
|
||||
val left = parent?.itemView?.left ?: lastChild?.itemView?.left ?: recyclerView.left
|
||||
val right = parent?.itemView?.right ?: lastChild?.itemView?.right ?: recyclerView.right
|
||||
|
||||
val expandingFraction = animationState.expandingFraction()
|
||||
val flippedExpandingFraction = expandingFraction.flippedFraction()
|
||||
val heightDelta = (bottom - top)
|
||||
return RectF(
|
||||
left + childrenBlockCollapsedHorizontalMargin * flippedExpandingFraction,
|
||||
top,
|
||||
right - childrenBlockCollapsedHorizontalMargin * flippedExpandingFraction,
|
||||
top + childrenBlockCollapsedHeight + heightDelta * expandingFraction + childTranslationY
|
||||
)
|
||||
}
|
||||
|
||||
private fun RectF.toPath(path: Path, topRadius: Float, bottomRadius: Float) {
|
||||
path.reset()
|
||||
path.addRoundRect(
|
||||
this,
|
||||
floatArrayOf(topRadius, topRadius, topRadius, topRadius, bottomRadius, bottomRadius, bottomRadius, bottomRadius),
|
||||
Path.Direction.CW
|
||||
)
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common
|
||||
|
||||
import android.view.ViewPropertyAnimator
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableItemAnimator
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator
|
||||
|
||||
private const val REMOVE_SCALE = 0.9f
|
||||
|
||||
class AssetTokensItemAnimator(
|
||||
settings: ExpandableAnimationSettings,
|
||||
expandableAnimator: ExpandableAnimator
|
||||
) : ExpandableItemAnimator(
|
||||
settings,
|
||||
expandableAnimator
|
||||
) {
|
||||
|
||||
override fun preAddImpl(holder: RecyclerView.ViewHolder) {
|
||||
holder.itemView.alpha = 0f
|
||||
holder.itemView.scaleX = REMOVE_SCALE
|
||||
holder.itemView.scaleY = REMOVE_SCALE
|
||||
}
|
||||
|
||||
override fun getAddAnimator(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
|
||||
return holder.itemView.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
}
|
||||
|
||||
override fun preRemoveImpl(holder: RecyclerView.ViewHolder) {
|
||||
resetAddState(holder)
|
||||
}
|
||||
|
||||
override fun getRemoveAnimator(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
|
||||
return holder.itemView.animate()
|
||||
.alpha(0f)
|
||||
.scaleX(REMOVE_SCALE)
|
||||
.scaleY(REMOVE_SCALE)
|
||||
}
|
||||
|
||||
override fun preMoveImpl(holder: RecyclerView.ViewHolder, fromY: Int, toY: Int) {
|
||||
val yDelta = toY - fromY
|
||||
holder.itemView.translationY += -yDelta
|
||||
}
|
||||
|
||||
override fun getMoveAnimator(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
|
||||
return holder.itemView.animate()
|
||||
.translationY(0f)
|
||||
}
|
||||
|
||||
override fun endAnimation(viewHolder: RecyclerView.ViewHolder) {
|
||||
super.endAnimation(viewHolder)
|
||||
|
||||
viewHolder.itemView.translationY = 0f
|
||||
viewHolder.itemView.alpha = 1f
|
||||
viewHolder.itemView.scaleX = 1f
|
||||
viewHolder.itemView.scaleY = 1f
|
||||
}
|
||||
|
||||
override fun resetAddState(holder: RecyclerView.ViewHolder) {
|
||||
holder.itemView.alpha = 1f
|
||||
holder.itemView.scaleX = 1f
|
||||
holder.itemView.scaleY = 1f
|
||||
}
|
||||
|
||||
override fun resetRemoveState(holder: RecyclerView.ViewHolder) {
|
||||
holder.itemView.alpha = 1f
|
||||
holder.itemView.scaleX = 1f
|
||||
holder.itemView.scaleY = 1f
|
||||
}
|
||||
|
||||
override fun resetMoveState(holder: RecyclerView.ViewHolder) {
|
||||
holder.itemView.translationY = 0f
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.PayloadGenerator
|
||||
import io.novafoundation.nova.common.list.resolvePayload
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAdapter
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableBaseItem
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemNetworkAssetBinding
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemNetworkAssetGroupBinding
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemTokenAssetBinding
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemTokenAssetGroupBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetGroupViewHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetViewHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetGroupViewHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetViewHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkAssetUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkGroupUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.AssetModel
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
private val priceRateExtractor = { asset: AssetModel -> asset.token.rate }
|
||||
private val recentChangeExtractor = { asset: AssetModel -> asset.token.recentRateChange }
|
||||
private val amountExtractor = { asset: AssetModel -> asset.amount }
|
||||
|
||||
private val tokenGroupPriceRateExtractor = { group: TokenGroupUi -> group.rate }
|
||||
private val tokenGroupRecentChangeExtractor = { group: TokenGroupUi -> group.recentRateChange }
|
||||
private val tokenGroupAmountExtractor = { group: TokenGroupUi -> group.balance }
|
||||
private val tokenGroupTypeExtractor = { group: TokenGroupUi -> group.groupType }
|
||||
|
||||
const val TYPE_NETWORK_GROUP = 0
|
||||
const val TYPE_NETWORK_ASSET = 1
|
||||
const val TYPE_TOKEN_GROUP = 2
|
||||
const val TYPE_TOKEN_ASSET = 3
|
||||
|
||||
class BalanceListAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: ItemAssetHandler,
|
||||
) : ListAdapter<BalanceListRvItem, ViewHolder>(DiffCallback), ExpandableAdapter {
|
||||
|
||||
interface ItemAssetHandler {
|
||||
fun assetClicked(asset: Chain.Asset)
|
||||
|
||||
fun tokenGroupClicked(tokenGroup: TokenGroupUi)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return when (viewType) {
|
||||
TYPE_NETWORK_GROUP -> NetworkAssetGroupViewHolder(ItemNetworkAssetGroupBinding.inflate(parent.inflater(), parent, false))
|
||||
TYPE_NETWORK_ASSET -> NetworkAssetViewHolder(ItemNetworkAssetBinding.inflate(parent.inflater(), parent, false), imageLoader)
|
||||
TYPE_TOKEN_GROUP -> TokenAssetGroupViewHolder(ItemTokenAssetGroupBinding.inflate(parent.inflater(), parent, false), imageLoader, itemHandler)
|
||||
TYPE_TOKEN_ASSET -> TokenAssetViewHolder(ItemTokenAssetBinding.inflate(parent.inflater(), parent, false), imageLoader)
|
||||
else -> error("Unknown view type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
return when (holder) {
|
||||
is NetworkAssetGroupViewHolder -> holder.bind(getItem(position) as NetworkGroupUi)
|
||||
is NetworkAssetViewHolder -> holder.bind(getItem(position) as NetworkAssetUi, itemHandler)
|
||||
is TokenAssetGroupViewHolder -> holder.bind(getItem(position) as TokenGroupUi)
|
||||
is TokenAssetViewHolder -> holder.bind(getItem(position) as TokenAssetUi, itemHandler)
|
||||
else -> error("Unknown holder")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
when (holder) {
|
||||
is NetworkAssetViewHolder -> {
|
||||
val item = getItem(position) as NetworkAssetUi
|
||||
resolvePayload(holder, position, payloads) {
|
||||
when (it) {
|
||||
priceRateExtractor -> holder.bindPriceInfo(item.asset)
|
||||
recentChangeExtractor -> holder.bindRecentChange(item.asset)
|
||||
amountExtractor -> holder.bindTotal(item.asset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TokenAssetViewHolder -> {
|
||||
val item = getItem(position) as TokenAssetUi
|
||||
holder.updateExpandableItem(item)
|
||||
|
||||
resolvePayload(holder, position, payloads) {
|
||||
when (it) {
|
||||
amountExtractor -> holder.bindTotal(item.asset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TokenAssetGroupViewHolder -> {
|
||||
val item = getItem(position) as TokenGroupUi
|
||||
holder.updateExpandableItem(item)
|
||||
|
||||
resolvePayload(holder, position, payloads) {
|
||||
when (it) {
|
||||
tokenGroupPriceRateExtractor -> holder.bindPriceRate(item)
|
||||
tokenGroupRecentChangeExtractor -> holder.bindRecentChange(item)
|
||||
tokenGroupAmountExtractor -> holder.bindTotal(item)
|
||||
tokenGroupTypeExtractor -> holder.bindGroupType(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> super.onBindViewHolder(holder, position, payloads)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is NetworkGroupUi -> TYPE_NETWORK_GROUP
|
||||
is NetworkAssetUi -> TYPE_NETWORK_ASSET
|
||||
is TokenGroupUi -> TYPE_TOKEN_GROUP
|
||||
is TokenAssetUi -> TYPE_TOKEN_ASSET
|
||||
else -> error("Unknown item type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItems(): List<ExpandableBaseItem> {
|
||||
return currentList
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<BalanceListRvItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: BalanceListRvItem, newItem: BalanceListRvItem): Boolean {
|
||||
return oldItem.itemId == newItem.itemId
|
||||
}
|
||||
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: BalanceListRvItem, newItem: BalanceListRvItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: BalanceListRvItem, newItem: BalanceListRvItem): Any? {
|
||||
return when {
|
||||
oldItem is NetworkAssetUi && newItem is NetworkAssetUi -> NetworkAssetPayloadGenerator.diff(oldItem.asset, newItem.asset)
|
||||
|
||||
oldItem is TokenAssetUi && newItem is TokenAssetUi -> TokenAssetPayloadGenerator.diff(oldItem.asset, newItem.asset)
|
||||
|
||||
oldItem is TokenGroupUi && newItem is TokenGroupUi -> TokenGroupAssetPayloadGenerator.diff(oldItem, newItem)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object NetworkAssetPayloadGenerator : PayloadGenerator<AssetModel>(
|
||||
priceRateExtractor,
|
||||
recentChangeExtractor,
|
||||
amountExtractor
|
||||
)
|
||||
|
||||
private object TokenAssetPayloadGenerator : PayloadGenerator<AssetModel>(
|
||||
amountExtractor
|
||||
)
|
||||
|
||||
private object TokenGroupAssetPayloadGenerator : PayloadGenerator<TokenGroupUi>(
|
||||
tokenGroupPriceRateExtractor,
|
||||
tokenGroupRecentChangeExtractor,
|
||||
tokenGroupAmountExtractor,
|
||||
tokenGroupTypeExtractor
|
||||
)
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common
|
||||
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.watchOnly.WatchOnlyMissingKeysPresenter
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class ControllableAssetCheckMixin(
|
||||
private val missingKeysPresenter: WatchOnlyMissingKeysPresenter,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val resourceManager: ResourceManager
|
||||
) {
|
||||
|
||||
val acknowledgeLedgerWarning = actionAwaitableMixinFactory.confirmingAction<String>()
|
||||
|
||||
suspend fun check(metaAccount: MetaAccount, chainAsset: Chain.Asset, action: () -> Unit) {
|
||||
when {
|
||||
metaAccount.type == LightMetaAccount.Type.LEDGER_LEGACY && chainAsset.type is Chain.Asset.Type.Orml -> showLedgerAssetNotSupportedWarning(
|
||||
chainAsset
|
||||
)
|
||||
metaAccount.type == LightMetaAccount.Type.WATCH_ONLY -> missingKeysPresenter.presentNoKeysFound()
|
||||
else -> action()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showLedgerAssetNotSupportedWarning(chainAsset: Chain.Asset) {
|
||||
val assetSymbol = chainAsset.symbol
|
||||
val warningMessage = resourceManager.getString(R.string.assets_receive_ledger_not_supported_message, assetSymbol, assetSymbol)
|
||||
|
||||
acknowledgeLedgerWarning.awaitAction(warningMessage)
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common
|
||||
|
||||
import io.novafoundation.nova.common.data.model.switch
|
||||
import io.novafoundation.nova.common.data.repository.AssetsViewModeRepository
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.utils.combineToTuple4
|
||||
import io.novafoundation.nova.common.utils.toggle
|
||||
import io.novafoundation.nova.common.utils.updateValue
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.models.AssetsByViewModeResult
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.NetworkAssetFormatter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.NetworkAssetFormatterFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.TokenAssetFormatter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.TokenAssetFormatterFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
|
||||
class ExpandableAssetsMixinFactory(
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val currencyInteractor: CurrencyInteractor,
|
||||
private val assetsViewModeRepository: AssetsViewModeRepository,
|
||||
private val amountFormatterProvider: MaskableValueFormatterProvider,
|
||||
private val networkAssetFormatterFactory: NetworkAssetFormatterFactory,
|
||||
private val tokenAssetFormatterFactory: TokenAssetFormatterFactory,
|
||||
) {
|
||||
|
||||
fun create(assetsFlow: Flow<AssetsByViewModeResult>): ExpandableAssetsMixin {
|
||||
return RealExpandableAssetsMixin(
|
||||
assetsFlow,
|
||||
currencyInteractor,
|
||||
amountFormatterProvider,
|
||||
networkAssetFormatterFactory,
|
||||
tokenAssetFormatterFactory,
|
||||
assetIconProvider,
|
||||
assetsViewModeRepository
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface ExpandableAssetsMixin {
|
||||
|
||||
val assetModelsFlow: Flow<List<BalanceListRvItem>>
|
||||
|
||||
fun expandToken(tokenGroupUi: TokenGroupUi)
|
||||
|
||||
suspend fun switchViewMode()
|
||||
}
|
||||
|
||||
class RealExpandableAssetsMixin(
|
||||
assetsFlow: Flow<AssetsByViewModeResult>,
|
||||
currencyInteractor: CurrencyInteractor,
|
||||
amountFormatterProvider: MaskableValueFormatterProvider,
|
||||
networkAssetFormatterFactory: NetworkAssetFormatterFactory,
|
||||
tokenAssetFormatterFactory: TokenAssetFormatterFactory,
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val assetsViewModeRepository: AssetsViewModeRepository
|
||||
) : ExpandableAssetsMixin {
|
||||
|
||||
private val assetsFormatters = amountFormatterProvider.provideFormatter()
|
||||
.map {
|
||||
AssetMappers(
|
||||
networkAssetFormatterFactory.create(it),
|
||||
tokenAssetFormatterFactory.create(it)
|
||||
)
|
||||
}
|
||||
|
||||
private val selectedCurrency = currencyInteractor.observeSelectCurrency()
|
||||
|
||||
private val expandedTokenIdsFlow = MutableStateFlow(setOf<String>())
|
||||
|
||||
override val assetModelsFlow: Flow<List<BalanceListRvItem>> = combineToTuple4(
|
||||
assetsFlow,
|
||||
expandedTokenIdsFlow,
|
||||
selectedCurrency,
|
||||
assetsFormatters
|
||||
).mapLatest { (assetsByViewMode, expandedTokens, currency, assetMappers) ->
|
||||
when (assetsByViewMode) {
|
||||
is AssetsByViewModeResult.ByNetworks -> assetMappers.networkAssetMapper.mapGroupedAssetsToUi(
|
||||
groupedAssets = assetsByViewMode.assets,
|
||||
assetIconProvider = assetIconProvider,
|
||||
currency = currency
|
||||
)
|
||||
|
||||
is AssetsByViewModeResult.ByTokens -> assetMappers.tokenAssetFormatter.mapGroupedAssetsToUi(
|
||||
groupedTokens = assetsByViewMode.tokens,
|
||||
assetIconProvider = assetIconProvider,
|
||||
assetFilter = { groupId, assetsInGroup -> filterTokens(groupId, assetsInGroup, expandedTokens) }
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override fun expandToken(tokenGroupUi: TokenGroupUi) {
|
||||
expandedTokenIdsFlow.updateValue { it.toggle(tokenGroupUi.itemId) }
|
||||
}
|
||||
|
||||
override suspend fun switchViewMode() {
|
||||
expandedTokenIdsFlow.value = emptySet()
|
||||
|
||||
val assetViewMode = assetsViewModeRepository.getAssetViewMode()
|
||||
assetsViewModeRepository.setAssetsViewMode(assetViewMode.switch())
|
||||
}
|
||||
|
||||
private fun filterTokens(groupId: String, assets: List<TokenAssetUi>, expandedGroups: Set<String>): List<TokenAssetUi> {
|
||||
if (groupId in expandedGroups) {
|
||||
return filterIfSingleItem(assets)
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun filterIfSingleItem(assets: List<TokenAssetUi>): List<TokenAssetUi> {
|
||||
return if (assets.size <= 1) {
|
||||
emptyList()
|
||||
} else {
|
||||
assets
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AssetMappers(
|
||||
val networkAssetMapper: NetworkAssetFormatter,
|
||||
val tokenAssetFormatter: TokenAssetFormatter
|
||||
)
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.utils.dp
|
||||
import io.novafoundation.nova.common.view.shape.addRipple
|
||||
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Note - clients are required to call [RecyclerView.invalidateItemDecorations] in [ListAdapter.submitList] callback due to issues with DiffUtil.
|
||||
* The issue is that this decoration does not currently support partial list updates and assumes it will be iterated over whole list
|
||||
* TODO update decoration to not require this invalidation
|
||||
*/
|
||||
class AssetBaseDecoration(
|
||||
private val background: Drawable,
|
||||
private val assetsAdapter: ListAdapter<*, *>,
|
||||
context: Context,
|
||||
private val preferences: AssetDecorationPreferences
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
companion object;
|
||||
|
||||
private val bounds = Rect()
|
||||
|
||||
// used to hide rounded corners for the last group to simulate effect of not-closed group
|
||||
private val finalGroupExtraPadding = 20.dp(context)
|
||||
|
||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (assetsAdapter.itemCount == 0) return
|
||||
|
||||
var groupTop: Int? = null
|
||||
|
||||
parent.children.forEachIndexed { index, view ->
|
||||
val viewHolder = parent.getChildViewHolder(view)
|
||||
|
||||
if (shouldSkip(viewHolder)) return@forEachIndexed
|
||||
|
||||
val bindingPosition = viewHolder.bindingAdapterPosition
|
||||
|
||||
val nextType = assetsAdapter.getItemViewTypeOrNull(bindingPosition + 1)
|
||||
|
||||
if (groupTop == null) {
|
||||
parent.getDecoratedBoundsWithMargins(view, bounds)
|
||||
groupTop = bounds.top + view.translationY.roundToInt()
|
||||
}
|
||||
|
||||
when {
|
||||
// if group is finished
|
||||
isFinalItemInGroup(nextType) -> {
|
||||
parent.getDecoratedBoundsWithMargins(view, bounds)
|
||||
bounds.set(view.left, bounds.top, view.right, bounds.bottom)
|
||||
|
||||
val groupBottom = bounds.bottom + view.translationY.roundToInt() - preferences.outerGroupPadding(viewHolder)
|
||||
|
||||
background.setBounds(bounds.left, groupTop!!, bounds.right, groupBottom)
|
||||
background.draw(c)
|
||||
|
||||
if (index + 1 < parent.childCount) {
|
||||
val nextView = parent.getChildAt(index + 1)
|
||||
parent.getDecoratedBoundsWithMargins(nextView, bounds)
|
||||
|
||||
groupTop = bounds.top + view.translationY.roundToInt()
|
||||
}
|
||||
}
|
||||
// draw last group
|
||||
index == parent.childCount - 1 -> {
|
||||
parent.getDecoratedBoundsWithMargins(view, bounds)
|
||||
bounds.set(view.left, bounds.top, view.right, bounds.bottom)
|
||||
|
||||
val groupBottom = bounds.bottom + view.translationY.roundToInt() + finalGroupExtraPadding
|
||||
background.setBounds(bounds.left, groupTop!!, bounds.right, groupBottom)
|
||||
background.draw(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val viewHolder = parent.getChildViewHolder(view)
|
||||
|
||||
if (shouldSkip(viewHolder)) {
|
||||
outRect.set(0, 0, 0, 0)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val adapterPosition = viewHolder.bindingAdapterPosition
|
||||
|
||||
val nextType = assetsAdapter.getItemViewTypeOrNull(adapterPosition + 1)
|
||||
|
||||
val bottom = if (isFinalItemInGroup(nextType)) {
|
||||
preferences.outerGroupPadding(viewHolder) + preferences.innerGroupPadding(viewHolder)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
outRect.set(0, 0, 0, bottom)
|
||||
}
|
||||
|
||||
private fun RecyclerView.Adapter<*>.getItemViewTypeOrNull(position: Int): Int? {
|
||||
if (position < 0 || position >= itemCount) return null
|
||||
|
||||
return getItemViewType(position)
|
||||
}
|
||||
|
||||
private fun isFinalItemInGroup(nextType: Int?): Boolean {
|
||||
return nextType == null || preferences.isGroupItem(nextType)
|
||||
}
|
||||
|
||||
private fun shouldSkip(viewHolder: RecyclerView.ViewHolder): Boolean {
|
||||
val noPosition = viewHolder.bindingAdapterPosition == RecyclerView.NO_POSITION
|
||||
val unsupportedViewHolder = !preferences.shouldUseViewHolder(viewHolder)
|
||||
|
||||
return noPosition || unsupportedViewHolder
|
||||
}
|
||||
}
|
||||
|
||||
fun AssetBaseDecoration.Companion.applyDefaultTo(
|
||||
recyclerView: RecyclerView,
|
||||
adapter: ListAdapter<*, *>,
|
||||
preferences: AssetDecorationPreferences = NetworkAssetDecorationPreferences()
|
||||
) {
|
||||
val groupBackground = with(recyclerView.context) {
|
||||
addRipple(getRoundedCornerDrawable(R.color.block_background))
|
||||
}
|
||||
val decoration = AssetBaseDecoration(
|
||||
background = groupBackground,
|
||||
assetsAdapter = adapter,
|
||||
context = recyclerView.context,
|
||||
preferences = preferences
|
||||
)
|
||||
recyclerView.addItemDecoration(decoration)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import io.novafoundation.nova.common.utils.dp
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.TYPE_NETWORK_GROUP
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.TYPE_TOKEN_GROUP
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetGroupViewHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.NetworkAssetViewHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.holders.TokenAssetGroupViewHolder
|
||||
|
||||
interface AssetDecorationPreferences {
|
||||
|
||||
fun innerGroupPadding(viewHolder: ViewHolder): Int
|
||||
|
||||
fun outerGroupPadding(viewHolder: ViewHolder): Int
|
||||
|
||||
fun isGroupItem(viewType: Int): Boolean
|
||||
|
||||
fun shouldUseViewHolder(viewHolder: ViewHolder): Boolean
|
||||
}
|
||||
|
||||
class NetworkAssetDecorationPreferences : AssetDecorationPreferences {
|
||||
|
||||
override fun innerGroupPadding(viewHolder: ViewHolder): Int {
|
||||
return 8.dp(viewHolder.itemView.context)
|
||||
}
|
||||
|
||||
override fun outerGroupPadding(viewHolder: ViewHolder): Int {
|
||||
return 8.dp(viewHolder.itemView.context)
|
||||
}
|
||||
|
||||
override fun isGroupItem(viewType: Int): Boolean {
|
||||
return viewType == TYPE_NETWORK_GROUP
|
||||
}
|
||||
|
||||
override fun shouldUseViewHolder(viewHolder: ViewHolder): Boolean {
|
||||
return viewHolder is NetworkAssetViewHolder ||
|
||||
viewHolder is NetworkAssetGroupViewHolder
|
||||
}
|
||||
}
|
||||
|
||||
class TokenAssetGroupDecorationPreferences : AssetDecorationPreferences {
|
||||
|
||||
override fun innerGroupPadding(viewHolder: ViewHolder): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun outerGroupPadding(viewHolder: ViewHolder): Int {
|
||||
return 8.dp(viewHolder.itemView.context)
|
||||
}
|
||||
|
||||
override fun isGroupItem(viewType: Int): Boolean {
|
||||
return viewType == TYPE_TOKEN_GROUP
|
||||
}
|
||||
|
||||
override fun shouldUseViewHolder(viewHolder: ViewHolder): Boolean {
|
||||
return viewHolder is TokenAssetGroupViewHolder
|
||||
}
|
||||
}
|
||||
|
||||
class CompoundAssetDecorationPreferences(private vararg val preferences: AssetDecorationPreferences) : AssetDecorationPreferences {
|
||||
|
||||
override fun innerGroupPadding(viewHolder: ViewHolder): Int {
|
||||
val firstPreferences = preferences.firstOrNull { it.shouldUseViewHolder(viewHolder) }
|
||||
return firstPreferences?.innerGroupPadding(viewHolder) ?: 0
|
||||
}
|
||||
|
||||
override fun outerGroupPadding(viewHolder: ViewHolder): Int {
|
||||
val firstPreferences = preferences.firstOrNull { it.shouldUseViewHolder(viewHolder) }
|
||||
return firstPreferences?.outerGroupPadding(viewHolder) ?: 0
|
||||
}
|
||||
|
||||
override fun isGroupItem(viewType: Int): Boolean {
|
||||
return preferences.any { it.isGroupItem(viewType) }
|
||||
}
|
||||
|
||||
override fun shouldUseViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
|
||||
return preferences.any { it.shouldUseViewHolder(viewHolder) }
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.buySell
|
||||
|
||||
import io.novafoundation.nova.common.mixin.restrictions.RestrictionCheckMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncher
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ButtonPreferences
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.primary
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.isMultisig
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.isThreshold1
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
|
||||
class BuySellRestrictionCheckMixin(
|
||||
private val accountUseCase: SelectedAccountUseCase,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val actionLauncher: ActionBottomSheetLauncher,
|
||||
) : RestrictionCheckMixin {
|
||||
|
||||
override suspend fun isRestricted(): Boolean {
|
||||
val selectedAccount = accountUseCase.getSelectedMetaAccount()
|
||||
return selectedAccount.isMultisig() && !selectedAccount.isThreshold1()
|
||||
}
|
||||
|
||||
override suspend fun checkRestrictionAndDo(action: () -> Unit) {
|
||||
when {
|
||||
isRestricted() -> showMultisigWarning()
|
||||
else -> action()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMultisigWarning() {
|
||||
actionLauncher.launchBottomSheet(
|
||||
imageRes = R.drawable.ic_multisig,
|
||||
title = resourceManager.getString(R.string.multisig_sell_not_supported_title),
|
||||
subtitle = resourceManager.getString(R.string.multisig_sell_not_supported_message),
|
||||
actionButtonPreferences = ButtonPreferences.primary(resourceManager.getString(R.string.common_ok_back)),
|
||||
neutralButtonPreferences = null
|
||||
)
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.buySell
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.mixin.restrictions.isAllowed
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.view.input.selector.ListSelectorMixin
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixin.SelectorType
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface BuySellSelectorMixin {
|
||||
|
||||
sealed interface SelectorType {
|
||||
|
||||
object AllAssets : SelectorType
|
||||
|
||||
class Asset(val chaiId: String, val assetId: Int) : SelectorType
|
||||
}
|
||||
|
||||
class SelectorPayload(vararg val items: ListSelectorMixin.Item)
|
||||
|
||||
val tradingEnabledFlow: Flow<Boolean>
|
||||
val actionLiveData: LiveData<Event<SelectorPayload>>
|
||||
val errorLiveData: MutableLiveData<Event<Pair<String, String>>>
|
||||
|
||||
fun openSelector()
|
||||
}
|
||||
|
||||
class RealBuySellSelectorMixin(
|
||||
private val buySellRestrictionCheckMixin: BuySellRestrictionCheckMixin,
|
||||
private val router: AssetsRouter,
|
||||
private val tradeTokenRegistry: TradeTokenRegistry,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val selectorType: SelectorType,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) : BuySellSelectorMixin {
|
||||
|
||||
override val tradingEnabledFlow: Flow<Boolean> = flowOf {
|
||||
when (selectorType) {
|
||||
SelectorType.AllAssets -> true
|
||||
is SelectorType.Asset -> {
|
||||
val chainAsset = chainRegistry.asset(selectorType.chaiId, selectorType.assetId)
|
||||
tradeTokenRegistry.hasProvider(chainAsset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val actionLiveData: MutableLiveData<Event<BuySellSelectorMixin.SelectorPayload>> = MutableLiveData()
|
||||
|
||||
override val errorLiveData: MutableLiveData<Event<Pair<String, String>>> = MutableLiveData()
|
||||
|
||||
override fun openSelector() = coroutineScope.launchUnit {
|
||||
val payload = when (selectorType) {
|
||||
SelectorType.AllAssets -> openAllAssetsSelector()
|
||||
|
||||
is SelectorType.Asset -> openSpecifiedAssetSelector(selectorType)
|
||||
}
|
||||
|
||||
if (payload != null) {
|
||||
actionLiveData.value = Event(payload)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun openAllAssetsSelector() = BuySellSelectorMixin.SelectorPayload(
|
||||
buyItem(enabled = true) { router.openBuyFlow() },
|
||||
sellItem(enabled = buySellRestrictionCheckMixin.isAllowed()) { router.openSellFlow() },
|
||||
bridgeItem(enabled = true) { router.openBridgeFlow() }
|
||||
)
|
||||
|
||||
private suspend fun openSpecifiedAssetSelector(selectorType: SelectorType.Asset): BuySellSelectorMixin.SelectorPayload? {
|
||||
val chainAsset = chainRegistry.asset(selectorType.chaiId, selectorType.assetId)
|
||||
val buyAvailable = tradeTokenRegistry.hasProvider(chainAsset, TradeTokenRegistry.TradeType.BUY)
|
||||
val sellAvailable = tradeTokenRegistry.hasProvider(chainAsset, TradeTokenRegistry.TradeType.SELL) &&
|
||||
buySellRestrictionCheckMixin.isAllowed()
|
||||
|
||||
if (!buyAvailable && !sellAvailable) {
|
||||
showErrorMessage(R.string.trade_token_not_supported_title, R.string.trade_token_not_supported_message)
|
||||
return null
|
||||
}
|
||||
|
||||
return BuySellSelectorMixin.SelectorPayload(
|
||||
buyItem(enabled = buyAvailable) { router.openBuyProviders(selectorType.chaiId, selectorType.assetId) },
|
||||
sellItem(enabled = sellAvailable) { router.openSellProviders(selectorType.chaiId, selectorType.assetId) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun buyItem(enabled: Boolean, action: () -> Unit): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_add_circle_outline,
|
||||
if (enabled) R.color.icon_primary else R.color.icon_inactive,
|
||||
R.string.wallet_asset_buy_tokens,
|
||||
if (enabled) R.color.text_primary else R.color.button_text_inactive,
|
||||
if (enabled) action else errorAction(R.string.buy_token_not_supported_title, R.string.buy_token_not_supported_message)
|
||||
)
|
||||
}
|
||||
|
||||
private fun sellItem(enabled: Boolean, action: () -> Unit): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_sell_tokens,
|
||||
if (enabled) R.color.icon_primary else R.color.icon_inactive,
|
||||
R.string.wallet_asset_sell_tokens,
|
||||
if (enabled) R.color.text_primary else R.color.button_text_inactive,
|
||||
if (enabled) action else sellErrorAction()
|
||||
)
|
||||
}
|
||||
|
||||
private fun bridgeItem(enabled: Boolean, action: () -> Unit): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_bridge,
|
||||
if (enabled) R.color.icon_primary else R.color.icon_inactive,
|
||||
R.string.wallet_asset_bridge,
|
||||
if (enabled) R.color.text_primary else R.color.button_text_inactive,
|
||||
action
|
||||
)
|
||||
}
|
||||
|
||||
private fun sellErrorAction(): () -> Unit = {
|
||||
coroutineScope.launch {
|
||||
buySellRestrictionCheckMixin.checkRestrictionAndDo {
|
||||
showErrorMessage(R.string.sell_token_not_supported_title, R.string.sell_token_not_supported_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun errorAction(titleRes: Int, messageRes: Int): () -> Unit = { showErrorMessage(titleRes, messageRes) }
|
||||
|
||||
private fun showErrorMessage(titleRes: Int, messageRes: Int) {
|
||||
errorLiveData.value = Event(Pair(resourceManager.getString(titleRes), resourceManager.getString(messageRes)))
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.buySell
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_buy_api.presentation.trade.TradeTokenRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class BuySellSelectorMixinFactory(
|
||||
private val router: AssetsRouter,
|
||||
private val tradeTokenRegistry: TradeTokenRegistry,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val buySellRestrictionCheckMixin: BuySellRestrictionCheckMixin
|
||||
) {
|
||||
|
||||
fun create(selectorType: BuySellSelectorMixin.SelectorType, coroutineScope: CoroutineScope): BuySellSelectorMixin {
|
||||
return RealBuySellSelectorMixin(
|
||||
buySellRestrictionCheckMixin,
|
||||
router,
|
||||
tradeTokenRegistry,
|
||||
chainRegistry,
|
||||
resourceManager,
|
||||
selectorType,
|
||||
coroutineScope
|
||||
)
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.buySell
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.TextView
|
||||
import io.novafoundation.nova.common.R
|
||||
import io.novafoundation.nova.common.base.BaseFragmentMixin
|
||||
import io.novafoundation.nova.common.utils.ViewClickGestureDetector
|
||||
import io.novafoundation.nova.common.utils.setCompoundDrawableTint
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.view.dialog.dialog
|
||||
import io.novafoundation.nova.common.view.input.selector.DynamicSelectorBottomSheet
|
||||
|
||||
fun BaseFragmentMixin<*>.setupBuySellSelectorMixin(
|
||||
buySellSelectorMixin: BuySellSelectorMixin
|
||||
) {
|
||||
buySellSelectorMixin.actionLiveData.observeEvent { action ->
|
||||
DynamicSelectorBottomSheet(
|
||||
context = fragment.requireContext(),
|
||||
payload = DynamicSelectorBottomSheet.Payload(
|
||||
titleRes = null,
|
||||
subtitle = null,
|
||||
data = action.items.toList()
|
||||
),
|
||||
onClicked = { _, item -> item.onClick() },
|
||||
).show()
|
||||
}
|
||||
|
||||
buySellSelectorMixin.errorLiveData.observeEvent {
|
||||
dialog(providedContext) {
|
||||
setTitle(it.first)
|
||||
setMessage(it.second)
|
||||
setPositiveButton(R.string.common_got_it) { _, _ -> }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun BaseFragmentMixin<*>.setupButSellActionButton(
|
||||
buySellSelectorMixin: BuySellSelectorMixin,
|
||||
actionButton: TextView
|
||||
) {
|
||||
val clickDetector = ViewClickGestureDetector(actionButton)
|
||||
actionButton.setOnTouchListener { v, event ->
|
||||
clickDetector.onTouchEvent(event)
|
||||
}
|
||||
actionButton.setOnClickListener { buySellSelectorMixin.openSelector() }
|
||||
|
||||
buySellSelectorMixin.tradingEnabledFlow.observe {
|
||||
if (it) {
|
||||
actionButton.setTextColorRes(R.color.actions_color)
|
||||
actionButton.setCompoundDrawableTint(actionButton.context.getColor(R.color.actions_color))
|
||||
} else {
|
||||
actionButton.setTextColorRes(R.color.icon_inactive)
|
||||
actionButton.setCompoundDrawableTint(actionButton.context.getColor(R.color.icon_inactive))
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.gifts
|
||||
|
||||
import io.novafoundation.nova.common.mixin.restrictions.RestrictionCheckMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncher
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ButtonPreferences
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.primary
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsSupportedState
|
||||
|
||||
class GiftsRestrictionCheckMixin(
|
||||
private val accountSupportedUseCase: GiftsAccountSupportedUseCase,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val actionLauncher: ActionBottomSheetLauncher,
|
||||
) : RestrictionCheckMixin {
|
||||
|
||||
override suspend fun isRestricted(): Boolean {
|
||||
return accountSupportedUseCase.supportedState() != GiftsSupportedState.SUPPORTED
|
||||
}
|
||||
|
||||
override suspend fun checkRestrictionAndDo(action: () -> Unit) {
|
||||
when {
|
||||
isRestricted() -> showMultisigWarning()
|
||||
else -> action()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMultisigWarning() {
|
||||
actionLauncher.launchBottomSheet(
|
||||
imageRes = R.drawable.ic_multisig,
|
||||
title = resourceManager.getString(R.string.multisig_gifts_not_supported_title),
|
||||
subtitle = resourceManager.getString(R.string.multisig_gifts_not_supported_message),
|
||||
actionButtonPreferences = ButtonPreferences.primary(resourceManager.getString(R.string.common_ok_back)),
|
||||
neutralButtonPreferences = null
|
||||
)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.holders
|
||||
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.presentation.masking.setMaskableText
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemNetworkAssetGroupBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkGroupUi
|
||||
|
||||
class NetworkAssetGroupViewHolder(
|
||||
private val binder: ItemNetworkAssetGroupBinding,
|
||||
) : GroupedListHolder(binder.root) {
|
||||
|
||||
fun bind(assetGroup: NetworkGroupUi) = with(binder) {
|
||||
itemAssetGroupChain.setChain(assetGroup.chainUi)
|
||||
itemAssetGroupBalance.setMaskableText(assetGroup.groupBalanceFiat)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.holders
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.presentation.masking.setMaskableText
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemNetworkAssetBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkAssetUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.AssetModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.maskableFiat
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.maskableToken
|
||||
|
||||
class NetworkAssetViewHolder(
|
||||
private val binder: ItemNetworkAssetBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
) : GroupedListHolder(binder.root) {
|
||||
|
||||
fun bind(networkAsset: NetworkAssetUi, itemHandler: BalanceListAdapter.ItemAssetHandler) = with(containerView) {
|
||||
val asset = networkAsset.asset
|
||||
binder.itemAssetImage.setTokenIcon(networkAsset.icon, imageLoader)
|
||||
|
||||
bindPriceInfo(asset)
|
||||
|
||||
bindRecentChange(asset)
|
||||
|
||||
bindTotal(asset)
|
||||
|
||||
binder.itemAssetToken.text = asset.token.configuration.symbol.value
|
||||
|
||||
setOnClickListener { itemHandler.assetClicked(asset.token.configuration) }
|
||||
}
|
||||
|
||||
fun bindTotal(asset: AssetModel) {
|
||||
binder.itemAssetBalance.setMaskableText(asset.amount.maskableToken())
|
||||
binder.itemAssetPriceAmount.setMaskableText(asset.amount.maskableFiat())
|
||||
}
|
||||
|
||||
fun bindRecentChange(asset: AssetModel) = with(containerView) {
|
||||
binder.itemAssetRateChange.setTextColorRes(asset.token.rateChangeColorRes)
|
||||
binder.itemAssetRateChange.text = asset.token.recentRateChange
|
||||
}
|
||||
|
||||
fun bindPriceInfo(asset: AssetModel) = with(containerView) {
|
||||
binder.itemAssetRate.text = asset.token.rate
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.holders
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.presentation.masking.setMaskableText
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableParentViewHolder
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableParentItem
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemTokenAssetGroupBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.maskableFiat
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.maskableToken
|
||||
|
||||
class TokenAssetGroupViewHolder(
|
||||
private val binder: ItemTokenAssetGroupBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: BalanceListAdapter.ItemAssetHandler,
|
||||
) : GroupedListHolder(binder.root), ExpandableParentViewHolder {
|
||||
|
||||
override var expandableItem: ExpandableParentItem? = null
|
||||
|
||||
fun bind(tokenGroup: TokenGroupUi) = with(binder) {
|
||||
updateExpandableItem(tokenGroup)
|
||||
|
||||
itemTokenGroupAssetImage.setTokenIcon(tokenGroup.tokenIcon, imageLoader)
|
||||
|
||||
bindPriceRateInternal(tokenGroup)
|
||||
|
||||
bindRecentChangeInternal(tokenGroup)
|
||||
|
||||
bindTotalInternal(tokenGroup)
|
||||
|
||||
updateListener(tokenGroup)
|
||||
|
||||
itemAssetTokenGroupToken.text = tokenGroup.tokenSymbol
|
||||
}
|
||||
|
||||
fun bindTotal(networkAsset: TokenGroupUi) {
|
||||
updateListener(networkAsset)
|
||||
bindTotalInternal(networkAsset)
|
||||
}
|
||||
|
||||
fun bindRecentChange(networkAsset: TokenGroupUi) {
|
||||
updateListener(networkAsset)
|
||||
bindRecentChangeInternal(networkAsset)
|
||||
}
|
||||
|
||||
fun bindPriceRate(networkAsset: TokenGroupUi) {
|
||||
updateListener(networkAsset)
|
||||
bindPriceRateInternal(networkAsset)
|
||||
}
|
||||
|
||||
fun bindGroupType(networkAsset: TokenGroupUi) {
|
||||
updateListener(networkAsset)
|
||||
}
|
||||
|
||||
private fun bindTotalInternal(networkAsset: TokenGroupUi) {
|
||||
val balance = networkAsset.balance
|
||||
binder.itemAssetTokenGroupBalance.setMaskableText(balance.maskableToken())
|
||||
binder.itemAssetTokenGroupPriceAmount.setMaskableText(balance.maskableFiat())
|
||||
}
|
||||
|
||||
private fun bindRecentChangeInternal(networkAsset: TokenGroupUi) {
|
||||
with(binder) {
|
||||
itemAssetTokenGroupRateChange.setTextColorRes(networkAsset.rateChangeColorRes)
|
||||
itemAssetTokenGroupRateChange.text = networkAsset.recentRateChange
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPriceRateInternal(networkAsset: TokenGroupUi) {
|
||||
with(binder) {
|
||||
itemAssetTokenGroupRate.text = networkAsset.rate
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateListener(tokenGroupUi: TokenGroupUi) {
|
||||
containerView.setOnClickListener { itemHandler.tokenGroupClicked(tokenGroupUi) }
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.holders
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.presentation.masking.setMaskableText
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableChildViewHolder
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.items.ExpandableChildItem
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_assets.databinding.ItemTokenAssetBinding
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.AssetModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.maskableFiat
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.maskableToken
|
||||
|
||||
class TokenAssetViewHolder(
|
||||
private val binder: ItemTokenAssetBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
) : GroupedListHolder(binder.root), ExpandableChildViewHolder {
|
||||
|
||||
override var expandableItem: ExpandableChildItem? = null
|
||||
|
||||
fun bind(tokenAsset: TokenAssetUi, itemHandler: BalanceListAdapter.ItemAssetHandler) = with(containerView) {
|
||||
updateExpandableItem(tokenAsset)
|
||||
|
||||
val asset = tokenAsset.asset
|
||||
binder.itemTokenAssetImage.setTokenIcon(tokenAsset.assetIcon, imageLoader)
|
||||
binder.itemTokenAssetChainIcon.loadChainIcon(tokenAsset.chain.icon, imageLoader)
|
||||
binder.itemTokenAssetChainName.text = tokenAsset.chain.name
|
||||
|
||||
bindTotal(asset)
|
||||
|
||||
binder.itemTokenAssetToken.text = asset.token.configuration.symbol.value
|
||||
|
||||
setOnClickListener { itemHandler.assetClicked(asset.token.configuration) }
|
||||
}
|
||||
|
||||
fun bindTotal(asset: AssetModel) {
|
||||
binder.itemTokenAssetBalance.setMaskableText(asset.amount.maskableToken())
|
||||
binder.itemTokenAssetPriceAmount.setMaskableText(asset.amount.maskableFiat())
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.mappers
|
||||
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.AssetModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.AmountConfig
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatter
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.FractionPartStyling
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
|
||||
abstract class CommonAssetFormatter(
|
||||
private val maskableValueFormatter: MaskableValueFormatter,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) {
|
||||
|
||||
protected fun mapAssetToAssetModel(
|
||||
asset: Asset,
|
||||
balance: AssetBalance.Amount
|
||||
): AssetModel {
|
||||
return AssetModel(
|
||||
token = mapTokenToTokenModel(asset.token),
|
||||
amount = maskableValueFormatter.format {
|
||||
amountFormatter.formatAmountToAmountModel(
|
||||
amount = balance.amount,
|
||||
token = asset.token,
|
||||
config = AmountConfig(
|
||||
includeAssetTicker = false,
|
||||
tokenFractionPartStyling = FractionPartStyling.Styled(R.dimen.asset_balance_fraction_size)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.mappers
|
||||
|
||||
import io.novafoundation.nova.common.list.GroupedList
|
||||
import io.novafoundation.nova.common.list.toListWithHeaders
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_assets.domain.common.NetworkAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithOffChainBalance
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetBalance
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkAssetUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.NetworkGroupUi
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.FiatFormatter
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatter
|
||||
import java.math.BigDecimal
|
||||
|
||||
class NetworkAssetFormatterFactory(
|
||||
private val fiatFormatter: FiatFormatter,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) {
|
||||
fun create(maskableFormatter: MaskableValueFormatter): NetworkAssetFormatter {
|
||||
return NetworkAssetFormatter(maskableFormatter, fiatFormatter, amountFormatter)
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkAssetFormatter(
|
||||
private val maskableFormatter: MaskableValueFormatter,
|
||||
private val fiatFormatter: FiatFormatter,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) : CommonAssetFormatter(maskableFormatter, amountFormatter) {
|
||||
|
||||
fun mapGroupedAssetsToUi(
|
||||
groupedAssets: GroupedList<NetworkAssetGroup, AssetWithOffChainBalance>,
|
||||
assetIconProvider: AssetIconProvider,
|
||||
currency: Currency,
|
||||
groupBalance: (NetworkAssetGroup) -> BigDecimal = NetworkAssetGroup::groupTotalBalanceFiat,
|
||||
balance: (AssetBalance) -> AssetBalance.Amount = AssetBalance::total,
|
||||
): List<BalanceListRvItem> {
|
||||
return groupedAssets.mapKeys { (assetGroup, _) -> mapAssetGroupToUi(assetGroup, currency, groupBalance) }
|
||||
.mapValues { (_, assets) -> mapAssetsToAssetModels(assetIconProvider, assets, balance) }
|
||||
.toListWithHeaders()
|
||||
.filterIsInstance<BalanceListRvItem>()
|
||||
}
|
||||
|
||||
private fun mapAssetsToAssetModels(
|
||||
assetIconProvider: AssetIconProvider,
|
||||
assets: List<AssetWithOffChainBalance>,
|
||||
balance: (AssetBalance) -> AssetBalance.Amount
|
||||
): List<BalanceListRvItem> {
|
||||
return assets.map {
|
||||
NetworkAssetUi(
|
||||
mapAssetToAssetModel(it.asset, balance(it.balanceWithOffchain)),
|
||||
assetIconProvider.getAssetIconOrFallback(it.asset.token.configuration)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapAssetGroupToUi(
|
||||
assetGroup: NetworkAssetGroup,
|
||||
currency: Currency,
|
||||
groupBalance: (NetworkAssetGroup) -> BigDecimal
|
||||
): NetworkGroupUi {
|
||||
return NetworkGroupUi(
|
||||
chainUi = mapChainToUi(assetGroup.chain),
|
||||
groupBalanceFiat = maskableFormatter.format { fiatFormatter.formatFiat(groupBalance(assetGroup), currency) }
|
||||
)
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.mappers
|
||||
|
||||
import io.novafoundation.nova.common.list.GroupedList
|
||||
import io.novafoundation.nova.common.list.toListWithHeaders
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.presentation.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.common.utils.formatting.formatAsChange
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetWithNetwork
|
||||
import io.novafoundation.nova.feature_assets.domain.common.TokenAssetGroup
|
||||
import io.novafoundation.nova.feature_assets.domain.common.AssetBalance
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.BalanceListRvItem
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenAssetUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.AmountConfig
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.FractionPartStyling
|
||||
|
||||
class TokenAssetFormatterFactory(
|
||||
private val amountFormatter: AmountFormatter
|
||||
) {
|
||||
fun create(maskableFormatter: MaskableValueFormatter): TokenAssetFormatter {
|
||||
return TokenAssetFormatter(maskableFormatter, amountFormatter)
|
||||
}
|
||||
}
|
||||
|
||||
class TokenAssetFormatter(
|
||||
private val maskableFormatter: MaskableValueFormatter,
|
||||
private val amountFormatter: AmountFormatter
|
||||
) : CommonAssetFormatter(maskableFormatter, amountFormatter) {
|
||||
|
||||
fun mapGroupedAssetsToUi(
|
||||
groupedTokens: GroupedList<TokenAssetGroup, AssetWithNetwork>,
|
||||
assetIconProvider: AssetIconProvider,
|
||||
assetFilter: (groupId: String, List<TokenAssetUi>) -> List<TokenAssetUi> = { _, assets -> assets },
|
||||
groupBalance: (TokenAssetGroup) -> AssetBalance.Amount = { it.groupBalance.total },
|
||||
balance: (AssetBalance) -> AssetBalance.Amount = AssetBalance::total,
|
||||
): List<BalanceListRvItem> {
|
||||
return groupedTokens.mapKeys { (group, assets) -> mapTokenAssetGroupToUi(assetIconProvider, group, assets, groupBalance) }
|
||||
.mapValues { (group, assets) ->
|
||||
val assetModels = mapAssetsToAssetModels(assetIconProvider, group, assets, balance)
|
||||
assetFilter(group.itemId, assetModels)
|
||||
}
|
||||
.toListWithHeaders()
|
||||
.filterIsInstance<BalanceListRvItem>()
|
||||
}
|
||||
|
||||
fun mapTokenAssetGroupToUi(
|
||||
assetIconProvider: AssetIconProvider,
|
||||
assetGroup: TokenAssetGroup,
|
||||
assets: List<AssetWithNetwork>,
|
||||
groupBalance: (TokenAssetGroup) -> AssetBalance.Amount = { it.groupBalance.total }
|
||||
): TokenGroupUi {
|
||||
val balance = groupBalance(assetGroup)
|
||||
return TokenGroupUi(
|
||||
itemId = assetGroup.groupId,
|
||||
tokenIcon = assetIconProvider.getAssetIconOrFallback(assetGroup.tokenInfo.icon),
|
||||
rate = mapCoinRateChange(assetGroup.tokenInfo.coinRate, assetGroup.tokenInfo.currency),
|
||||
recentRateChange = assetGroup.tokenInfo.coinRate?.recentRateChange.orZero().formatAsChange(),
|
||||
rateChangeColorRes = mapCoinRateChangeColorRes(assetGroup.tokenInfo.coinRate),
|
||||
tokenSymbol = assetGroup.tokenInfo.symbol.value,
|
||||
singleItemGroup = assetGroup.itemsCount <= 1,
|
||||
balance = maskableFormatter.format {
|
||||
amountFormatter.formatAmountToAmountModel(
|
||||
amount = balance.amount,
|
||||
token = assetGroup.tokenInfo.token,
|
||||
config = AmountConfig(
|
||||
includeAssetTicker = false,
|
||||
tokenFractionPartStyling = FractionPartStyling.Styled(R.dimen.asset_balance_fraction_size)
|
||||
)
|
||||
)
|
||||
},
|
||||
groupType = mapType(assets)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapAssetsToAssetModels(
|
||||
assetIconProvider: AssetIconProvider,
|
||||
group: TokenGroupUi,
|
||||
assets: List<AssetWithNetwork>,
|
||||
balance: (AssetBalance) -> AssetBalance.Amount
|
||||
): List<TokenAssetUi> {
|
||||
return assets.map {
|
||||
TokenAssetUi(
|
||||
group.getId(),
|
||||
mapAssetToAssetModel(it.asset, balance(it.balanceWithOffChain)),
|
||||
assetIconProvider.getAssetIconOrFallback(it.asset.token.configuration),
|
||||
mapChainToUi(it.chain)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapType(
|
||||
assets: List<AssetWithNetwork>,
|
||||
): TokenGroupUi.GroupType {
|
||||
return if (assets.size == 1) {
|
||||
TokenGroupUi.GroupType.SingleItem(assets.first().asset.token.configuration)
|
||||
} else {
|
||||
TokenGroupUi.GroupType.Group
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.common.mappers
|
||||
|
||||
import androidx.annotation.ColorRes
|
||||
import io.novafoundation.nova.common.utils.formatting.formatAsChange
|
||||
import io.novafoundation.nova.common.utils.isNonNegative
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.TokenModel
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRateChange
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun mapCoinRateChange(coinRateChange: CoinRateChange?, currency: Currency): String {
|
||||
val rateChange = coinRateChange?.rate
|
||||
return mapCoinRateChange(rateChange.orZero(), currency)
|
||||
}
|
||||
|
||||
fun mapCoinRateChange(rate: BigDecimal, currency: Currency): String {
|
||||
return rate.formatAsCurrency(currency)
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
fun mapCoinRateChangeColorRes(coinRateChange: CoinRateChange?): Int {
|
||||
val rateChange = coinRateChange?.recentRateChange
|
||||
|
||||
return when {
|
||||
rateChange == null || rateChange.isZero -> R.color.text_secondary
|
||||
rateChange.isNonNegative -> R.color.text_positive
|
||||
else -> R.color.text_negative
|
||||
}
|
||||
}
|
||||
|
||||
fun mapTokenToTokenModel(token: Token): TokenModel {
|
||||
return with(token) {
|
||||
TokenModel(
|
||||
configuration = configuration,
|
||||
rate = mapCoinRateChange(token.coinRate, token.currency),
|
||||
recentRateChange = (coinRate?.recentRateChange ?: BigDecimal.ZERO).formatAsChange(),
|
||||
rateChangeColorRes = mapCoinRateChangeColorRes(coinRate)
|
||||
)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import io.novafoundation.nova.common.utils.setDrawableEnd
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.BalancesView
|
||||
|
||||
class AssetDetailBalancesView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0,
|
||||
) : BalancesView(context, attrs, defStyle) {
|
||||
|
||||
val transferable = item(R.string.wallet_balance_transferable)
|
||||
|
||||
val locked = item(R.string.wallet_balance_locked).apply {
|
||||
setOwnDividerVisible(false)
|
||||
title.setDrawableEnd(R.drawable.ic_info, paddingInDp = 4)
|
||||
}
|
||||
|
||||
fun showBalanceDetails(show: Boolean) {
|
||||
expandableView.setExpandable(show)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail
|
||||
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.TokenModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel
|
||||
|
||||
class AssetDetailsModel(
|
||||
val token: TokenModel,
|
||||
val assetIcon: Icon,
|
||||
val total: AmountModel,
|
||||
val transferable: AmountModel,
|
||||
val locked: AmountModel
|
||||
)
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
|
||||
import io.novafoundation.nova.common.utils.hideKeyboard
|
||||
import io.novafoundation.nova.common.utils.insets.applyBarMargin
|
||||
import io.novafoundation.nova.common.utils.insets.applyNavigationBarInsets
|
||||
import io.novafoundation.nova.common.view.setModelOrHide
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_assets.databinding.FragmentBalanceDetailBinding
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.setupButSellActionButton
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.setupBuySellSelectorMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.BalanceLocksModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.receive.view.LedgerNotSupportedWarningBottomSheet
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.history.setBannerModelOrHide
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.history.showState
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.setTotalAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.showAmount
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_TOKEN = "KEY_TOKEN"
|
||||
|
||||
class BalanceDetailFragment : BaseFragment<BalanceDetailViewModel, FragmentBalanceDetailBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getBundle(assetPayload: AssetPayload): Bundle {
|
||||
return Bundle().apply {
|
||||
putParcelable(KEY_TOKEN, assetPayload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentBalanceDetailBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.root.applyNavigationBarInsets(consume = false)
|
||||
binder.balanceDetailBack.applyBarMargin()
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
hideKeyboard()
|
||||
|
||||
binder.transfersContainer.initializeBehavior(anchorView = binder.balanceDetailContent)
|
||||
|
||||
binder.transfersContainer.setScrollingListener(viewModel::transactionsScrolled)
|
||||
|
||||
binder.transfersContainer.setSlidingStateListener(::setRefreshEnabled)
|
||||
|
||||
binder.transfersContainer.setTransactionClickListener(viewModel::transactionClicked)
|
||||
|
||||
binder.transfersContainer.setFilterClickListener { viewModel.filterClicked() }
|
||||
|
||||
binder.transfersContainer.setBannerClickListener() { viewModel.filterClicked() }
|
||||
|
||||
binder.balanceDetailContainer.setOnRefreshListener {
|
||||
viewModel.sync()
|
||||
}
|
||||
|
||||
binder.balanceDetailBack.setOnClickListener { viewModel.backClicked() }
|
||||
|
||||
binder.balanceDetailActions.send.setOnClickListener {
|
||||
viewModel.sendClicked()
|
||||
}
|
||||
|
||||
binder.balanceDetailActions.swap.setOnClickListener {
|
||||
viewModel.swapClicked()
|
||||
}
|
||||
|
||||
binder.balanceDetailActions.receive.setOnClickListener {
|
||||
viewModel.receiveClicked()
|
||||
}
|
||||
|
||||
binder.balanceDetailActions.gift.setOnClickListener {
|
||||
viewModel.giftClicked()
|
||||
}
|
||||
|
||||
binder.balanceDetailsBalances.locked.setOnClickListener {
|
||||
viewModel.lockedInfoClicked()
|
||||
}
|
||||
|
||||
binder.balanceDetailsMigrationAlert.setOnCloseClickListener { viewModel.closeMigrationAlert() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
val token = argument<AssetPayload>(KEY_TOKEN)
|
||||
|
||||
FeatureUtils.getFeature<AssetsFeatureComponent>(
|
||||
requireContext(),
|
||||
AssetsFeatureApi::class.java
|
||||
)
|
||||
.balanceDetailComponentFactory()
|
||||
.create(this, token)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: BalanceDetailViewModel) {
|
||||
observeBrowserEvents(viewModel)
|
||||
|
||||
setupBuySellSelectorMixin(viewModel.buySellSelectorMixin)
|
||||
setupButSellActionButton(viewModel.buySellSelectorMixin, binder.balanceDetailActions.buySell)
|
||||
|
||||
viewModel.state.observe(binder.transfersContainer::showState)
|
||||
viewModel.destinationMigrationBannerFlow.observe {
|
||||
binder.transfersContainer.setBannerModelOrHide(it)
|
||||
}
|
||||
|
||||
viewModel.assetDetailsModel.observe { asset ->
|
||||
binder.balanceDetailTokenIcon.setTokenIcon(asset.assetIcon, imageLoader)
|
||||
binder.balanceDetailTokenName.text = asset.token.configuration.symbol.value
|
||||
|
||||
binder.balanceDetailsBalances.setTotalAmount(asset.total)
|
||||
binder.balanceDetailsBalances.transferable.showAmount(asset.transferable)
|
||||
binder.balanceDetailsBalances.locked.showAmount(asset.locked)
|
||||
}
|
||||
|
||||
viewModel.supportExpandableBalanceDetails.observe {
|
||||
binder.balanceDetailsBalances.showBalanceDetails(it)
|
||||
}
|
||||
|
||||
viewModel.priceChartFormatters.observe {
|
||||
binder.priceChartView.setTextInjectors(it.price, it.priceChange, it.date)
|
||||
}
|
||||
|
||||
viewModel.priceChartTitle.observe {
|
||||
binder.priceChartView.setTitle(it)
|
||||
}
|
||||
|
||||
viewModel.priceChartModels.observe {
|
||||
if (it == null) {
|
||||
binder.priceChartView.isGone = true
|
||||
return@observe
|
||||
}
|
||||
|
||||
binder.priceChartView.setCharts(it)
|
||||
}
|
||||
|
||||
viewModel.hideRefreshEvent.observeEvent {
|
||||
binder.balanceDetailContainer.isRefreshing = false
|
||||
}
|
||||
|
||||
viewModel.showLockedDetailsEvent.observeEvent(::showLockedDetails)
|
||||
|
||||
viewModel.sendEnabled.observe(binder.balanceDetailActions.send::setEnabled)
|
||||
|
||||
viewModel.swapButtonEnabled.observe(binder.balanceDetailActions.swap::setEnabled)
|
||||
|
||||
viewModel.giftsButtonEnabled.observe(binder.balanceDetailActions.gift::setEnabled)
|
||||
|
||||
viewModel.acknowledgeLedgerWarning.awaitableActionLiveData.observeEvent {
|
||||
LedgerNotSupportedWarningBottomSheet(
|
||||
context = requireContext(),
|
||||
onSuccess = { it.onSuccess(Unit) },
|
||||
message = it.payload
|
||||
).show()
|
||||
}
|
||||
|
||||
viewModel.chainUI.observe {
|
||||
binder.balanceDetailsBalances.setChain(it)
|
||||
}
|
||||
|
||||
viewModel.originMigrationAlertFlow.observe {
|
||||
binder.balanceDetailsMigrationAlert.setModelOrHide(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRefreshEnabled(bottomSheetState: Int) {
|
||||
val bottomSheetCollapsed = BottomSheetBehavior.STATE_COLLAPSED == bottomSheetState
|
||||
binder.balanceDetailContainer.isEnabled = bottomSheetCollapsed
|
||||
}
|
||||
|
||||
private fun showLockedDetails(model: BalanceLocksModel) {
|
||||
LockedTokensBottomSheet(requireContext(), model).show()
|
||||
}
|
||||
}
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.common.mixin.api.Browserable
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.utils.sumByBigInteger
|
||||
import io.novafoundation.nova.common.view.AlertModel
|
||||
import io.novafoundation.nova.common.view.AlertView
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationInfoUseCase
|
||||
import io.novafoundation.nova.feature_ahm_api.domain.model.ChainMigrationConfig
|
||||
import io.novafoundation.nova.feature_ahm_api.presentation.getChainMigrationDateFormat
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.locks.BalanceLocksInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.price.AssetPriceChart
|
||||
import io.novafoundation.nova.feature_assets.domain.price.ChartsInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.send.SendInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.gifts.GiftsRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.mappers.mapTokenToTokenModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.BalanceLocksModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.send.amount.SendPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.filter.TransactionHistoryFilterPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.history.TransactionHistoryBannerModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.history.mixin.TransactionHistoryMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.history.mixin.TransactionHistoryUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.views.priceCharts.PriceChartModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.views.priceCharts.formatters.RealDateChartTextInjector
|
||||
import io.novafoundation.nova.feature_assets.presentation.views.priceCharts.formatters.RealPriceChangeTextInjector
|
||||
import io.novafoundation.nova.feature_assets.presentation.views.priceCharts.formatters.RealPricePriceTextInjector
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
|
||||
import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.PricePeriod
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.unlabeledReserves
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.AmountConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.FiatConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.balanceId
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.mapBalanceIdToUi
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.fullChainAssetId
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.toAssetPayload
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
|
||||
private const val ORIGIN_MIGRATION_ALERT = "ORIGIN_MIGRATION_ALERT"
|
||||
|
||||
class BalanceDetailViewModel(
|
||||
private val walletInteractor: WalletInteractor,
|
||||
private val balanceLocksInteractor: BalanceLocksInteractor,
|
||||
private val sendInteractor: SendInteractor,
|
||||
private val router: AssetsRouter,
|
||||
private val assetPayload: AssetPayload,
|
||||
private val transactionHistoryMixin: TransactionHistoryMixin,
|
||||
private val accountUseCase: SelectedAccountUseCase,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val currencyInteractor: CurrencyInteractor,
|
||||
private val controllableAssetCheck: ControllableAssetCheckMixin,
|
||||
private val externalBalancesInteractor: ExternalBalancesInteractor,
|
||||
private val swapAvailabilityInteractor: SwapAvailabilityInteractor,
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val chartsInteractor: ChartsInteractor,
|
||||
private val buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
|
||||
private val amountFormatter: AmountFormatter,
|
||||
private val chainMigrationInfoUseCase: ChainMigrationInfoUseCase,
|
||||
private val giftsAvailableGiftAssetsUseCase: AvailableGiftAssetsUseCase,
|
||||
private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin
|
||||
) : BaseViewModel(), TransactionHistoryUi by transactionHistoryMixin, Browserable {
|
||||
|
||||
override val openBrowserEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
val acknowledgeLedgerWarning = controllableAssetCheck.acknowledgeLedgerWarning
|
||||
|
||||
private val _hideRefreshEvent = MutableLiveData<Event<Unit>>()
|
||||
val hideRefreshEvent: LiveData<Event<Unit>> = _hideRefreshEvent
|
||||
|
||||
private val _showLockedDetailsEvent = MutableLiveData<Event<BalanceLocksModel>>()
|
||||
val showLockedDetailsEvent: LiveData<Event<BalanceLocksModel>> = _showLockedDetailsEvent
|
||||
|
||||
private val chainFlow = walletInteractor.chainFlow(assetPayload.chainId)
|
||||
.shareInBackground()
|
||||
|
||||
private val assetFlow = walletInteractor.assetFlow(assetPayload.chainId, assetPayload.chainAssetId)
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
private val chainAssetFlow = assetFlow.map { it.token.configuration }
|
||||
.distinctUntilChangedBy { it.fullId }
|
||||
|
||||
private val balanceLocksFlow = balanceLocksInteractor.balanceLocksFlow(assetPayload.chainId, assetPayload.chainAssetId)
|
||||
.catch { error ->
|
||||
Log.e(LOG_TAG, "Failed to load balance locks: ${error.message}")
|
||||
emit(emptyList())
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
private val balanceHoldsFlow = balanceLocksInteractor.balanceHoldsFlow(assetPayload.chainId, assetPayload.chainAssetId)
|
||||
.catch { error ->
|
||||
Log.e(LOG_TAG, "Failed to load balance holds: ${error.message}")
|
||||
emit(emptyList())
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
private val selectedAccountFlow = accountUseCase.selectedMetaAccountFlow()
|
||||
.share()
|
||||
|
||||
private val externalBalancesFlow = externalBalancesInteractor.observeExternalBalances(assetPayload.fullChainAssetId)
|
||||
.onStart { emit(emptyList()) }
|
||||
.shareInBackground()
|
||||
|
||||
private val migrationConfigFlow = chainMigrationInfoUseCase.observeMigrationConfigOrNull(assetPayload.chainId, assetPayload.chainAssetId)
|
||||
.shareInBackground()
|
||||
|
||||
val assetDetailsModel = combine(assetFlow, externalBalancesFlow) { asset, externalBalances ->
|
||||
mapAssetToUi(asset, externalBalances)
|
||||
}
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val supportExpandableBalanceDetails = assetFlow.map { it.totalInPlanks.isPositive() }
|
||||
.shareInBackground()
|
||||
|
||||
private val lockedBalanceModel = combine(balanceLocksFlow, balanceHoldsFlow, externalBalancesFlow, assetFlow) { locks, holds, externalBalances, asset ->
|
||||
mapBalanceLocksToUi(locks, holds, externalBalances, asset)
|
||||
}
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val buySellSelectorMixin = buySellSelectorMixinFactory.create(
|
||||
BuySellSelectorMixin.SelectorType.Asset(assetPayload.chainId, assetPayload.chainAssetId),
|
||||
viewModelScope
|
||||
)
|
||||
|
||||
val chainUI = chainFlow.map { mapChainToUi(it) }
|
||||
|
||||
val swapButtonEnabled = chainAssetFlow.flatMapLatest {
|
||||
swapAvailabilityInteractor.swapAvailableFlow(it, viewModelScope)
|
||||
}
|
||||
.onStart { emit(false) }
|
||||
.catch { error ->
|
||||
Log.e(LOG_TAG, "Failed to check swap availability: ${error.message}")
|
||||
emit(false)
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
val giftsButtonEnabled = chainAssetFlow.map {
|
||||
giftsAvailableGiftAssetsUseCase.isGiftsAvailable(it)
|
||||
}
|
||||
.onStart { emit(false) }
|
||||
.catch { error ->
|
||||
Log.e(LOG_TAG, "Failed to check gifts availability: ${error.message}")
|
||||
emit(false)
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
val sendEnabled = assetFlow.map {
|
||||
sendInteractor.areTransfersEnabled(it.token.configuration)
|
||||
}
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val priceChartTitle = assetFlow.map {
|
||||
val tokenName = it.token.configuration.symbol.value
|
||||
resourceManager.getString(R.string.price_chart_title, tokenName)
|
||||
}.shareInBackground()
|
||||
|
||||
val priceChartFormatters: Flow<PriceChartTextInjectors> = assetFlow.map { asset ->
|
||||
val lastCoinRate = asset.token.coinRate?.rate
|
||||
val currency = currencyInteractor.getSelectedCurrency()
|
||||
|
||||
PriceChartTextInjectors(
|
||||
RealPricePriceTextInjector(currency, lastCoinRate),
|
||||
RealPriceChangeTextInjector(resourceManager, currency),
|
||||
RealDateChartTextInjector(resourceManager)
|
||||
)
|
||||
}.shareInBackground()
|
||||
|
||||
private val dateFormatter = getChainMigrationDateFormat()
|
||||
|
||||
val originMigrationAlertFlow = combine(
|
||||
migrationConfigFlow,
|
||||
chainFlow,
|
||||
selectedAccountFlow,
|
||||
chainMigrationInfoUseCase.observeInfoShouldBeHidden(ORIGIN_MIGRATION_ALERT, assetPayload.chainId, assetPayload.chainAssetId)
|
||||
) { configWithChains, chain, metaAccount, shouldBeHidden ->
|
||||
if (shouldBeHidden) return@combine null
|
||||
if (configWithChains == null) return@combine null
|
||||
if (configWithChains.originAsset.notMatchWithBalanceAsset()) return@combine null
|
||||
if (!metaAccount.hasAccountIn(configWithChains.destinationChain)) return@combine null
|
||||
|
||||
val config = configWithChains.config
|
||||
val sourceAsset = configWithChains.originAsset
|
||||
val destinationChain = configWithChains.destinationChain
|
||||
val formattedDate = dateFormatter.format(config.timeStartAt)
|
||||
AlertModel(
|
||||
style = AlertView.Style.fromPreset(AlertView.StylePreset.INFO),
|
||||
message = resourceManager.getString(R.string.asset_details_source_asset_alert_title, sourceAsset.symbol.value, destinationChain.name),
|
||||
subMessage = resourceManager.getString(
|
||||
R.string.asset_details_source_asset_alert_message,
|
||||
formattedDate,
|
||||
sourceAsset.symbol.value,
|
||||
destinationChain.name
|
||||
),
|
||||
linkAction = AlertModel.ActionModel(resourceManager.getString(R.string.common_learn_more)) { learnMoreMigrationClicked(config) },
|
||||
buttonAction = AlertModel.ActionModel(
|
||||
resourceManager.getString(R.string.asset_details_source_asset_alert_button, destinationChain.name),
|
||||
{ openAssetDetails(chainData = configWithChains.config.destinationData) }
|
||||
)
|
||||
)
|
||||
}.shareInBackground()
|
||||
|
||||
val destinationMigrationBannerFlow = combine(
|
||||
migrationConfigFlow,
|
||||
chainFlow,
|
||||
selectedAccountFlow,
|
||||
) { configWithChains, chain, metaAccount ->
|
||||
if (configWithChains == null) return@combine null
|
||||
if (configWithChains.destinationAsset.notMatchWithBalanceAsset()) return@combine null
|
||||
if (!metaAccount.hasAccountIn(configWithChains.originChain)) return@combine null
|
||||
|
||||
val sourceAsset = configWithChains.originAsset
|
||||
val sourceChain = configWithChains.originChain
|
||||
TransactionHistoryBannerModel(
|
||||
resourceManager.getString(R.string.transaction_history_migration_source_message, sourceAsset.symbol.value, sourceChain.name),
|
||||
{ openAssetDetails(chainData = configWithChains.config.originData) }
|
||||
)
|
||||
}.shareInBackground()
|
||||
|
||||
private val priceCharts: Flow<List<AssetPriceChart>?> = assetFlow.map { it.token.configuration.priceId }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
val priceId = it ?: return@flatMapLatest flowOf { null }
|
||||
chartsInteractor.chartsFlow(priceId)
|
||||
}.shareInBackground()
|
||||
|
||||
val priceChartModels = priceCharts.map { charts ->
|
||||
charts?.map { mapChartsToUi(it) }
|
||||
}.shareInBackground()
|
||||
|
||||
init {
|
||||
sync()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
transactionHistoryMixin.cancel()
|
||||
}
|
||||
|
||||
fun transactionsScrolled(index: Int) {
|
||||
transactionHistoryMixin.scrolled(index)
|
||||
}
|
||||
|
||||
fun filterClicked() {
|
||||
val payload = TransactionHistoryFilterPayload(assetPayload)
|
||||
router.openFilter(payload)
|
||||
}
|
||||
|
||||
fun sync() {
|
||||
launch {
|
||||
runCatching {
|
||||
swapAvailabilityInteractor.sync(viewModelScope)
|
||||
|
||||
val currency = currencyInteractor.getSelectedCurrency()
|
||||
val deferredAssetSync = async { walletInteractor.syncAssetsRates(currency) }
|
||||
val deferredTransactionsSync = async { transactionHistoryMixin.syncFirstOperationsPage() }
|
||||
|
||||
awaitAll(deferredAssetSync, deferredTransactionsSync)
|
||||
}.onFailure { error ->
|
||||
Log.e(LOG_TAG, "Sync failed: ${error.message}")
|
||||
}
|
||||
|
||||
_hideRefreshEvent.value = Event(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun sendClicked() {
|
||||
router.openSend(SendPayload.SpecifiedOrigin(assetPayload))
|
||||
}
|
||||
|
||||
fun receiveClicked() = checkControllableAsset {
|
||||
router.openReceive(assetPayload)
|
||||
}
|
||||
|
||||
fun swapClicked() {
|
||||
launch {
|
||||
val chainAsset = assetFlow.first().token.configuration
|
||||
val payload = SwapSettingsPayload.DefaultFlow(chainAsset.fullId.toAssetPayload())
|
||||
router.openSwapSetupAmount(payload)
|
||||
}
|
||||
}
|
||||
|
||||
fun giftClicked() = launchUnit {
|
||||
giftsRestrictionCheckMixin.checkRestrictionAndDo {
|
||||
router.openGiftsByAsset(assetPayload)
|
||||
}
|
||||
}
|
||||
|
||||
fun lockedInfoClicked() = launch {
|
||||
val balanceLocks = lockedBalanceModel.first()
|
||||
_showLockedDetailsEvent.value = Event(balanceLocks)
|
||||
}
|
||||
|
||||
fun closeMigrationAlert() {
|
||||
chainMigrationInfoUseCase.markMigrationInfoAsHidden(ORIGIN_MIGRATION_ALERT, assetPayload.chainId, assetPayload.chainAssetId)
|
||||
}
|
||||
|
||||
private fun checkControllableAsset(action: () -> Unit) {
|
||||
launch {
|
||||
val metaAccount = selectedAccountFlow.first()
|
||||
val chainAsset = assetFlow.first().token.configuration
|
||||
controllableAssetCheck.check(metaAccount, chainAsset) { action() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapAssetToUi(asset: Asset, externalBalances: List<ExternalBalance>): AssetDetailsModel {
|
||||
val totalContributedPlanks = externalBalances.sumByBigInteger { it.amount }
|
||||
val totalContributed = asset.token.amountFromPlanks(totalContributedPlanks)
|
||||
|
||||
return AssetDetailsModel(
|
||||
token = mapTokenToTokenModel(asset.token),
|
||||
total = amountFormatter.formatAmountToAmountModel(
|
||||
asset.total + totalContributed,
|
||||
asset,
|
||||
AmountConfig(useTokenAbbreviation = false, fiatAbbreviation = FiatConfig.AbbreviationStyle.NO_ABBREVIATION)
|
||||
),
|
||||
transferable = amountFormatter.formatAmountToAmountModel(asset.transferable, asset),
|
||||
locked = amountFormatter.formatAmountToAmountModel(asset.locked + totalContributed, asset),
|
||||
assetIcon = assetIconProvider.getAssetIconOrFallback(asset.token.configuration)
|
||||
)
|
||||
}
|
||||
|
||||
private fun openAssetDetails(chainData: ChainMigrationConfig.ChainData) {
|
||||
router.openAssetDetails(
|
||||
AssetPayload(
|
||||
chainId = chainData.chainId,
|
||||
chainAssetId = chainData.assetId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapBalanceLocksToUi(
|
||||
balanceLocks: List<BalanceLock>,
|
||||
holds: List<BalanceHold>,
|
||||
externalBalances: List<ExternalBalance>,
|
||||
asset: Asset
|
||||
): BalanceLocksModel {
|
||||
val mappedLocks = balanceLocks.map {
|
||||
BalanceLocksModel.Lock(
|
||||
mapBalanceIdToUi(resourceManager, it.id.value),
|
||||
amountFormatter.formatAmountToAmountModel(it.amountInPlanks, asset)
|
||||
)
|
||||
}
|
||||
|
||||
val mappedHolds = holds.map {
|
||||
BalanceLocksModel.Lock(
|
||||
mapBalanceIdToUi(resourceManager, it.identifier),
|
||||
amountFormatter.formatAmountToAmountModel(it.amountInPlanks, asset)
|
||||
)
|
||||
}
|
||||
|
||||
val unlabeledReserves = asset.unlabeledReserves(holds)
|
||||
|
||||
val reservedBalance = BalanceLocksModel.Lock(
|
||||
resourceManager.getString(R.string.wallet_balance_reserved),
|
||||
amountFormatter.formatAmountToAmountModel(unlabeledReserves, asset)
|
||||
)
|
||||
|
||||
val external = externalBalances.map { externalBalance ->
|
||||
BalanceLocksModel.Lock(
|
||||
name = mapBalanceIdToUi(resourceManager, externalBalance.type.balanceId),
|
||||
amount = amountFormatter.formatAmountToAmountModel(externalBalance.amount, asset)
|
||||
)
|
||||
}
|
||||
|
||||
val locks = buildList {
|
||||
addAll(mappedLocks)
|
||||
addAll(mappedHolds)
|
||||
add(reservedBalance)
|
||||
addAll(external)
|
||||
}
|
||||
|
||||
return BalanceLocksModel(locks)
|
||||
}
|
||||
|
||||
private fun mapChartsToUi(assetPriceChart: AssetPriceChart): PriceChartModel {
|
||||
val buttonText = mapButtonText(assetPriceChart.range)
|
||||
|
||||
return if (assetPriceChart.chart is ExtendedLoadingState.Loaded) {
|
||||
val periodName = mapPeriodName(assetPriceChart.range)
|
||||
val supportTimeShowing = supportTimeShowing(assetPriceChart.range)
|
||||
val mappedChart = assetPriceChart.chart.data.map { PriceChartModel.Chart.Price(it.timestamp, it.rate) }
|
||||
PriceChartModel.Chart(buttonText, periodName, supportTimeShowing, mappedChart)
|
||||
} else {
|
||||
PriceChartModel.Loading(buttonText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapButtonText(pricePeriod: PricePeriod): String {
|
||||
val buttonTextRes = when (pricePeriod) {
|
||||
PricePeriod.DAY -> R.string.price_chart_day
|
||||
PricePeriod.WEEK -> R.string.price_chart_week
|
||||
PricePeriod.MONTH -> R.string.price_chart_month
|
||||
PricePeriod.YEAR -> R.string.price_chart_year
|
||||
PricePeriod.MAX -> R.string.price_chart_max
|
||||
}
|
||||
|
||||
return resourceManager.getString(buttonTextRes)
|
||||
}
|
||||
|
||||
private fun mapPeriodName(pricePeriod: PricePeriod): String {
|
||||
val periodNameRes = when (pricePeriod) {
|
||||
PricePeriod.DAY -> R.string.price_charts_period_today
|
||||
PricePeriod.WEEK -> R.string.price_charts_period_week
|
||||
PricePeriod.MONTH -> R.string.price_charts_period_month
|
||||
PricePeriod.YEAR -> R.string.price_charts_period_year
|
||||
PricePeriod.MAX -> R.string.price_charts_period_all
|
||||
}
|
||||
|
||||
return resourceManager.getString(periodNameRes)
|
||||
}
|
||||
|
||||
private fun supportTimeShowing(pricePeriod: PricePeriod): Boolean {
|
||||
return when (pricePeriod) {
|
||||
PricePeriod.DAY, PricePeriod.WEEK, PricePeriod.MONTH -> true
|
||||
PricePeriod.YEAR, PricePeriod.MAX -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun learnMoreMigrationClicked(config: ChainMigrationConfig) {
|
||||
launch {
|
||||
openBrowserEvent.value = Event(config.wikiURL)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Chain.Asset.notMatchWithBalanceAsset(): Boolean {
|
||||
return assetPayload.chainId != chainId || assetPayload.chainAssetId != id
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMargins
|
||||
import io.novafoundation.nova.common.databinding.BottomSheeetFixedListBinding
|
||||
import io.novafoundation.nova.common.view.TableCellView
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.common.view.bottomSheet.list.fixed.FixedListBottomSheet
|
||||
import io.novafoundation.nova.feature_assets.presentation.model.BalanceLocksModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.showAmount
|
||||
|
||||
class LockedTokensBottomSheet(
|
||||
context: Context,
|
||||
private val balanceLocks: BalanceLocksModel
|
||||
) : FixedListBottomSheet<BottomSheeetFixedListBinding>(context, viewConfiguration = ViewConfiguration.default(context)) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.wallet_balance_locked)
|
||||
val viewItems = createViewItems(balanceLocks.locks)
|
||||
viewItems.forEach { addItem(it) }
|
||||
}
|
||||
|
||||
private fun createViewItems(locks: List<BalanceLocksModel.Lock>): List<TableCellView> {
|
||||
return locks.map(::createViewItem)
|
||||
}
|
||||
|
||||
private fun createViewItem(lock: BalanceLocksModel.Lock): TableCellView {
|
||||
return TableCellView.createTableCellView(context).apply {
|
||||
setOwnDividerVisible(false)
|
||||
setTitle(lock.name)
|
||||
showAmount(lock.amount)
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
updateMargins(
|
||||
left = getCommonPadding(),
|
||||
right = getCommonPadding()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail
|
||||
|
||||
import io.novafoundation.nova.feature_assets.presentation.views.priceCharts.formatters.DateChartTextInjector
|
||||
import io.novafoundation.nova.feature_assets.presentation.views.priceCharts.formatters.PriceChangeTextInjector
|
||||
import io.novafoundation.nova.feature_assets.presentation.views.priceCharts.formatters.PriceTextInjector
|
||||
|
||||
class PriceChartTextInjectors(
|
||||
val price: PriceTextInjector,
|
||||
val priceChange: PriceChangeTextInjector,
|
||||
val date: DateChartTextInjector
|
||||
)
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.DeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.addParamIfNotNull
|
||||
|
||||
class AssetDetailsDeepLinkData(
|
||||
val accountAddress: String?,
|
||||
val chainId: String,
|
||||
val assetId: Int
|
||||
)
|
||||
|
||||
class AssetDetailsDeepLinkConfigurator(
|
||||
private val linkBuilderFactory: LinkBuilderFactory
|
||||
) : DeepLinkConfigurator<AssetDetailsDeepLinkData> {
|
||||
|
||||
val action = "open"
|
||||
val screen = "asset"
|
||||
val deepLinkPrefix = "/$action/$screen"
|
||||
val addressParam = "address"
|
||||
val chainIdParam = "chainId"
|
||||
val assetIdParam = "assetId"
|
||||
|
||||
override fun configure(payload: AssetDetailsDeepLinkData, type: DeepLinkConfigurator.Type): Uri {
|
||||
return linkBuilderFactory.newLink(type)
|
||||
.setAction(action)
|
||||
.setScreen(screen)
|
||||
.addParamIfNotNull(addressParam, payload.accountAddress)
|
||||
.addParam(chainIdParam, payload.chainId)
|
||||
.addParam(assetIdParam, payload.assetId.toString())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.findMetaAccountOrThrow
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.CallbackEvent
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.ext.isEnabled
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AssetDetailsDeepLinkHandler(
|
||||
private val router: AssetsRouter,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val automaticInteractionGate: AutomaticInteractionGate,
|
||||
private val assetDetailsDeepLinkConfigurator: AssetDetailsDeepLinkConfigurator
|
||||
) : DeepLinkHandler {
|
||||
|
||||
override val callbackFlow = MutableSharedFlow<CallbackEvent>()
|
||||
|
||||
override suspend fun matches(data: Uri): Boolean {
|
||||
val path = data.path ?: return false
|
||||
|
||||
return path.startsWith(assetDetailsDeepLinkConfigurator.deepLinkPrefix)
|
||||
}
|
||||
|
||||
override suspend fun handleDeepLink(data: Uri): Result<Unit> = runCatching {
|
||||
automaticInteractionGate.awaitInteractionAllowed()
|
||||
|
||||
val address = data.getAddress()
|
||||
val chainId = data.getChainIdOrPolkadot()
|
||||
val assetId = data.getAssetId() ?: throw IllegalStateException()
|
||||
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
require(chain.isEnabled)
|
||||
|
||||
address?.let { selectMetaAccount(chain, address) }
|
||||
|
||||
val payload = AssetPayload(chainId, assetId)
|
||||
router.openAssetDetailsFromDeepLink(payload)
|
||||
}
|
||||
|
||||
private suspend fun selectMetaAccount(chain: Chain, address: String) = withContext(Dispatchers.Default) {
|
||||
val metaAccount = accountRepository.findMetaAccountOrThrow(chain.accountIdOf(address), chain.id)
|
||||
accountRepository.selectMetaAccount(metaAccount.id)
|
||||
}
|
||||
|
||||
private fun Uri.getAddress(): String? {
|
||||
return getQueryParameter(assetDetailsDeepLinkConfigurator.addressParam)
|
||||
}
|
||||
|
||||
private fun Uri.getChainIdOrPolkadot(): String {
|
||||
return getQueryParameter(assetDetailsDeepLinkConfigurator.chainIdParam) ?: ChainGeneses.POLKADOT
|
||||
}
|
||||
|
||||
private fun Uri.getAssetId(): Int? {
|
||||
return getQueryParameter(assetDetailsDeepLinkConfigurator.assetIdParam)?.toIntOrNull()
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail.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_wallet_api.presentation.model.AssetPayload
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.detail.BalanceDetailFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
BalanceDetailModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface BalanceDetailComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance assetPayload: AssetPayload,
|
||||
): BalanceDetailComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: BalanceDetailFragment)
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.detail.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.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
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_ahm_api.domain.ChainMigrationInfoUseCase
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.locks.BalanceLocksInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.locks.BalanceLocksInteractorImpl
|
||||
import io.novafoundation.nova.feature_assets.domain.price.ChartsInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.send.SendInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.ControllableAssetCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.gifts.GiftsRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.detail.BalanceDetailViewModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.filter.HistoryFiltersProviderFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.history.mixin.TransactionHistoryMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.transaction.history.mixin.TransactionHistoryProvider
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class BalanceDetailModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideBalanceLocksInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
balanceLocksRepository: BalanceLocksRepository,
|
||||
balanceHoldsRepository: BalanceHoldsRepository,
|
||||
accountRepository: AccountRepository
|
||||
): BalanceLocksInteractor {
|
||||
return BalanceLocksInteractorImpl(
|
||||
chainRegistry = chainRegistry,
|
||||
balanceLocksRepository = balanceLocksRepository,
|
||||
balanceHoldsRepository = balanceHoldsRepository,
|
||||
accountRepository = accountRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideTransferHistoryMixin(
|
||||
walletInteractor: WalletInteractor,
|
||||
assetsRouter: AssetsRouter,
|
||||
historyFiltersProviderFactory: HistoryFiltersProviderFactory,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
resourceManager: ResourceManager,
|
||||
assetPayload: AssetPayload,
|
||||
addressDisplayUseCase: AddressDisplayUseCase,
|
||||
chainRegistry: ChainRegistry,
|
||||
currencyRepository: CurrencyRepository,
|
||||
assetIconProvider: AssetIconProvider
|
||||
): TransactionHistoryMixin {
|
||||
return TransactionHistoryProvider(
|
||||
walletInteractor = walletInteractor,
|
||||
router = assetsRouter,
|
||||
historyFiltersProviderFactory = historyFiltersProviderFactory,
|
||||
resourceManager = resourceManager,
|
||||
addressDisplayUseCase = addressDisplayUseCase,
|
||||
assetsSourceRegistry = assetSourceRegistry,
|
||||
chainRegistry = chainRegistry,
|
||||
chainId = assetPayload.chainId,
|
||||
assetId = assetPayload.chainAssetId,
|
||||
currencyRepository = currencyRepository,
|
||||
assetIconProvider
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(BalanceDetailViewModel::class)
|
||||
fun provideViewModel(
|
||||
walletInteractor: WalletInteractor,
|
||||
balanceLocksInteractor: BalanceLocksInteractor,
|
||||
sendInteractor: SendInteractor,
|
||||
router: AssetsRouter,
|
||||
transactionHistoryMixin: TransactionHistoryMixin,
|
||||
assetPayload: AssetPayload,
|
||||
accountUseCase: SelectedAccountUseCase,
|
||||
resourceManager: ResourceManager,
|
||||
currencyInteractor: CurrencyInteractor,
|
||||
controllableAssetCheckMixin: ControllableAssetCheckMixin,
|
||||
externalBalancesInteractor: ExternalBalancesInteractor,
|
||||
swapAvailabilityInteractor: SwapAvailabilityInteractor,
|
||||
assetIconProvider: AssetIconProvider,
|
||||
chartsInteractor: ChartsInteractor,
|
||||
buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
|
||||
amountFormatter: AmountFormatter,
|
||||
chainMigrationInfoUseCase: ChainMigrationInfoUseCase,
|
||||
giftsAvailableGiftAssetsUseCase: AvailableGiftAssetsUseCase,
|
||||
giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin,
|
||||
): ViewModel {
|
||||
return BalanceDetailViewModel(
|
||||
walletInteractor = walletInteractor,
|
||||
balanceLocksInteractor = balanceLocksInteractor,
|
||||
sendInteractor = sendInteractor,
|
||||
router = router,
|
||||
assetPayload = assetPayload,
|
||||
transactionHistoryMixin = transactionHistoryMixin,
|
||||
accountUseCase = accountUseCase,
|
||||
resourceManager = resourceManager,
|
||||
currencyInteractor = currencyInteractor,
|
||||
controllableAssetCheck = controllableAssetCheckMixin,
|
||||
externalBalancesInteractor = externalBalancesInteractor,
|
||||
swapAvailabilityInteractor = swapAvailabilityInteractor,
|
||||
assetIconProvider = assetIconProvider,
|
||||
chartsInteractor = chartsInteractor,
|
||||
buySellSelectorMixinFactory = buySellSelectorMixinFactory,
|
||||
amountFormatter = amountFormatter,
|
||||
chainMigrationInfoUseCase = chainMigrationInfoUseCase,
|
||||
giftsRestrictionCheckMixin = giftsRestrictionCheckMixin,
|
||||
giftsAvailableGiftAssetsUseCase = giftsAvailableGiftAssetsUseCase
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory,
|
||||
): BalanceDetailViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(BalanceDetailViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.list.EditablePlaceholderAdapter
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.hideKeyboard
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.ExpandableAnimationSettings
|
||||
import io.novafoundation.nova.common.utils.recyclerView.expandable.animator.ExpandableAnimator
|
||||
import io.novafoundation.nova.common.utils.recyclerView.space.SpaceBetween
|
||||
import io.novafoundation.nova.common.utils.recyclerView.space.addSpaceItemDecoration
|
||||
import io.novafoundation.nova.common.view.PlaceholderModel
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.databinding.FragmentBalanceListBinding
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureComponent
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.BalanceBreakdownBottomSheet
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.AssetBaseDecoration
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetTokensDecoration
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetTokensItemAnimator
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.BalanceListAdapter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.baseDecoration.applyDefaultTo
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.setupBuySellSelectorMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.createForAssets
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.items.TokenGroupUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetsHeaderAdapter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetsHeaderHolder
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.ManageAssetsAdapter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.ManageAssetsHolder
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.BannerHolder
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.bindWithAdapter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import javax.inject.Inject
|
||||
|
||||
class BalanceListFragment :
|
||||
BaseFragment<BalanceListViewModel, FragmentBalanceListBinding>(),
|
||||
BalanceListAdapter.ItemAssetHandler,
|
||||
AssetsHeaderAdapter.Handler,
|
||||
ManageAssetsAdapter.Handler {
|
||||
|
||||
override fun createBinding() = FragmentBalanceListBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private var balanceBreakdownBottomSheet: BalanceBreakdownBottomSheet? = null
|
||||
|
||||
private val headerAdapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
AssetsHeaderAdapter(this)
|
||||
}
|
||||
|
||||
private val bannerAdapter: PromotionBannerAdapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
PromotionBannerAdapter(closable = true)
|
||||
}
|
||||
|
||||
private val manageAssetsAdapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ManageAssetsAdapter(this)
|
||||
}
|
||||
|
||||
private val emptyAssetsPlaceholder by lazy(LazyThreadSafetyMode.NONE) {
|
||||
EditablePlaceholderAdapter(
|
||||
model = getAssetsPlaceholderModel(),
|
||||
clickListener = { buySellClicked() }
|
||||
)
|
||||
}
|
||||
|
||||
private val assetsAdapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BalanceListAdapter(imageLoader, this)
|
||||
}
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ConcatAdapter(headerAdapter, bannerAdapter, manageAssetsAdapter, emptyAssetsPlaceholder, assetsAdapter)
|
||||
}
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.balanceListAssets.applyStatusBarInsets()
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
hideKeyboard()
|
||||
|
||||
setupRecyclerView()
|
||||
|
||||
binder.walletContainer.setOnRefreshListener {
|
||||
viewModel.fullSync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
binder.balanceListAssets.setHasFixedSize(true)
|
||||
binder.balanceListAssets.adapter = adapter
|
||||
|
||||
setupAssetsDecorationForRecyclerView()
|
||||
setupRecyclerViewSpacing()
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<AssetsFeatureComponent>(
|
||||
requireContext(),
|
||||
AssetsFeatureApi::class.java
|
||||
)
|
||||
.balanceListComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: BalanceListViewModel) {
|
||||
setupBuySellSelectorMixin(viewModel.buySellSelectorMixin)
|
||||
|
||||
viewModel.bannersMixin.bindWithAdapter(bannerAdapter) {
|
||||
binder.balanceListAssets.invalidateItemDecorations()
|
||||
}
|
||||
|
||||
viewModel.assetListMixin.assetModelsFlow.observe {
|
||||
assetsAdapter.submitList(it) {
|
||||
binder.balanceListAssets.invalidateItemDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.maskingModeEnableFlow.observe(headerAdapter::setMaskingEnabled)
|
||||
viewModel.totalBalanceFlow.observe(headerAdapter::setTotalBalance)
|
||||
viewModel.selectedWalletModelFlow.observe(headerAdapter::setSelectedWallet)
|
||||
viewModel.shouldShowPlaceholderFlow.observe(emptyAssetsPlaceholder::show)
|
||||
viewModel.nftCountFlow.observe(headerAdapter::setNftCountLabel)
|
||||
viewModel.nftPreviewsUi.observe(headerAdapter::setNftPreviews)
|
||||
|
||||
viewModel.hideRefreshEvent.observeEvent {
|
||||
binder.walletContainer.isRefreshing = false
|
||||
}
|
||||
|
||||
viewModel.balanceBreakdownFlow.observe {
|
||||
if (balanceBreakdownBottomSheet?.isShowing == true) {
|
||||
balanceBreakdownBottomSheet?.setBalanceBreakdown(it)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.showBalanceBreakdownEvent.observeEvent { totalBalanceBreakdown ->
|
||||
if (balanceBreakdownBottomSheet == null) {
|
||||
balanceBreakdownBottomSheet = BalanceBreakdownBottomSheet(requireContext())
|
||||
|
||||
balanceBreakdownBottomSheet?.setOnDismissListener {
|
||||
balanceBreakdownBottomSheet = null
|
||||
}
|
||||
}
|
||||
balanceBreakdownBottomSheet?.setOnShowListener {
|
||||
balanceBreakdownBottomSheet?.setBalanceBreakdown(totalBalanceBreakdown)
|
||||
}
|
||||
balanceBreakdownBottomSheet?.show()
|
||||
}
|
||||
|
||||
viewModel.walletConnectAccountSessionsUI.observe(headerAdapter::setWalletConnectModel)
|
||||
viewModel.pendingOperationsCountModel.observe(headerAdapter::setPendingOperationsCountModel)
|
||||
viewModel.filtersIndicatorIcon.observe(headerAdapter::setFilterIconRes)
|
||||
viewModel.assetViewModeModelFlow.observe { manageAssetsAdapter.setAssetViewModeModel(it) }
|
||||
}
|
||||
|
||||
override fun assetClicked(asset: Chain.Asset) {
|
||||
viewModel.assetClicked(asset)
|
||||
}
|
||||
|
||||
override fun tokenGroupClicked(tokenGroup: TokenGroupUi) {
|
||||
if (tokenGroup.groupType is TokenGroupUi.GroupType.SingleItem) {
|
||||
viewModel.assetClicked(tokenGroup.groupType.asset)
|
||||
} else {
|
||||
val itemAnimator = binder.balanceListAssets.itemAnimator as AssetTokensItemAnimator
|
||||
itemAnimator.prepareForAnimation()
|
||||
|
||||
viewModel.assetListMixin.expandToken(tokenGroup)
|
||||
}
|
||||
}
|
||||
|
||||
override fun totalBalanceClicked() {
|
||||
viewModel.balanceBreakdownClicked()
|
||||
}
|
||||
|
||||
override fun manageClicked() {
|
||||
viewModel.manageClicked()
|
||||
}
|
||||
|
||||
override fun searchClicked() {
|
||||
viewModel.searchClicked()
|
||||
}
|
||||
|
||||
override fun avatarClicked() {
|
||||
viewModel.avatarClicked()
|
||||
}
|
||||
|
||||
override fun goToNftsClicked() {
|
||||
viewModel.goToNftsClicked()
|
||||
}
|
||||
|
||||
override fun walletConnectClicked() {
|
||||
viewModel.walletConnectClicked()
|
||||
}
|
||||
|
||||
override fun maskClicked() {
|
||||
viewModel.toggleMasking()
|
||||
}
|
||||
|
||||
override fun sendClicked() {
|
||||
viewModel.sendClicked()
|
||||
}
|
||||
|
||||
override fun receiveClicked() {
|
||||
viewModel.receiveClicked()
|
||||
}
|
||||
|
||||
override fun buySellClicked() {
|
||||
viewModel.buySellClicked()
|
||||
}
|
||||
|
||||
override fun novaCardClick() {
|
||||
viewModel.novaCardClicked()
|
||||
}
|
||||
|
||||
override fun pendingOperationsClicked() {
|
||||
viewModel.pendingOperationsClicked()
|
||||
}
|
||||
|
||||
override fun assetViewModeClicked() {
|
||||
viewModel.switchViewMode()
|
||||
}
|
||||
|
||||
override fun swapClicked() {
|
||||
viewModel.swapClicked()
|
||||
}
|
||||
|
||||
override fun giftClicked() {
|
||||
viewModel.giftClicked()
|
||||
}
|
||||
|
||||
private fun setupRecyclerViewSpacing() {
|
||||
binder.balanceListAssets.addSpaceItemDecoration {
|
||||
add(SpaceBetween(AssetsHeaderHolder, BannerHolder, spaceDp = 4))
|
||||
add(SpaceBetween(BannerHolder, ManageAssetsHolder, spaceDp = 4))
|
||||
add(SpaceBetween(AssetsHeaderHolder, ManageAssetsHolder, spaceDp = 24))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAssetsDecorationForRecyclerView() {
|
||||
val animationSettings = ExpandableAnimationSettings.createForAssets()
|
||||
val animator = ExpandableAnimator(binder.balanceListAssets, animationSettings, assetsAdapter)
|
||||
|
||||
AssetBaseDecoration.applyDefaultTo(binder.balanceListAssets, assetsAdapter)
|
||||
|
||||
binder.balanceListAssets.addItemDecoration(AssetTokensDecoration(requireContext(), assetsAdapter, animator))
|
||||
binder.balanceListAssets.itemAnimator = AssetTokensItemAnimator(animationSettings, animator)
|
||||
}
|
||||
|
||||
private fun getAssetsPlaceholderModel() = PlaceholderModel(
|
||||
text = getString(R.string.wallet_assets_empty),
|
||||
imageRes = R.drawable.ic_planet_outline,
|
||||
buttonText = getString(R.string.assets_buy_tokens_placeholder_button)
|
||||
)
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.list
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.data.model.AssetViewMode
|
||||
import io.novafoundation.nova.common.data.model.MaskingMode
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.common.domain.dataOrNull
|
||||
import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase
|
||||
import io.novafoundation.nova.common.presentation.LoadingState
|
||||
import io.novafoundation.nova.common.presentation.masking.MaskableModel
|
||||
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.formatting.formatAsPercentage
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.utils.withSafeLoading
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_assets.R
|
||||
import io.novafoundation.nova.feature_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.list.NftPreviews
|
||||
import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdown
|
||||
import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdownInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.BalanceBreakdownAmount
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.BalanceBreakdownItem
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.BalanceBreakdownTotal
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.breakdown.model.TotalBalanceBreakdownModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetListMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixin
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.NftPreviewUi
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.model.TotalBalanceModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.AssetViewModeModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.view.PendingOperationsCountModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.novacard.common.NovaCardRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.assetsSource
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency
|
||||
import io.novafoundation.nova.feature_nft_api.data.model.Nft
|
||||
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.FiatFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.mapBalanceIdToUi
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatter
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.gifts.GiftsRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.FiatConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.FractionPartStyling
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.mapNumberOfActiveSessionsToUi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private typealias SyncAction = suspend (MetaAccount) -> Unit
|
||||
|
||||
class BalanceListViewModel(
|
||||
private val promotionBannersMixinFactory: PromotionBannersMixinFactory,
|
||||
private val bannerSourceFactory: BannersSourceFactory,
|
||||
private val walletInteractor: WalletInteractor,
|
||||
private val assetsListInteractor: AssetsListInteractor,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val router: AssetsRouter,
|
||||
private val currencyInteractor: CurrencyInteractor,
|
||||
private val balanceBreakdownInteractor: BalanceBreakdownInteractor,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val walletConnectSessionsUseCase: WalletConnectSessionsUseCase,
|
||||
private val swapAvailabilityInteractor: SwapAvailabilityInteractor,
|
||||
private val assetListMixinFactory: AssetListMixinFactory,
|
||||
private val amountFormatter: AmountFormatter,
|
||||
private val fiatFormatter: FiatFormatter,
|
||||
private val maskableValueFormatterProvider: MaskableValueFormatterProvider,
|
||||
private val buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
|
||||
private val multisigPendingOperationsService: MultisigPendingOperationsService,
|
||||
private val novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin,
|
||||
private val maskingModeUseCase: MaskingModeUseCase,
|
||||
private val giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val maskableAmountFormatterFlow = maskableValueFormatterProvider.provideFormatter()
|
||||
.shareInBackground()
|
||||
|
||||
private val _hideRefreshEvent = MutableLiveData<Event<Unit>>()
|
||||
val hideRefreshEvent: LiveData<Event<Unit>> = _hideRefreshEvent
|
||||
|
||||
private val _showBalanceBreakdownEvent = MutableLiveData<Event<TotalBalanceBreakdownModel>>()
|
||||
val showBalanceBreakdownEvent: LiveData<Event<TotalBalanceBreakdownModel>> = _showBalanceBreakdownEvent
|
||||
|
||||
val bannersMixin = promotionBannersMixinFactory.create(bannerSourceFactory.assetsSource(), viewModelScope)
|
||||
|
||||
private val selectedCurrency = currencyInteractor.observeSelectCurrency()
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
private val fullSyncActions: List<SyncAction> = listOf(
|
||||
{ walletInteractor.syncAssetsRates(selectedCurrency.first()) },
|
||||
walletInteractor::syncAllNfts
|
||||
)
|
||||
|
||||
val buySellSelectorMixin = buySellSelectorMixinFactory.create(BuySellSelectorMixin.SelectorType.AllAssets, viewModelScope)
|
||||
|
||||
val assetListMixin = assetListMixinFactory.create(viewModelScope)
|
||||
|
||||
private val externalBalancesFlow = assetListMixin.externalBalancesFlow
|
||||
|
||||
private val isFiltersEnabledFlow = walletInteractor.isFiltersEnabledFlow()
|
||||
|
||||
private val accountChangeSyncActions: List<SyncAction> = listOf(
|
||||
walletInteractor::syncAllNfts
|
||||
)
|
||||
|
||||
private val selectedMetaAccount = selectedAccountUseCase.selectedMetaAccountFlow()
|
||||
.share()
|
||||
|
||||
val selectedWalletModelFlow = selectedAccountUseCase.selectedWalletModelFlow()
|
||||
.shareInBackground()
|
||||
|
||||
private val balanceBreakdown = balanceBreakdownInteractor.balanceBreakdownFlow(assetListMixin.assetsFlow, externalBalancesFlow)
|
||||
.shareInBackground()
|
||||
|
||||
private val nftsPreviews = assetsListInteractor.observeNftPreviews()
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val nftCountFlow = nftsPreviews
|
||||
.combine(maskableAmountFormatterFlow, ::formatNftCount)
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val nftPreviewsUi = nftsPreviews
|
||||
.combine(maskableAmountFormatterFlow, ::mapNftPreviewToUi)
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val maskingModeEnableFlow = maskingModeUseCase.observeMaskingMode()
|
||||
.map { it == MaskingMode.ENABLED }
|
||||
.shareInBackground()
|
||||
|
||||
val totalBalanceFlow = combine(
|
||||
balanceBreakdown,
|
||||
swapAvailabilityInteractor.anySwapAvailableFlow(),
|
||||
maskableAmountFormatterFlow
|
||||
) { breakdown, swapSupported, maskableAmountFormatter ->
|
||||
val currency = selectedCurrency.first()
|
||||
TotalBalanceModel(
|
||||
isBreakdownAvailable = breakdown.breakdown.isNotEmpty(),
|
||||
totalBalanceFiat = maskableAmountFormatter.format {
|
||||
fiatFormatter.formatFiat(
|
||||
breakdown.total,
|
||||
currency,
|
||||
config = FiatConfig(
|
||||
abbreviationStyle = FiatConfig.AbbreviationStyle.SIMPLE_ABBREVIATION,
|
||||
fractionPartStyling = FractionPartStyling.Styled(R.dimen.total_balance_fraction_size)
|
||||
)
|
||||
)
|
||||
},
|
||||
lockedBalanceFiat = maskableAmountFormatter.format { fiatFormatter.formatFiat(breakdown.locksTotal.amount, currency) },
|
||||
enableSwap = swapSupported
|
||||
)
|
||||
}
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val shouldShowPlaceholderFlow = assetListMixin.assetModelsFlow.map { it.isEmpty() }
|
||||
|
||||
val balanceBreakdownFlow = balanceBreakdown.map {
|
||||
val currency = selectedCurrency.first()
|
||||
val total = it.total.formatAsCurrency(currency)
|
||||
TotalBalanceBreakdownModel(total, mapBreakdownToList(it, currency))
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
private val walletConnectAccountSessionCount = selectedMetaAccount.flatMapLatest {
|
||||
walletConnectSessionsUseCase.activeSessionsNumberFlow(it)
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
val walletConnectAccountSessionsUI = walletConnectAccountSessionCount
|
||||
.map(::mapNumberOfActiveSessionsToUi)
|
||||
.shareInBackground()
|
||||
|
||||
val filtersIndicatorIcon = isFiltersEnabledFlow
|
||||
.map { if (it) R.drawable.ic_chip_filter_indicator else R.drawable.ic_chip_filter }
|
||||
.shareInBackground()
|
||||
|
||||
val assetViewModeModelFlow = assetListMixin.assetsViewModeFlow.map {
|
||||
when (it) {
|
||||
AssetViewMode.NETWORKS -> AssetViewModeModel(R.drawable.ic_asset_view_networks, R.string.asset_view_networks)
|
||||
AssetViewMode.TOKENS -> AssetViewModeModel(R.drawable.ic_asset_view_tokens, R.string.asset_view_tokens)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val pendingOperationsCountModel = multisigPendingOperationsService.pendingOperationsCountFlow()
|
||||
.withSafeLoading()
|
||||
.combine(maskableAmountFormatterFlow, ::formatPendingOperationsCount)
|
||||
.shareInBackground()
|
||||
|
||||
init {
|
||||
selectedCurrency
|
||||
.onEach { fullSync() }
|
||||
.launchIn(this)
|
||||
|
||||
nftsPreviews
|
||||
.debounce(1L.seconds)
|
||||
.onEach { nfts ->
|
||||
nfts.nftPreviews
|
||||
.filter { it.details is Nft.Details.Loadable }
|
||||
.forEach { assetsListInteractor.fullSyncNft(it) }
|
||||
}
|
||||
.inBackground()
|
||||
.launchIn(this)
|
||||
|
||||
selectedMetaAccount
|
||||
.mapLatest { syncWith(accountChangeSyncActions, it) }
|
||||
.launchIn(this)
|
||||
|
||||
walletInteractor.nftSyncTrigger()
|
||||
.onEach { trigger -> walletInteractor.syncChainNfts(selectedMetaAccount.first(), trigger.chain) }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun fullSync() {
|
||||
viewModelScope.launch {
|
||||
syncWith(fullSyncActions, selectedMetaAccount.first())
|
||||
|
||||
_hideRefreshEvent.value = Event(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun assetClicked(asset: Chain.Asset) {
|
||||
val payload = AssetPayload(
|
||||
chainId = asset.chainId,
|
||||
chainAssetId = asset.id
|
||||
)
|
||||
|
||||
router.openAssetDetails(payload)
|
||||
}
|
||||
|
||||
fun avatarClicked() {
|
||||
router.openSwitchWallet()
|
||||
}
|
||||
|
||||
fun manageClicked() {
|
||||
router.openManageTokens()
|
||||
}
|
||||
|
||||
fun goToNftsClicked() {
|
||||
router.openNfts()
|
||||
}
|
||||
|
||||
fun searchClicked() {
|
||||
router.openAssetSearch()
|
||||
}
|
||||
|
||||
fun walletConnectClicked() {
|
||||
launch {
|
||||
if (walletConnectAccountSessionCount.first() > 0) {
|
||||
val metaAccount = selectedMetaAccount.first()
|
||||
router.openWalletConnectSessions(metaAccount.id)
|
||||
} else {
|
||||
router.openWalletConnectScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun balanceBreakdownClicked() {
|
||||
launch {
|
||||
val totalBalance = totalBalanceFlow.first()
|
||||
if (totalBalance.isBreakdownAvailable) {
|
||||
val balanceBreakdown = balanceBreakdownFlow.first()
|
||||
_showBalanceBreakdownEvent.value = Event(balanceBreakdown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncWith(syncActions: List<SyncAction>, metaAccount: MetaAccount) = if (syncActions.size == 1) {
|
||||
val syncAction = syncActions.first()
|
||||
syncAction(metaAccount)
|
||||
} else {
|
||||
val syncJobs = syncActions.map { async { it(metaAccount) } }
|
||||
syncJobs.joinAll()
|
||||
}
|
||||
|
||||
private fun mapNftPreviewToUi(nftPreviews: NftPreviews, maskableValueFormatter: MaskableValueFormatter): MaskableModel<List<NftPreviewUi>> {
|
||||
return maskableValueFormatter.format {
|
||||
nftPreviews.nftPreviews.map {
|
||||
when (val details = it.details) {
|
||||
Nft.Details.Loadable -> LoadingState.Loading()
|
||||
is Nft.Details.Loaded -> {
|
||||
LoadingState.Loaded(details.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapBreakdownToList(balanceBreakdown: BalanceBreakdown, currency: Currency): List<BalanceBreakdownItem> {
|
||||
return buildList {
|
||||
add(
|
||||
BalanceBreakdownTotal(
|
||||
resourceManager.getString(R.string.wallet_balance_transferable),
|
||||
balanceBreakdown.transferableTotal.amount.formatAsCurrency(currency),
|
||||
R.drawable.ic_transferable,
|
||||
balanceBreakdown.transferableTotal.percentage.formatAsPercentage()
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
BalanceBreakdownTotal(
|
||||
resourceManager.getString(R.string.wallet_balance_locked),
|
||||
balanceBreakdown.locksTotal.amount.formatAsCurrency(currency),
|
||||
R.drawable.ic_lock,
|
||||
balanceBreakdown.locksTotal.percentage.formatAsPercentage()
|
||||
)
|
||||
)
|
||||
|
||||
val breakdown = balanceBreakdown.breakdown.map {
|
||||
BalanceBreakdownAmount(
|
||||
name = it.token.configuration.symbol.value + " " + mapBalanceIdToUi(resourceManager, it.id),
|
||||
amount = amountFormatter.formatAmountToAmountModel(it.tokenAmount, it.token)
|
||||
)
|
||||
}
|
||||
|
||||
addAll(breakdown)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPendingOperationsCount(
|
||||
operationsLoadingState: ExtendedLoadingState<Int>,
|
||||
formatter: MaskableValueFormatter
|
||||
): PendingOperationsCountModel {
|
||||
return when (val count = operationsLoadingState.dataOrNull) {
|
||||
null, 0 -> PendingOperationsCountModel.Gone
|
||||
|
||||
else -> PendingOperationsCountModel.Visible(formatter.format { count.format() })
|
||||
}
|
||||
}
|
||||
|
||||
fun sendClicked() {
|
||||
router.openSendFlow()
|
||||
}
|
||||
|
||||
fun receiveClicked() {
|
||||
router.openReceiveFlow()
|
||||
}
|
||||
|
||||
fun buySellClicked() {
|
||||
buySellSelectorMixin.openSelector()
|
||||
}
|
||||
|
||||
fun swapClicked() {
|
||||
router.openSwapFlow()
|
||||
}
|
||||
|
||||
fun giftClicked() = launchUnit {
|
||||
giftsRestrictionCheckMixin.checkRestrictionAndDo {
|
||||
router.openGifts()
|
||||
}
|
||||
}
|
||||
|
||||
fun novaCardClicked() = launchUnit {
|
||||
novaCardRestrictionCheckMixin.checkRestrictionAndDo {
|
||||
router.openNovaCard()
|
||||
}
|
||||
}
|
||||
|
||||
fun switchViewMode() {
|
||||
launch { assetListMixin.switchViewMode() }
|
||||
}
|
||||
|
||||
fun pendingOperationsClicked() {
|
||||
router.openPendingMultisigOperations()
|
||||
}
|
||||
|
||||
private fun formatNftCount(nftPreviews: NftPreviews, formatter: MaskableValueFormatter): MaskableModel<String>? {
|
||||
if (nftPreviews.totalNftsCount == 0) return null
|
||||
|
||||
return formatter.format { nftPreviews.totalNftsCount.format() }
|
||||
}
|
||||
|
||||
fun toggleMasking() {
|
||||
maskingModeUseCase.toggleMaskingMode()
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.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_assets.presentation.balance.list.BalanceListFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
BalanceListModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface BalanceListComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): BalanceListComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: BalanceListFragment)
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.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.data.repository.AssetsViewModeRepository
|
||||
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.domain.usecase.MaskingModeUseCase
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.MultisigPendingOperationsService
|
||||
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_assets.domain.WalletInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.ExternalBalancesInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.assets.list.AssetsListInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.breakdown.BalanceBreakdownInteractor
|
||||
import io.novafoundation.nova.feature_assets.presentation.AssetsRouter
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.AssetListMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.ExpandableAssetsMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.buySell.BuySellSelectorMixinFactory
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.list.BalanceListViewModel
|
||||
import io.novafoundation.nova.feature_assets.presentation.novacard.common.NovaCardRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository
|
||||
import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.FiatFormatter
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
|
||||
import io.novafoundation.nova.feature_assets.presentation.balance.common.gifts.GiftsRestrictionCheckMixin
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class BalanceListModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
accountRepository: AccountRepository,
|
||||
nftRepository: NftRepository,
|
||||
assetsViewModeRepository: AssetsViewModeRepository
|
||||
) = AssetsListInteractor(accountRepository, nftRepository, assetsViewModeRepository)
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideBalanceBreakdownInteractor(
|
||||
accountRepository: AccountRepository,
|
||||
balanceLocksRepository: BalanceLocksRepository,
|
||||
balanceHoldsRepository: BalanceHoldsRepository
|
||||
): BalanceBreakdownInteractor {
|
||||
return BalanceBreakdownInteractor(
|
||||
accountRepository,
|
||||
balanceLocksRepository,
|
||||
balanceHoldsRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideAssetListMixinFactory(
|
||||
walletInteractor: WalletInteractor,
|
||||
assetsListInteractor: AssetsListInteractor,
|
||||
externalBalancesInteractor: ExternalBalancesInteractor,
|
||||
expandableAssetsMixinFactory: ExpandableAssetsMixinFactory
|
||||
): AssetListMixinFactory {
|
||||
return AssetListMixinFactory(
|
||||
walletInteractor,
|
||||
assetsListInteractor,
|
||||
externalBalancesInteractor,
|
||||
expandableAssetsMixinFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(BalanceListViewModel::class)
|
||||
fun provideViewModel(
|
||||
promotionBannersMixinFactory: PromotionBannersMixinFactory,
|
||||
bannerSourceFactory: BannersSourceFactory,
|
||||
walletInteractor: WalletInteractor,
|
||||
assetsListInteractor: AssetsListInteractor,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
router: AssetsRouter,
|
||||
currencyInteractor: CurrencyInteractor,
|
||||
balanceBreakdownInteractor: BalanceBreakdownInteractor,
|
||||
resourceManager: ResourceManager,
|
||||
walletConnectSessionsUseCase: WalletConnectSessionsUseCase,
|
||||
swapAvailabilityInteractor: SwapAvailabilityInteractor,
|
||||
assetListMixinFactory: AssetListMixinFactory,
|
||||
amountFormatter: AmountFormatter,
|
||||
buySellSelectorMixinFactory: BuySellSelectorMixinFactory,
|
||||
multisigPendingOperationsService: MultisigPendingOperationsService,
|
||||
novaCardRestrictionCheckMixin: NovaCardRestrictionCheckMixin,
|
||||
maskableValueFormatterProvider: MaskableValueFormatterProvider,
|
||||
maskingModeUseCase: MaskingModeUseCase,
|
||||
fiatFormatter: FiatFormatter,
|
||||
giftsRestrictionCheckMixin: GiftsRestrictionCheckMixin,
|
||||
): ViewModel {
|
||||
return BalanceListViewModel(
|
||||
promotionBannersMixinFactory = promotionBannersMixinFactory,
|
||||
bannerSourceFactory = bannerSourceFactory,
|
||||
walletInteractor = walletInteractor,
|
||||
assetsListInteractor = assetsListInteractor,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
router = router,
|
||||
currencyInteractor = currencyInteractor,
|
||||
balanceBreakdownInteractor = balanceBreakdownInteractor,
|
||||
resourceManager = resourceManager,
|
||||
walletConnectSessionsUseCase = walletConnectSessionsUseCase,
|
||||
swapAvailabilityInteractor = swapAvailabilityInteractor,
|
||||
assetListMixinFactory = assetListMixinFactory,
|
||||
amountFormatter = amountFormatter,
|
||||
maskableValueFormatterProvider = maskableValueFormatterProvider,
|
||||
buySellSelectorMixinFactory = buySellSelectorMixinFactory,
|
||||
multisigPendingOperationsService = multisigPendingOperationsService,
|
||||
novaCardRestrictionCheckMixin = novaCardRestrictionCheckMixin,
|
||||
maskingModeUseCase = maskingModeUseCase,
|
||||
fiatFormatter = fiatFormatter,
|
||||
giftsRestrictionCheckMixin = giftsRestrictionCheckMixin
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory,
|
||||
): BalanceListViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(BalanceListViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_assets.presentation.balance.list.model
|
||||
|
||||
import io.novafoundation.nova.common.presentation.LoadingState
|
||||
|
||||
typealias NftMedia = String?
|
||||
typealias NftPreviewUi = LoadingState<NftMedia>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user