mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 05:38:02 +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,37 @@
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
android {
|
||||
namespace 'io.novafoundation.nova.feature_wallet_api'
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation coroutinesDep
|
||||
implementation project(':runtime')
|
||||
implementation project(":feature-account-api")
|
||||
implementation project(":feature-currency-api")
|
||||
implementation project(':runtime')
|
||||
implementation project(":common")
|
||||
|
||||
implementation androidDep
|
||||
implementation materialDep
|
||||
|
||||
implementation daggerDep
|
||||
implementation project(':feature-xcm:api')
|
||||
ksp daggerCompiler
|
||||
|
||||
implementation substrateSdkDep
|
||||
|
||||
implementation constraintDep
|
||||
|
||||
implementation lifeCycleKtxDep
|
||||
|
||||
api project(':core-api')
|
||||
api project(':core-db')
|
||||
|
||||
|
||||
testImplementation project(':test-shared')
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.cache
|
||||
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.core_db.dao.AssetDao
|
||||
import io.novafoundation.nova.core_db.dao.AssetReadOnlyCache
|
||||
import io.novafoundation.nova.core_db.dao.ClearAssetsParams
|
||||
import io.novafoundation.nova.core_db.dao.TokenDao
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal
|
||||
import io.novafoundation.nova.core_db.model.TokenLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.ext.enabledAssets
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AssetCache(
|
||||
private val tokenDao: TokenDao,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val assetDao: AssetDao,
|
||||
) : AssetReadOnlyCache by assetDao {
|
||||
|
||||
private val assetUpdateMutex = Mutex()
|
||||
|
||||
/**
|
||||
* @return true if asset was changed. false if it remained the same
|
||||
*/
|
||||
suspend fun updateAsset(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset,
|
||||
builder: (local: AssetLocal) -> AssetLocal,
|
||||
): Boolean = withContext(Dispatchers.IO) {
|
||||
val assetId = chainAsset.id
|
||||
val chainId = chainAsset.chainId
|
||||
|
||||
assetUpdateMutex.withLock {
|
||||
val cachedAsset = assetDao.getAsset(metaId, chainId, assetId) ?: AssetLocal.createEmpty(assetId, chainId, metaId)
|
||||
|
||||
val newAsset = builder.invoke(cachedAsset)
|
||||
|
||||
assetDao.insertAsset(newAsset)
|
||||
|
||||
cachedAsset != newAsset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see updateAsset
|
||||
*/
|
||||
suspend fun updateAsset(
|
||||
accountId: AccountId,
|
||||
chainAsset: Chain.Asset,
|
||||
builder: (local: AssetLocal) -> AssetLocal,
|
||||
): Boolean = withContext(Dispatchers.IO) {
|
||||
val applicableMetaAccount = accountRepository.findMetaAccount(accountId, chainAsset.chainId)
|
||||
|
||||
applicableMetaAccount?.let {
|
||||
updateAsset(it.id, chainAsset, builder)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
suspend fun updateAssetsByChain(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
builder: (Chain.Asset) -> AssetLocal
|
||||
): CollectionDiffer.Diff<AssetLocal> = withContext(Dispatchers.IO) {
|
||||
val oldAssetsLocal = getAssetsInChain(metaAccount.id, chain.id)
|
||||
val newAssetsLocal = chain.enabledAssets().map { builder(it) }
|
||||
val diff = CollectionDiffer.findDiff(newAssetsLocal, oldAssetsLocal, forceUseNewItems = false)
|
||||
assetDao.insertAssets(diff.newOrUpdated)
|
||||
diff
|
||||
}
|
||||
|
||||
suspend fun clearAssets(assetIds: List<FullChainAssetId>) = withContext(Dispatchers.IO) {
|
||||
val localAssetIds = assetIds.map { ClearAssetsParams(it.chainId, it.assetId) }
|
||||
assetDao.clearAssets(localAssetIds)
|
||||
}
|
||||
|
||||
suspend fun deleteAllTokens() {
|
||||
tokenDao.deleteAll()
|
||||
}
|
||||
|
||||
suspend fun updateTokens(newTokens: List<TokenLocal>) {
|
||||
val oldTokens = tokenDao.getTokens()
|
||||
val diff = CollectionDiffer.findDiff(newTokens, oldTokens, forceUseNewItems = false)
|
||||
tokenDao.applyDiff(diff)
|
||||
}
|
||||
|
||||
suspend fun insertToken(tokens: TokenLocal) = tokenDao.insertToken(tokens)
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.cache
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountInfo
|
||||
import io.novafoundation.nova.common.domain.balance.EDCountingMode
|
||||
import io.novafoundation.nova.common.domain.balance.TransferableMode
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal.EDCountingModeLocal
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal.TransferableModeLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.ChainAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
|
||||
suspend fun AssetCache.updateAsset(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset,
|
||||
accountInfo: AccountInfo,
|
||||
) = updateAsset(metaId, chainAsset, nativeBalanceUpdater(accountInfo))
|
||||
|
||||
suspend fun AssetCache.updateAsset(
|
||||
accountId: AccountId,
|
||||
chainAsset: Chain.Asset,
|
||||
accountInfo: AccountInfo,
|
||||
) = updateAsset(accountId, chainAsset, nativeBalanceUpdater(accountInfo))
|
||||
|
||||
suspend fun AssetCache.updateNonLockableAsset(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset,
|
||||
assetBalance: Balance,
|
||||
) {
|
||||
updateAsset(metaId, chainAsset) {
|
||||
it.copy(
|
||||
freeInPlanks = assetBalance,
|
||||
frozenInPlanks = Balance.ZERO,
|
||||
reservedInPlanks = Balance.ZERO,
|
||||
transferableMode = TransferableModeLocal.REGULAR,
|
||||
edCountingMode = EDCountingModeLocal.TOTAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun AssetCache.updateFromChainBalance(
|
||||
metaId: Long,
|
||||
chainAssetBalance: ChainAssetBalance
|
||||
) {
|
||||
updateAsset(metaId, chainAssetBalance.chainAsset) {
|
||||
it.copy(
|
||||
freeInPlanks = chainAssetBalance.free,
|
||||
frozenInPlanks = chainAssetBalance.frozen,
|
||||
reservedInPlanks = chainAssetBalance.reserved,
|
||||
transferableMode = chainAssetBalance.transferableMode.toLocal(),
|
||||
edCountingMode = chainAssetBalance.edCountingMode.toLocal()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun TransferableMode.toLocal(): TransferableModeLocal {
|
||||
return when (this) {
|
||||
TransferableMode.REGULAR -> TransferableModeLocal.REGULAR
|
||||
TransferableMode.HOLDS_AND_FREEZES -> TransferableModeLocal.HOLDS_AND_FREEZES
|
||||
}
|
||||
}
|
||||
|
||||
fun EDCountingMode.toLocal(): EDCountingModeLocal {
|
||||
return when (this) {
|
||||
EDCountingMode.TOTAL -> EDCountingModeLocal.TOTAL
|
||||
EDCountingMode.FREE -> EDCountingModeLocal.FREE
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeBalanceUpdater(accountInfo: AccountInfo) = { asset: AssetLocal ->
|
||||
val data = accountInfo.data
|
||||
|
||||
val transferableMode: TransferableModeLocal
|
||||
val edCountingMode: EDCountingModeLocal
|
||||
|
||||
if (data.flags.holdsAndFreezesEnabled()) {
|
||||
transferableMode = TransferableModeLocal.HOLDS_AND_FREEZES
|
||||
edCountingMode = EDCountingModeLocal.FREE
|
||||
} else {
|
||||
transferableMode = TransferableModeLocal.REGULAR
|
||||
edCountingMode = EDCountingModeLocal.TOTAL
|
||||
}
|
||||
|
||||
asset.copy(
|
||||
freeInPlanks = data.free,
|
||||
frozenInPlanks = data.frozen,
|
||||
reservedInPlanks = data.reserved,
|
||||
transferableMode = transferableMode,
|
||||
edCountingMode = edCountingMode
|
||||
)
|
||||
}
|
||||
|
||||
fun bindAccountInfoOrDefault(hex: String?, runtime: RuntimeSnapshot): AccountInfo {
|
||||
return hex?.let { bindAccountInfo(it, runtime) } ?: AccountInfo.empty()
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.cache
|
||||
|
||||
import io.novafoundation.nova.core_db.dao.CoinPriceDao
|
||||
import io.novafoundation.nova.core_db.model.CoinPriceLocal
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.data.source.CoinPriceLocalDataSource
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
|
||||
class CoinPriceLocalDataSourceImpl(
|
||||
private val coinPriceDao: CoinPriceDao
|
||||
) : CoinPriceLocalDataSource {
|
||||
|
||||
override suspend fun getFloorCoinPriceAtTime(priceId: String, currency: Currency, timestamp: Long): HistoricalCoinRate? {
|
||||
val coinPriceLocal = coinPriceDao.getFloorCoinPriceAtTime(priceId, currency.code, timestamp)
|
||||
return coinPriceLocal?.let { mapCoinPriceFromLocal(it) }
|
||||
}
|
||||
|
||||
override suspend fun hasCeilingCoinPriceAtTime(priceId: String, currency: Currency, timestamp: Long): Boolean {
|
||||
return coinPriceDao.hasCeilingCoinPriceAtTime(priceId, currency.code, timestamp)
|
||||
}
|
||||
|
||||
override suspend fun getCoinPriceRange(priceId: String, currency: Currency, fromTimestamp: Long, toTimestamp: Long): List<HistoricalCoinRate> {
|
||||
return coinPriceDao.getCoinPriceRange(priceId, currency.code, fromTimestamp, toTimestamp)
|
||||
.map { mapCoinPriceFromLocal(it) }
|
||||
}
|
||||
|
||||
override suspend fun updateCoinPrice(priceId: String, currency: Currency, coinRate: List<HistoricalCoinRate>) {
|
||||
coinPriceDao.updateCoinPrices(priceId, currency.code, coinRate.map { mapCoinPriceToLocal(priceId, currency, it) })
|
||||
}
|
||||
|
||||
private fun mapCoinPriceFromLocal(coinPriceLocal: CoinPriceLocal): HistoricalCoinRate {
|
||||
return HistoricalCoinRate(
|
||||
timestamp = coinPriceLocal.timestamp,
|
||||
rate = coinPriceLocal.rate
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapCoinPriceToLocal(priceId: String, currency: Currency, coinPrice: HistoricalCoinRate): CoinPriceLocal {
|
||||
return CoinPriceLocal(
|
||||
priceId = priceId,
|
||||
currencyId = currency.code,
|
||||
timestamp = coinPrice.timestamp,
|
||||
rate = coinPrice.rate
|
||||
)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.mappers
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.presentation.masking.MaskableModel
|
||||
import io.novafoundation.nova.common.presentation.masking.map
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_wallet_api.R
|
||||
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.presentation.formatters.formatPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetModel
|
||||
|
||||
@Deprecated("Create and use special formatter for that")
|
||||
fun mapAssetToAssetModel(
|
||||
assetIconProvider: AssetIconProvider,
|
||||
asset: Asset,
|
||||
resourceManager: ResourceManager,
|
||||
maskableBalance: MaskableModel<Balance>,
|
||||
icon: Icon = assetIconProvider.getAssetIconOrFallback(asset.token.configuration),
|
||||
@StringRes patternId: Int? = R.string.common_available_format
|
||||
): AssetModel {
|
||||
val formattedAmount = maskableBalance.map { it.formatPlanks(asset.token.configuration) }
|
||||
.map { amount -> patternId?.let { resourceManager.getString(patternId, amount) } ?: amount }
|
||||
|
||||
return with(asset) {
|
||||
AssetModel(
|
||||
chainId = asset.token.configuration.chainId,
|
||||
chainAssetId = asset.token.configuration.id,
|
||||
icon = icon,
|
||||
tokenName = token.configuration.name,
|
||||
tokenSymbol = token.configuration.symbol.value,
|
||||
assetBalance = formattedAmount
|
||||
)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.mappers
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.toFeeDisplay
|
||||
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
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("This is a internal logic related to fee mixin. To access or set the fee use corresponding methods from FeeLoaderMixinV2.Presentation")
|
||||
fun <F : FeeBase> mapFeeToFeeModel(
|
||||
fee: F,
|
||||
token: Token,
|
||||
includeZeroFiat: Boolean = true,
|
||||
amountFormatter: AmountFormatter
|
||||
): FeeModel<F, FeeDisplay> = FeeModel(
|
||||
display = amountFormatter.formatAmountToAmountModel(
|
||||
amountInPlanks = fee.amount,
|
||||
token = token,
|
||||
AmountConfig(includeZeroFiat = includeZeroFiat)
|
||||
).toFeeDisplay(),
|
||||
fee = fee
|
||||
)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.mappers
|
||||
|
||||
import io.novafoundation.nova.core_db.model.AssetAndChainId
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationBaseLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
|
||||
fun mapOperationStatusToOperationLocalStatus(status: Operation.Status) = when (status) {
|
||||
Operation.Status.PENDING -> OperationBaseLocal.Status.PENDING
|
||||
Operation.Status.COMPLETED -> OperationBaseLocal.Status.COMPLETED
|
||||
Operation.Status.FAILED -> OperationBaseLocal.Status.FAILED
|
||||
}
|
||||
|
||||
fun mapAssetWithAmountToLocal(
|
||||
chainAssetWithAmount: ChainAssetWithAmount
|
||||
): SwapTypeLocal.AssetWithAmount = with(chainAssetWithAmount) {
|
||||
return SwapTypeLocal.AssetWithAmount(
|
||||
assetId = AssetAndChainId(chainAsset.chainId, chainAsset.id),
|
||||
amount = amount
|
||||
)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain
|
||||
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.argumentType
|
||||
import io.novafoundation.nova.common.utils.balances
|
||||
import io.novafoundation.nova.common.utils.firstExistingCallName
|
||||
import io.novafoundation.nova.common.utils.hasCall
|
||||
import io.novafoundation.nova.runtime.util.constructAccountLookupInstance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.call
|
||||
import java.math.BigInteger
|
||||
|
||||
enum class TransferMode {
|
||||
KEEP_ALIVE, ALLOW_DEATH, ALL
|
||||
}
|
||||
|
||||
fun ExtrinsicBuilder.nativeTransfer(accountId: AccountId, amount: BigInteger, mode: TransferMode = TransferMode.ALLOW_DEATH): ExtrinsicBuilder {
|
||||
when (mode) {
|
||||
TransferMode.KEEP_ALIVE -> transferKeepAlive(accountId, amount)
|
||||
TransferMode.ALLOW_DEATH -> transferAllowDeath(accountId, amount)
|
||||
TransferMode.ALL -> transferAll(accountId, amount)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun ExtrinsicBuilder.transferKeepAlive(accountId: AccountId, amount: BigInteger) {
|
||||
val destType = runtime.metadata.balances().call("transfer_keep_alive").argumentType("dest")
|
||||
|
||||
call(
|
||||
moduleName = Modules.BALANCES,
|
||||
callName = "transfer_keep_alive",
|
||||
arguments = mapOf(
|
||||
"dest" to destType.constructAccountLookupInstance(accountId),
|
||||
"value" to amount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExtrinsicBuilder.transferAllowDeath(accountId: AccountId, amount: BigInteger) {
|
||||
val callName = runtime.metadata.balances().firstExistingCallName("transfer_allow_death", "transfer")
|
||||
val destType = runtime.metadata.balances().call(callName).argumentType("dest")
|
||||
|
||||
call(
|
||||
moduleName = Modules.BALANCES,
|
||||
callName = callName,
|
||||
arguments = mapOf(
|
||||
"dest" to destType.constructAccountLookupInstance(accountId),
|
||||
"value" to amount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExtrinsicBuilder.transferAll(accountId: AccountId, amount: BigInteger) {
|
||||
val transferAllPresent = runtime.metadata.balances().hasCall("transfer_all")
|
||||
|
||||
if (transferAllPresent) {
|
||||
val destType = runtime.metadata.balances().call("transfer_all").argumentType("dest")
|
||||
|
||||
call(
|
||||
moduleName = Modules.BALANCES,
|
||||
callName = "transfer_all",
|
||||
arguments = mapOf(
|
||||
"dest" to destType.constructAccountLookupInstance(accountId),
|
||||
"keep_alive" to false
|
||||
)
|
||||
)
|
||||
} else {
|
||||
transferAllowDeath(accountId, amount)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.AssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers
|
||||
|
||||
interface AssetSource {
|
||||
|
||||
val transfers: AssetTransfers
|
||||
|
||||
val balance: AssetBalance
|
||||
|
||||
val history: AssetHistory
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
interface AssetSourceRegistry {
|
||||
|
||||
fun sourceFor(chainAsset: Chain.Asset): AssetSource
|
||||
|
||||
fun allSources(): List<AssetSource>
|
||||
|
||||
suspend fun getEventDetector(chainAsset: Chain.Asset): AssetEventDetector
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
suspend fun AssetSourceRegistry.existentialDeposit(chainAsset: Chain.Asset): BigDecimal {
|
||||
return chainAsset.amountFromPlanks(existentialDepositInPlanks(chainAsset))
|
||||
}
|
||||
|
||||
suspend fun AssetSourceRegistry.existentialDepositInPlanks(chainAsset: Chain.Asset): BigInteger {
|
||||
return sourceFor(chainAsset).balance.existentialDeposit(chainAsset)
|
||||
}
|
||||
|
||||
suspend fun AssetSourceRegistry.totalCanBeDroppedBelowMinimumBalance(chainAsset: Chain.Asset): Boolean {
|
||||
return sourceFor(chainAsset).transfers.totalCanDropBelowMinimumBalance(chainAsset)
|
||||
}
|
||||
|
||||
fun AssetSourceRegistry.isSelfSufficientAsset(chainAsset: Chain.Asset): Boolean {
|
||||
return sourceFor(chainAsset).balance.isSelfSufficient(chainAsset)
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.ChainAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdatePoint
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.balance.ValidatingBalance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import java.math.BigInteger
|
||||
|
||||
sealed class BalanceSyncUpdate {
|
||||
|
||||
class CauseFetchable(val blockHash: BlockHash) : BalanceSyncUpdate()
|
||||
|
||||
class CauseFetched(val cause: RealtimeHistoryUpdate) : BalanceSyncUpdate()
|
||||
|
||||
object NoCause : BalanceSyncUpdate()
|
||||
}
|
||||
|
||||
interface AssetBalance {
|
||||
|
||||
suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*>
|
||||
|
||||
suspend fun startSyncingBalanceHolds(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> = emptyFlow<Nothing>()
|
||||
|
||||
fun isSelfSufficient(chainAsset: Chain.Asset): Boolean
|
||||
|
||||
suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger
|
||||
|
||||
suspend fun queryAccountBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId
|
||||
): ChainAssetBalance
|
||||
|
||||
suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint>
|
||||
|
||||
/**
|
||||
* @return emits hash of the blocks where changes occurred. If no change were detected based on the upstream event - should emit null
|
||||
*/
|
||||
suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate>
|
||||
}
|
||||
|
||||
suspend fun AssetBalance.queryAccountBalanceCatching(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId
|
||||
): Result<ChainAssetBalance> {
|
||||
return runCatching { queryAccountBalance(chain, chainAsset, accountId) }
|
||||
}
|
||||
|
||||
suspend fun AssetBalance.accountBalanceForValidation(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId
|
||||
): ValidatingBalance {
|
||||
val assetBalance = queryAccountBalance(chain, chainAsset, accountId)
|
||||
val ed = existentialDeposit(chainAsset)
|
||||
return ValidatingBalance(assetBalance, ed)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountData
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.edCountingMode
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.transferableMode
|
||||
import io.novafoundation.nova.common.domain.balance.EDCountingMode
|
||||
import io.novafoundation.nova.common.domain.balance.TransferableMode
|
||||
import io.novafoundation.nova.common.domain.balance.calculateBalanceCountedTowardsEd
|
||||
import io.novafoundation.nova.common.domain.balance.calculateReservable
|
||||
import io.novafoundation.nova.common.domain.balance.calculateTransferable
|
||||
import io.novafoundation.nova.common.domain.balance.reservedPreventsDusting
|
||||
import io.novafoundation.nova.common.domain.balance.totalBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
data class ChainAssetBalance(
|
||||
val chainAsset: Chain.Asset,
|
||||
val free: Balance,
|
||||
val reserved: Balance,
|
||||
val frozen: Balance,
|
||||
val transferableMode: TransferableMode,
|
||||
val edCountingMode: EDCountingMode
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun default(chainAsset: Chain.Asset, free: Balance, reserved: Balance, frozen: Balance): ChainAssetBalance {
|
||||
return ChainAssetBalance(chainAsset, free, reserved, frozen, TransferableMode.REGULAR, EDCountingMode.TOTAL)
|
||||
}
|
||||
|
||||
fun default(chainAsset: Chain.Asset, accountBalance: AccountBalance): ChainAssetBalance {
|
||||
return default(chainAsset, free = accountBalance.free, reserved = accountBalance.reserved, frozen = accountBalance.free)
|
||||
}
|
||||
|
||||
fun fromFree(chainAsset: Chain.Asset, free: Balance): ChainAssetBalance {
|
||||
return default(chainAsset, free = free, reserved = BigInteger.ZERO, frozen = BigInteger.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to view current balance from the legacy perspective
|
||||
* Useful for pallets that still use old Currencies implementation instead of Fungibles
|
||||
*/
|
||||
fun legacyAdapter(): ChainAssetBalance {
|
||||
return copy(transferableMode = TransferableMode.REGULAR, edCountingMode = EDCountingMode.TOTAL)
|
||||
}
|
||||
|
||||
val total = totalBalance(free, reserved)
|
||||
|
||||
val transferable = transferableMode.calculateTransferable(free, frozen, reserved)
|
||||
|
||||
val countedTowardsEd = edCountingMode.calculateBalanceCountedTowardsEd(free, reserved)
|
||||
|
||||
fun reservable(existentialDeposit: Balance): Balance {
|
||||
return transferableMode.calculateReservable(free = free, frozen = frozen, ed = existentialDeposit)
|
||||
}
|
||||
|
||||
fun shouldBeDusted(existentialDeposit: Balance): Boolean {
|
||||
// https://github.com/paritytech/polkadot-sdk/blob/e5ac83cd28610bd10a85638d90a8ee082ef2d908/substrate/frame/balances/src/lib.rs#L1096
|
||||
return free < existentialDeposit && !edCountingMode.reservedPreventsDusting(reserved)
|
||||
}
|
||||
}
|
||||
|
||||
fun ChainAssetBalance.ensureMeetsEdOrDust(existentialDeposit: Balance): ChainAssetBalance {
|
||||
return if (shouldBeDusted(existentialDeposit)) {
|
||||
copy(free = BigInteger.ZERO, reserved = BigInteger.ZERO, frozen = BigInteger.ZERO)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fun ChainAssetBalance.transferableAmount(): BigDecimal = chainAsset.amountFromPlanks(transferable)
|
||||
|
||||
fun ChainAssetBalance.countedTowardsEdAmount(): BigDecimal = chainAsset.amountFromPlanks(countedTowardsEd)
|
||||
|
||||
fun AccountData.toChainAssetBalance(chainAsset: Chain.Asset): ChainAssetBalance {
|
||||
return ChainAssetBalance(
|
||||
chainAsset = chainAsset,
|
||||
free = free,
|
||||
reserved = reserved,
|
||||
frozen = frozen,
|
||||
transferableMode = flags.transferableMode(),
|
||||
edCountingMode = flags.edCountingMode()
|
||||
)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import java.math.BigInteger
|
||||
|
||||
class StatemineAssetDetails(
|
||||
val status: Status,
|
||||
val isSufficient: Boolean,
|
||||
val minimumBalance: BigInteger,
|
||||
val issuer: AccountIdKey
|
||||
) {
|
||||
|
||||
enum class Status {
|
||||
Live, Frozen, Destroying
|
||||
}
|
||||
}
|
||||
|
||||
val StatemineAssetDetails.Status.transfersFrozen: Boolean
|
||||
get() = this != StatemineAssetDetails.Status.Live
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash
|
||||
|
||||
data class TransferableBalanceUpdatePoint(
|
||||
val updatedAt: BlockHash
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
|
||||
interface AssetEventDetector {
|
||||
|
||||
fun detectDeposit(event: GenericEvent.Instance): DepositEvent?
|
||||
}
|
||||
|
||||
fun AssetEventDetector.tryDetectDeposit(event: GenericEvent.Instance): DepositEvent? {
|
||||
return runCatching { detectDeposit(event) }
|
||||
.onFailure { Log.w("AssetEventDetector", "Failed to parse event $event", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
class DepositEvent(
|
||||
val destination: AccountIdKey,
|
||||
val amount: Balance,
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
class TransferEvent(
|
||||
val from: AccountId,
|
||||
val to: AccountId,
|
||||
val amount: Balance
|
||||
)
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history
|
||||
|
||||
import io.novafoundation.nova.common.data.model.DataPage
|
||||
import io.novafoundation.nova.common.data.model.PageOffset
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
interface AssetHistory {
|
||||
|
||||
suspend fun fetchOperationsForBalanceChange(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
blockHash: String,
|
||||
accountId: AccountId,
|
||||
): List<RealtimeHistoryUpdate>
|
||||
|
||||
fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter>
|
||||
|
||||
suspend fun additionalFirstPageSync(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
page: Result<DataPage<Operation>>
|
||||
)
|
||||
|
||||
suspend fun getOperations(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency,
|
||||
): DataPage<Operation>
|
||||
|
||||
suspend fun getSyncedPageOffset(
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): PageOffset
|
||||
|
||||
/**
|
||||
* Checks if operation is not a phishing one
|
||||
*/
|
||||
fun isOperationSafe(operation: Operation): Boolean
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
class RealtimeHistoryUpdate(
|
||||
val txHash: String,
|
||||
val status: Operation.Status,
|
||||
val type: Type,
|
||||
) {
|
||||
|
||||
sealed class Type {
|
||||
|
||||
abstract fun relates(accountId: AccountId): Boolean
|
||||
|
||||
class Transfer(
|
||||
val senderId: AccountId,
|
||||
val recipientId: AccountId,
|
||||
val amountInPlanks: Balance,
|
||||
val chainAsset: Chain.Asset,
|
||||
) : Type() {
|
||||
|
||||
override fun relates(accountId: AccountId): Boolean {
|
||||
return senderId contentEquals accountId || recipientId contentEquals accountId
|
||||
}
|
||||
}
|
||||
|
||||
class Swap(
|
||||
val amountIn: ChainAssetWithAmount,
|
||||
val amountOut: ChainAssetWithAmount,
|
||||
val amountFee: ChainAssetWithAmount,
|
||||
val senderId: AccountId,
|
||||
val receiverId: AccountId
|
||||
) : Type() {
|
||||
|
||||
override fun relates(accountId: AccountId): Boolean {
|
||||
return senderId contentEquals accountId || receiverId contentEquals accountId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Extractor
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Factory
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
interface SubstrateRealtimeOperationFetcher {
|
||||
|
||||
suspend fun extractRealtimeHistoryUpdates(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
blockHash: String,
|
||||
): List<RealtimeHistoryUpdate>
|
||||
|
||||
interface Extractor {
|
||||
|
||||
suspend fun extractRealtimeHistoryUpdates(
|
||||
extrinsicVisit: ExtrinsicVisit,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): RealtimeHistoryUpdate.Type?
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
|
||||
sealed class Source {
|
||||
|
||||
class FromExtractor(val extractor: Extractor) : Source()
|
||||
|
||||
class Known(val id: Id) : Source() {
|
||||
|
||||
enum class Id {
|
||||
ASSET_CONVERSION_SWAP, HYDRA_DX_SWAP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun create(sources: List<Source>): SubstrateRealtimeOperationFetcher
|
||||
}
|
||||
}
|
||||
|
||||
fun Extractor.asSource(): Factory.Source {
|
||||
return Factory.Source.FromExtractor(this)
|
||||
}
|
||||
|
||||
fun Factory.Source.Known.Id.asSource(): Factory.Source {
|
||||
return Factory.Source.Known(this)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun buildAssetTransfer(
|
||||
metaAccount: MetaAccount,
|
||||
feePaymentCurrency: FeePaymentCurrency,
|
||||
origin: ChainWithAsset,
|
||||
destination: ChainWithAsset,
|
||||
amount: BigDecimal,
|
||||
transferringMaxAmount: Boolean,
|
||||
address: String,
|
||||
): AssetTransfer {
|
||||
return BaseAssetTransfer(
|
||||
sender = metaAccount,
|
||||
recipient = address,
|
||||
originChain = origin.chain,
|
||||
originChainAsset = origin.asset,
|
||||
destinationChain = destination.chain,
|
||||
destinationChainAsset = destination.asset,
|
||||
amount = amount,
|
||||
transferringMaxAmount = transferringMaxAmount,
|
||||
feePaymentCurrency = feePaymentCurrency
|
||||
)
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers
|
||||
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
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_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.OriginFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.intoFeeList
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeChangeDetectedFailure
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.InsufficientBalanceToStayAboveEDError
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.NotEnoughToPayFeesError
|
||||
import io.novafoundation.nova.runtime.ext.commissionAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
typealias AssetTransfersValidationSystem = ValidationSystem<AssetTransferPayload, AssetTransferValidationFailure>
|
||||
typealias AssetTransfersValidation = Validation<AssetTransferPayload, AssetTransferValidationFailure>
|
||||
typealias AssetTransfersValidationSystemBuilder = ValidationSystemBuilder<AssetTransferPayload, AssetTransferValidationFailure>
|
||||
|
||||
sealed class AssetTransferValidationFailure {
|
||||
|
||||
sealed class WillRemoveAccount : AssetTransferValidationFailure() {
|
||||
object WillBurnDust : WillRemoveAccount()
|
||||
|
||||
class WillTransferDust(val dust: BigDecimal) : WillRemoveAccount()
|
||||
}
|
||||
|
||||
sealed class DeadRecipient : AssetTransferValidationFailure() {
|
||||
|
||||
object InUsedAsset : DeadRecipient()
|
||||
|
||||
class InCommissionAsset(val commissionAsset: Chain.Asset) : DeadRecipient()
|
||||
}
|
||||
|
||||
sealed class NotEnoughFunds : AssetTransferValidationFailure() {
|
||||
object InUsedAsset : NotEnoughFunds()
|
||||
|
||||
class InCommissionAsset(
|
||||
override val chainAsset: Chain.Asset,
|
||||
override val maxUsable: BigDecimal,
|
||||
override val fee: BigDecimal
|
||||
) : NotEnoughFunds(), NotEnoughToPayFeesError
|
||||
|
||||
class ToStayAboveED(override val asset: Chain.Asset, override val errorModel: InsufficientBalanceToStayAboveEDError.ErrorModel) :
|
||||
NotEnoughFunds(),
|
||||
InsufficientBalanceToStayAboveEDError
|
||||
|
||||
class ToPayCrossChainFee(
|
||||
val usedAsset: Chain.Asset,
|
||||
val fee: BigDecimal,
|
||||
val remainingBalanceAfterTransfer: BigDecimal,
|
||||
) : NotEnoughFunds()
|
||||
|
||||
class ToStayAboveEdBeforePayingDeliveryFees(
|
||||
val maxPossibleTransferAmount: Balance,
|
||||
val chainAsset: Chain.Asset,
|
||||
) : NotEnoughFunds()
|
||||
}
|
||||
|
||||
class InvalidRecipientAddress(val chain: Chain) : AssetTransferValidationFailure()
|
||||
|
||||
class PhishingRecipient(val address: String) : AssetTransferValidationFailure()
|
||||
|
||||
object NonPositiveAmount : AssetTransferValidationFailure()
|
||||
|
||||
object RecipientCannotAcceptTransfer : AssetTransferValidationFailure()
|
||||
|
||||
class FeeChangeDetected(
|
||||
override val payload: FeeChangeDetectedFailure.Payload<SubmissionFee>
|
||||
) : AssetTransferValidationFailure(), FeeChangeDetectedFailure<SubmissionFee>
|
||||
|
||||
object RecipientIsSystemAccount : AssetTransferValidationFailure()
|
||||
|
||||
object DryRunFailed : AssetTransferValidationFailure()
|
||||
}
|
||||
|
||||
data class AssetTransferPayload(
|
||||
val transfer: WeightedAssetTransfer,
|
||||
val originFee: OriginFee,
|
||||
val crossChainFee: FeeBase?,
|
||||
val originCommissionAsset: Asset,
|
||||
val originUsedAsset: Asset
|
||||
)
|
||||
|
||||
val AssetTransferPayload.commissionChainAsset: Chain.Asset
|
||||
get() = originCommissionAsset.token.configuration
|
||||
|
||||
val AssetTransferPayload.originFeeList: List<Fee>
|
||||
get() = originFee.intoFeeList()
|
||||
|
||||
val AssetTransferPayload.originFeeListInUsedAsset: List<Fee>
|
||||
get() = if (isSendingCommissionAsset) {
|
||||
originFeeList
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val AssetTransferPayload.isSendingCommissionAsset
|
||||
get() = transfer.originChainAsset == commissionChainAsset
|
||||
|
||||
val AssetTransferPayload.isReceivingCommissionAsset
|
||||
get() = transfer.destinationChainAsset == transfer.destinationChain.commissionAsset
|
||||
|
||||
val AssetTransferPayload.receivingAmountInCommissionAsset: BigInteger
|
||||
get() = if (isReceivingCommissionAsset) {
|
||||
transfer.amountInPlanks
|
||||
} else {
|
||||
BigInteger.ZERO
|
||||
}
|
||||
|
||||
val AssetTransferPayload.sendingAmountInCommissionAsset: BigDecimal
|
||||
get() = if (isSendingCommissionAsset) {
|
||||
transfer.amount
|
||||
} else {
|
||||
0.toBigDecimal()
|
||||
}
|
||||
|
||||
val AssetTransfer.amountInPlanks
|
||||
get() = originChainAsset.planksFromAmount(amount)
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdKeyIn
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
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_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrNull
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface AssetTransferDirection {
|
||||
|
||||
val originChain: Chain
|
||||
|
||||
val originChainAsset: Chain.Asset
|
||||
|
||||
val destinationChain: Chain
|
||||
|
||||
val destinationChainAsset: Chain.Asset
|
||||
}
|
||||
|
||||
interface AssetTransferBase : AssetTransferDirection {
|
||||
|
||||
val recipientAccountId: AccountIdKey
|
||||
get() = destinationChain.accountIdOrDefault(recipient).intoKey()
|
||||
|
||||
val recipient: String
|
||||
|
||||
val feePaymentCurrency: FeePaymentCurrency
|
||||
|
||||
val amountPlanks: Balance
|
||||
}
|
||||
|
||||
fun AssetTransferBase.amount(): BigDecimal {
|
||||
return originChainAsset.amountFromPlanks(amountPlanks)
|
||||
}
|
||||
|
||||
fun AssetTransferBase.replaceAmount(newAmount: Balance): AssetTransferBase {
|
||||
return AssetTransferBase(recipient, originChain, originChainAsset, destinationChain, destinationChainAsset, feePaymentCurrency, newAmount)
|
||||
}
|
||||
|
||||
// TODO this is too specialized for this module
|
||||
interface AssetTransfer : AssetTransferBase {
|
||||
|
||||
val sender: MetaAccount
|
||||
|
||||
val amount: BigDecimal
|
||||
|
||||
val transferringMaxAmount: Boolean
|
||||
|
||||
override val amountPlanks: Balance
|
||||
get() = originChainAsset.planksFromAmount(amount)
|
||||
}
|
||||
|
||||
fun AssetTransferDirection(
|
||||
originChain: Chain,
|
||||
originChainAsset: Chain.Asset,
|
||||
destinationChain: Chain,
|
||||
destinationChainAsset: Chain.Asset
|
||||
): AssetTransferDirection {
|
||||
return object : AssetTransferDirection {
|
||||
override val originChain: Chain = originChain
|
||||
override val originChainAsset: Chain.Asset = originChainAsset
|
||||
override val destinationChain: Chain = destinationChain
|
||||
override val destinationChainAsset: Chain.Asset = destinationChainAsset
|
||||
}
|
||||
}
|
||||
|
||||
fun AssetTransferBase(
|
||||
recipient: String,
|
||||
originChain: Chain,
|
||||
originChainAsset: Chain.Asset,
|
||||
destinationChain: Chain,
|
||||
destinationChainAsset: Chain.Asset,
|
||||
feePaymentCurrency: FeePaymentCurrency,
|
||||
amountPlanks: Balance
|
||||
): AssetTransferBase {
|
||||
return object : AssetTransferBase {
|
||||
override val recipient: String = recipient
|
||||
override val originChain: Chain = originChain
|
||||
override val originChainAsset: Chain.Asset = originChainAsset
|
||||
override val destinationChain: Chain = destinationChain
|
||||
override val destinationChainAsset: Chain.Asset = destinationChainAsset
|
||||
override val feePaymentCurrency: FeePaymentCurrency = feePaymentCurrency
|
||||
override val amountPlanks: Balance = amountPlanks
|
||||
}
|
||||
}
|
||||
|
||||
data class BaseAssetTransfer(
|
||||
override val sender: MetaAccount,
|
||||
override val recipient: String,
|
||||
override val originChain: Chain,
|
||||
override val originChainAsset: Chain.Asset,
|
||||
override val destinationChain: Chain,
|
||||
override val destinationChainAsset: Chain.Asset,
|
||||
override val feePaymentCurrency: FeePaymentCurrency,
|
||||
override val amount: BigDecimal,
|
||||
override val transferringMaxAmount: Boolean
|
||||
) : AssetTransfer
|
||||
|
||||
data class WeightedAssetTransfer(
|
||||
override val sender: MetaAccount,
|
||||
override val recipient: String,
|
||||
override val originChain: Chain,
|
||||
override val originChainAsset: Chain.Asset,
|
||||
override val destinationChain: Chain,
|
||||
override val destinationChainAsset: Chain.Asset,
|
||||
override val feePaymentCurrency: FeePaymentCurrency,
|
||||
override val amount: BigDecimal,
|
||||
override val transferringMaxAmount: Boolean,
|
||||
val fee: OriginFee,
|
||||
) : AssetTransfer {
|
||||
|
||||
constructor(assetTransfer: AssetTransfer, fee: OriginFee) : this(
|
||||
sender = assetTransfer.sender,
|
||||
recipient = assetTransfer.recipient,
|
||||
originChain = assetTransfer.originChain,
|
||||
originChainAsset = assetTransfer.originChainAsset,
|
||||
destinationChain = assetTransfer.destinationChain,
|
||||
destinationChainAsset = assetTransfer.destinationChainAsset,
|
||||
feePaymentCurrency = assetTransfer.feePaymentCurrency,
|
||||
amount = assetTransfer.amount,
|
||||
transferringMaxAmount = assetTransfer.transferringMaxAmount,
|
||||
fee = fee
|
||||
)
|
||||
}
|
||||
|
||||
val AssetTransfer.isCrossChain
|
||||
get() = originChain.id != destinationChain.id
|
||||
|
||||
fun AssetTransfer.recipientOrNull(): AccountId? {
|
||||
return destinationChain.accountIdOrNull(recipient)
|
||||
}
|
||||
|
||||
val AssetTransfer.senderAccountId: AccountIdKey
|
||||
get() = sender.requireAccountIdKeyIn(originChain)
|
||||
|
||||
interface AssetTransfers {
|
||||
|
||||
fun getValidationSystem(coroutineScope: CoroutineScope): AssetTransfersValidationSystem
|
||||
|
||||
suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee
|
||||
|
||||
suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission>
|
||||
|
||||
suspend fun performTransferAndAwaitExecution(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<TransactionExecution>
|
||||
|
||||
suspend fun totalCanDropBelowMinimumBalance(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean
|
||||
|
||||
suspend fun recipientCanAcceptTransfer(chainAsset: Chain.Asset, recipient: AccountId): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the transfer from the given call
|
||||
* This function might throw - do not use it directly. For fail-safe version use [tryParseTransfer]
|
||||
*/
|
||||
@Internal
|
||||
suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall?
|
||||
}
|
||||
|
||||
suspend fun AssetTransfers.tryParseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
return runCatching { parseTransfer(call, chain) }
|
||||
.onFailure { Log.e(LOG_TAG, "Failed to parse call: $call", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
fun AssetTransfer.asWeighted(fee: OriginFee) = WeightedAssetTransfer(this, fee)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EthereumTransactionExecution
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.ExtrinsicExecutionResult
|
||||
|
||||
sealed interface TransactionExecution {
|
||||
|
||||
class Ethereum(val ethereumTransactionExecution: EthereumTransactionExecution) : TransactionExecution
|
||||
|
||||
class Substrate(val extrinsicExecutionResult: ExtrinsicExecutionResult) : TransactionExecution
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
|
||||
class TransferParsedFromCall(
|
||||
val amount: ChainAssetWithAmount,
|
||||
val destination: AccountIdKey
|
||||
)
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
typealias Balance = BigInteger
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters
|
||||
|
||||
import io.novafoundation.nova.core.updater.Updater
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
interface BalanceLocksUpdaterFactory {
|
||||
|
||||
fun create(chain: Chain): Updater<MetaAccount>
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters
|
||||
|
||||
import io.novafoundation.nova.core.updater.Updater
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
interface PaymentUpdaterFactory {
|
||||
|
||||
fun createFullSync(chain: Chain): Updater<MetaAccount>
|
||||
|
||||
fun createLightSync(chain: Chain): Updater<MetaAccount>
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
import java.math.BigInteger
|
||||
|
||||
data class CrossChainFeeModel(
|
||||
val paidByAccount: Balance = BigInteger.ZERO,
|
||||
val paidFromHolding: Balance = BigInteger.ZERO
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
fun CrossChainFeeModel.paidByAccountOrNull(): Balance? {
|
||||
return paidByAccount.takeIf { paidByAccount.isPositive() }
|
||||
}
|
||||
|
||||
fun CrossChainFeeModel.Companion.zero() = CrossChainFeeModel()
|
||||
|
||||
operator fun CrossChainFeeModel.plus(other: CrossChainFeeModel) = CrossChainFeeModel(
|
||||
paidByAccount = paidByAccount + other.paidByAccount,
|
||||
paidFromHolding = paidFromHolding + other.paidFromHolding
|
||||
)
|
||||
|
||||
fun CrossChainFeeModel?.orZero() = this ?: CrossChainFeeModel.zero()
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.crosschain
|
||||
|
||||
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.Fee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferFeatures
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlin.time.Duration
|
||||
|
||||
interface CrossChainTransactor {
|
||||
|
||||
context(ExtrinsicService)
|
||||
suspend fun estimateOriginFee(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase
|
||||
): Fee
|
||||
|
||||
context(ExtrinsicService)
|
||||
suspend fun performTransfer(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
): Result<ExtrinsicSubmission>
|
||||
|
||||
suspend fun requiredRemainingAmountAfterTransfer(configuration: CrossChainTransferConfiguration): Balance
|
||||
|
||||
/**
|
||||
* @return result of actual received balance on destination
|
||||
*/
|
||||
context(ExtrinsicService)
|
||||
suspend fun performAndTrackTransfer(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
): Result<Balance>
|
||||
|
||||
suspend fun supportsXcmExecute(
|
||||
originChainId: ChainId,
|
||||
features: DynamicCrossChainTransferFeatures
|
||||
): Boolean
|
||||
|
||||
suspend fun estimateMaximumExecutionTime(configuration: CrossChainTransferConfiguration): Duration
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransfersConfiguration
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface CrossChainTransfersRepository {
|
||||
|
||||
suspend fun syncConfiguration()
|
||||
|
||||
fun configurationFlow(): Flow<CrossChainTransfersConfiguration>
|
||||
|
||||
suspend fun getConfiguration(): CrossChainTransfersConfiguration
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem
|
||||
|
||||
interface CrossChainValidationSystemProvider {
|
||||
|
||||
fun createValidationSystem(): AssetTransfersValidationSystem
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransferConfiguration
|
||||
|
||||
interface CrossChainWeigher {
|
||||
|
||||
suspend fun estimateFee(transfer: AssetTransferBase, config: CrossChainTransferConfiguration): CrossChainFeeModel
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
sealed class XcmTransferDryRunOrigin {
|
||||
|
||||
/**
|
||||
* Use fake signed origin that will be topped up to perform the dry run
|
||||
* Useful for dry running as the part of fee calculation process
|
||||
*/
|
||||
data object Fake : XcmTransferDryRunOrigin()
|
||||
|
||||
/**
|
||||
* Use [accountId] as a origin for simulation. Simulation will be done on the current state of the account,
|
||||
* without preliminary top ups e.t.c.
|
||||
* [crossChainFee] will be added to the transfer amount
|
||||
* Useful for final dry run, when all transfer parameters are known and finalized
|
||||
*/
|
||||
class Signed(val accountId: AccountIdKey, val crossChainFee: Balance) : XcmTransferDryRunOrigin()
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.priceApi
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
class CoinRangeResponse(val prices: List<List<BigDecimal>>) {
|
||||
|
||||
class Price(val millis: Long, val price: BigDecimal)
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.priceApi
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface CoingeckoApi {
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://api.coingecko.com"
|
||||
|
||||
fun getRecentRateFieldName(priceId: String): String {
|
||||
return priceId + "_24h_change"
|
||||
}
|
||||
}
|
||||
|
||||
@GET("/api/v3/simple/price")
|
||||
suspend fun getAssetPrice(
|
||||
@Query("ids") priceIds: String,
|
||||
@Query("vs_currencies") currency: String,
|
||||
@Query("include_24hr_change") includeRateChange: Boolean
|
||||
): Map<String, Map<String, Double?>>
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.network.priceApi
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface ProxyPriceApi {
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://tokens-price.novasama-tech.org"
|
||||
}
|
||||
|
||||
@GET("/api/v3/coins/{id}/market_chart")
|
||||
suspend fun getLastCoinRange(
|
||||
@Path("id") id: String,
|
||||
@Query("vs_currency") currency: String,
|
||||
@Query("days") days: String
|
||||
): CoinRangeResponse
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
interface AccountInfoRepository {
|
||||
|
||||
suspend fun getAccountInfo(
|
||||
chainId: ChainId,
|
||||
accountId: AccountId
|
||||
): AccountInfo
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface BalanceHoldsRepository {
|
||||
|
||||
suspend fun chainHasHoldId(chainId: ChainId, holdId: BalanceHold.HoldId): Boolean
|
||||
|
||||
suspend fun observeBalanceHolds(metaInt: Long, chainAsset: Chain.Asset): Flow<List<BalanceHold>>
|
||||
|
||||
fun observeHoldsForMetaAccount(metaInt: Long): Flow<List<BalanceHold>>
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLockId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface BalanceLocksRepository {
|
||||
|
||||
fun observeBalanceLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow<List<BalanceLock>>
|
||||
|
||||
suspend fun getBalanceLocks(metaId: Long, chainAsset: Chain.Asset): List<BalanceLock>
|
||||
|
||||
suspend fun getBiggestLock(chain: Chain, chainAsset: Chain.Asset): BalanceLock?
|
||||
|
||||
suspend fun observeBalanceLock(chainAsset: Chain.Asset, lockId: BalanceLockId): Flow<BalanceLock?>
|
||||
|
||||
fun observeLocksForMetaAccount(metaAccount: MetaAccount): Flow<List<BalanceLock>>
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
import retrofit2.HttpException
|
||||
import kotlin.jvm.Throws
|
||||
import kotlin.time.Duration
|
||||
|
||||
interface CoinPriceRepository {
|
||||
|
||||
@Throws(HttpException::class)
|
||||
suspend fun getCoinPriceAtTime(priceId: String, currency: Currency, timestamp: Duration): HistoricalCoinRate?
|
||||
|
||||
@Throws(HttpException::class)
|
||||
suspend fun getLastHistoryForPeriod(priceId: String, currency: Currency, range: PricePeriod): List<HistoricalCoinRate>
|
||||
}
|
||||
|
||||
@Throws(HttpException::class)
|
||||
suspend fun CoinPriceRepository.getAllCoinPriceHistory(priceId: String, currency: Currency): List<HistoricalCoinRate> {
|
||||
return getLastHistoryForPeriod(priceId, currency, PricePeriod.MAX)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ExternalBalance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ExternalBalanceRepository {
|
||||
|
||||
fun observeAccountExternalBalances(metaId: Long): Flow<List<ExternalBalance>>
|
||||
|
||||
fun observeAccountChainExternalBalances(metaId: Long, assetId: FullChainAssetId): Flow<List<ExternalBalance>>
|
||||
|
||||
suspend fun deleteExternalBalances(assetIds: List<FullChainAssetId>)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.XcmChain
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction.ParachainId.Companion.JUNCTION_TYPE_PARACHAIN
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction.ParachainId.Companion.JUNCTION_TYPE_TEYRCHAIN
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.chainLocation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
|
||||
// Pezkuwi chain IDs - these chains use "Teyrchain" instead of "Parachain" in XCM
|
||||
private val PEZKUWI_CHAIN_IDS = setOf(
|
||||
"bb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75", // PEZKUWI
|
||||
"00d0e1d0581c3cd5c5768652d52f4520184018b44f56a2ae1e0dc9d65c00c948", // PEZKUWI_ASSET_HUB
|
||||
"58269e9c184f721e0309332d90cafc410df1519a5dc27a5fd9b3bf5fd2d129f8" // PEZKUWI_PEOPLE
|
||||
)
|
||||
|
||||
private fun junctionTypeNameForChain(chainId: ChainId): String {
|
||||
return if (chainId in PEZKUWI_CHAIN_IDS) JUNCTION_TYPE_TEYRCHAIN else JUNCTION_TYPE_PARACHAIN
|
||||
}
|
||||
|
||||
suspend fun ParachainInfoRepository.getXcmChain(chain: Chain): XcmChain {
|
||||
return XcmChain(paraId(chain.id), chain)
|
||||
}
|
||||
|
||||
suspend fun ParachainInfoRepository.getChainLocation(chainId: ChainId): ChainLocation {
|
||||
val junctionTypeName = junctionTypeNameForChain(chainId)
|
||||
val location = AbsoluteMultiLocation.chainLocation(paraId(chainId), junctionTypeName)
|
||||
return ChainLocation(chainId, location)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
enum class PricePeriod {
|
||||
DAY, WEEK, MONTH, YEAR, MAX
|
||||
}
|
||||
|
||||
fun PricePeriod.duration(): Duration {
|
||||
return when (this) {
|
||||
PricePeriod.DAY -> 1.days
|
||||
PricePeriod.WEEK -> 7.days
|
||||
PricePeriod.MONTH -> 30.days
|
||||
PricePeriod.YEAR -> 365.days
|
||||
PricePeriod.MAX -> Duration.INFINITE
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.repository
|
||||
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.StatemineAssetDetails
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface StatemineAssetsRepository {
|
||||
|
||||
suspend fun getAssetDetails(
|
||||
chainId: ChainId,
|
||||
assetType: Chain.Asset.Type.Statemine,
|
||||
): StatemineAssetDetails
|
||||
|
||||
suspend fun subscribeAndSyncAssetDetails(
|
||||
chainId: ChainId,
|
||||
assetType: Chain.Asset.Type.Statemine,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<StatemineAssetDetails>
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.source
|
||||
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
|
||||
interface CoinPriceLocalDataSource {
|
||||
|
||||
suspend fun getFloorCoinPriceAtTime(priceId: String, currency: Currency, timestamp: Long): HistoricalCoinRate?
|
||||
|
||||
suspend fun hasCeilingCoinPriceAtTime(priceId: String, currency: Currency, timestamp: Long): Boolean
|
||||
|
||||
suspend fun getCoinPriceRange(priceId: String, currency: Currency, fromTimestamp: Long, toTimestamp: Long): List<HistoricalCoinRate>
|
||||
|
||||
suspend fun updateCoinPrice(priceId: String, currency: Currency, coinRate: List<HistoricalCoinRate>)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.data.source
|
||||
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.PricePeriod
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRateChange
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
|
||||
interface CoinPriceRemoteDataSource {
|
||||
|
||||
suspend fun getLastCoinPriceRange(priceId: String, currency: Currency, range: PricePeriod): List<HistoricalCoinRate>
|
||||
|
||||
suspend fun getCoinRates(priceIds: Set<String>, currency: Currency): Map<String, CoinRateChange?>
|
||||
|
||||
suspend fun getCoinRate(priceId: String, currency: Currency): CoinRateChange?
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class BalanceLocks
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.di
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
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.AccountInfoRepository
|
||||
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.data.repository.StatemineAssetsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryTokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.AssetGetOptionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
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.TokenRepository
|
||||
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.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.MultisigExtrinsicValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.ProxyHaveEnoughFeeValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.context.AssetsValidationContext
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.EnoughAmountValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.MinAmountFieldValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.AssetModelFormatter
|
||||
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.amount.TokenFormatter
|
||||
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.getAsset.GetAssetOptionsMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.maxAction.MaxActionProviderFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
|
||||
|
||||
interface WalletFeatureApi {
|
||||
|
||||
val assetSourceRegistry: AssetSourceRegistry
|
||||
|
||||
val phishingValidationFactory: PhishingValidationFactory
|
||||
|
||||
val crossChainTransfersRepository: CrossChainTransfersRepository
|
||||
|
||||
val crossChainWeigher: CrossChainWeigher
|
||||
|
||||
val crossChainTransactor: CrossChainTransactor
|
||||
|
||||
val crossChainValidationSystemProvider: CrossChainValidationSystemProvider
|
||||
|
||||
val balanceLocksRepository: BalanceLocksRepository
|
||||
|
||||
val chainAssetRepository: ChainAssetRepository
|
||||
|
||||
val erc20Standard: Erc20Standard
|
||||
|
||||
val arbitraryAssetUseCase: ArbitraryAssetUseCase
|
||||
|
||||
val externalBalancesRepository: ExternalBalanceRepository
|
||||
|
||||
val paymentUpdaterFactory: PaymentUpdaterFactory
|
||||
|
||||
val balanceLocksUpdaterFactory: BalanceLocksUpdaterFactory
|
||||
|
||||
val coinPriceRepository: CoinPriceRepository
|
||||
|
||||
val crossChainTransfersUseCase: CrossChainTransfersUseCase
|
||||
|
||||
val arbitraryTokenUseCase: ArbitraryTokenUseCase
|
||||
|
||||
val holdsRepository: BalanceHoldsRepository
|
||||
|
||||
val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory
|
||||
|
||||
val assetsValidationContextFactory: AssetsValidationContext.Factory
|
||||
|
||||
val statemineAssetsRepository: StatemineAssetsRepository
|
||||
|
||||
val multisigExtrinsicValidationFactory: MultisigExtrinsicValidationFactory
|
||||
|
||||
val accountInfoRepository: AccountInfoRepository
|
||||
|
||||
val amountFormatter: AmountFormatter
|
||||
|
||||
val fiatFormatter: FiatFormatter
|
||||
|
||||
val tokenFormatter: TokenFormatter
|
||||
|
||||
val assetModelFormatter: AssetModelFormatter
|
||||
|
||||
val assetGetOptionsUseCase: AssetGetOptionsUseCase
|
||||
|
||||
val enoughAmountValidatorFactory: EnoughAmountValidatorFactory
|
||||
|
||||
val minAmountFieldValidatorFactory: MinAmountFieldValidatorFactory
|
||||
|
||||
val getAssetOptionsMixinFactory: GetAssetOptionsMixin.Factory
|
||||
|
||||
val sendUseCase: SendUseCase
|
||||
|
||||
fun provideWalletRepository(): WalletRepository
|
||||
|
||||
fun provideTokenRepository(): TokenRepository
|
||||
|
||||
fun provideAssetCache(): AssetCache
|
||||
|
||||
fun provideWallConstants(): WalletConstants
|
||||
|
||||
fun provideFeeLoaderMixinFactory(): FeeLoaderMixin.Factory
|
||||
|
||||
fun provideAmountChooserFactory(): AmountChooserMixin.Factory
|
||||
|
||||
fun proxyPriceApi(): ProxyPriceApi
|
||||
|
||||
fun coingeckoApi(): CoingeckoApi
|
||||
|
||||
fun enoughTotalToStayAboveEDValidationFactory(): EnoughTotalToStayAboveEDValidationFactory
|
||||
|
||||
fun proxyHaveEnoughFeeValidationFactory(): ProxyHaveEnoughFeeValidationFactory
|
||||
|
||||
fun maxActionProviderFactory(): MaxActionProviderFactory
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.di.common
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.implementations.AssetUseCaseImpl
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
|
||||
|
||||
@Module(includes = [TokenUseCaseModule::class])
|
||||
class AssetUseCaseModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetUseCase(
|
||||
walletRepository: WalletRepository,
|
||||
accountRepository: AccountRepository,
|
||||
sharedState: SelectedAssetOptionSharedState<*>,
|
||||
): AssetUseCase = AssetUseCaseImpl(
|
||||
walletRepository,
|
||||
accountRepository,
|
||||
sharedState
|
||||
)
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.di.common
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.AssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SelectableAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.implementations.SelectableAssetUseCaseImpl
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterFactory
|
||||
import io.novafoundation.nova.common.presentation.masking.formatter.MaskableValueFormatterProvider
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.AssetModelFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.assetSelector.AssetSelectorFactory
|
||||
import io.novafoundation.nova.runtime.state.SelectableSingleAssetSharedState
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
|
||||
|
||||
@Module(includes = [SelectableAssetUseCaseModule.BindsModule::class, TokenUseCaseModule::class])
|
||||
class SelectableAssetUseCaseModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetUseCase(
|
||||
walletRepository: WalletRepository,
|
||||
accountRepository: AccountRepository,
|
||||
sharedState: SelectableSingleAssetSharedState<*>,
|
||||
): SelectableAssetUseCase<*> = SelectableAssetUseCaseImpl(
|
||||
walletRepository,
|
||||
accountRepository,
|
||||
sharedState,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAssetSelectorMixinFactory(
|
||||
assetUseCase: SelectableAssetUseCase<*>,
|
||||
singleAssetSharedState: SelectableSingleAssetSharedState<*>,
|
||||
maskableValueFormatterProvider: MaskableValueFormatterProvider,
|
||||
maskableValueFormatterFactory: MaskableValueFormatterFactory,
|
||||
resourceManager: ResourceManager,
|
||||
assetModelFormatter: AssetModelFormatter
|
||||
) = AssetSelectorFactory(
|
||||
assetUseCase,
|
||||
singleAssetSharedState,
|
||||
resourceManager,
|
||||
maskableValueFormatterProvider,
|
||||
maskableValueFormatterFactory,
|
||||
assetModelFormatter
|
||||
)
|
||||
|
||||
@Module
|
||||
interface BindsModule {
|
||||
|
||||
@Binds
|
||||
fun bindAssetUseCase(selectableAssetUseCase: SelectableAssetUseCase<*>): AssetUseCase
|
||||
|
||||
@Binds
|
||||
fun bindSelectedAssetState(selectableSingleAssetSharedState: SelectableSingleAssetSharedState<*>): SelectedAssetOptionSharedState<*>
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.di.common
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.TokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.implementations.SharedStateTokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
|
||||
|
||||
@Module
|
||||
class TokenUseCaseModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTokenUseCase(
|
||||
tokenRepository: TokenRepository,
|
||||
sharedState: SelectedAssetOptionSharedState<*>,
|
||||
chainRegistry: ChainRegistry
|
||||
): TokenUseCase = SharedStateTokenUseCase(
|
||||
tokenRepository = tokenRepository,
|
||||
chainRegistry = chainRegistry,
|
||||
sharedState = sharedState,
|
||||
)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOfAll
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
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.asset
|
||||
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.ChainId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
|
||||
interface ArbitraryAssetUseCase {
|
||||
|
||||
fun assetFlow(chainId: ChainId, assetId: ChainAssetId): Flow<Asset>
|
||||
|
||||
fun assetFlow(chainAsset: Chain.Asset): Flow<Asset>
|
||||
|
||||
suspend fun getAsset(chainAsset: Chain.Asset): Asset?
|
||||
}
|
||||
|
||||
class RealArbitraryAssetUseCase(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val walletRepository: WalletRepository,
|
||||
private val chainRegistry: ChainRegistry
|
||||
) : ArbitraryAssetUseCase {
|
||||
|
||||
override fun assetFlow(chainId: ChainId, assetId: ChainAssetId): Flow<Asset> {
|
||||
return flowOfAll {
|
||||
val chainAsset = chainRegistry.asset(chainId, assetId)
|
||||
|
||||
assetFlow(chainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override fun assetFlow(chainAsset: Chain.Asset): Flow<Asset> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
walletRepository.assetFlow(metaAccount.id, chainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAsset(chainAsset: Chain.Asset): Asset? {
|
||||
val account = accountRepository.getSelectedMetaAccount()
|
||||
return walletRepository.getAsset(account.id, chainAsset)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
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.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalToken
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
|
||||
interface ArbitraryTokenUseCase {
|
||||
|
||||
fun historicalTokenFlow(chainAsset: Chain.Asset, at: Duration): Flow<HistoricalToken>
|
||||
|
||||
suspend fun historicalToken(chainAsset: Chain.Asset, at: Duration): HistoricalToken
|
||||
|
||||
suspend fun getToken(chainAssetId: FullChainAssetId): Token
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
class RealArbitraryTokenUseCase @Inject constructor(
|
||||
private val coinPriceRepository: CoinPriceRepository,
|
||||
private val currencyRepository: CurrencyRepository,
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) : ArbitraryTokenUseCase {
|
||||
|
||||
override fun historicalTokenFlow(chainAsset: Chain.Asset, at: Duration): Flow<HistoricalToken> {
|
||||
return flowOf { historicalToken(chainAsset, at) }
|
||||
}
|
||||
|
||||
override suspend fun historicalToken(chainAsset: Chain.Asset, at: Duration): HistoricalToken = withContext(Dispatchers.IO) {
|
||||
val currency = currencyRepository.getSelectedCurrency()
|
||||
val priceId = chainAsset.priceId
|
||||
|
||||
val rate = if (priceId != null) {
|
||||
runCatching { coinPriceRepository.getCoinPriceAtTime(priceId, currency, at) }
|
||||
.getOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
HistoricalToken(currency, rate, chainAsset)
|
||||
}
|
||||
|
||||
override suspend fun getToken(chainAssetId: FullChainAssetId): Token {
|
||||
return tokenRepository.getToken(chainRegistry.asset(chainAssetId))
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.GetAssetOption
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AssetGetOptionsUseCase {
|
||||
|
||||
fun observeAssetGetOptionsForSelectedAccount(chainAssetFlow: Flow<Chain.Asset?>): Flow<Set<GetAssetOption>>
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.runtime.state.SelectableAssetAdditionalData
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState.SupportedAssetOption
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface GenericAssetUseCase<A> {
|
||||
fun currentAssetAndOptionFlow(): Flow<AssetAndOption<A>>
|
||||
|
||||
fun currentAssetFlow(): Flow<Asset> {
|
||||
return currentAssetAndOptionFlow().map { it.asset }
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectableAssetUseCase<A : SelectableAssetAdditionalData> : GenericAssetUseCase<A> {
|
||||
suspend fun availableAssetsToSelect(): List<AssetAndOption<A>>
|
||||
}
|
||||
|
||||
data class AssetAndOption<out A>(val asset: Asset, val option: SupportedAssetOption<A>)
|
||||
|
||||
typealias AssetUseCase = GenericAssetUseCase<*>
|
||||
|
||||
typealias SelectableAssetAndOption = AssetAndOption<SelectableAssetAdditionalData>
|
||||
|
||||
suspend fun AssetUseCase.getCurrentAsset() = currentAssetFlow().first()
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.TransactionExecution
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface SendUseCase {
|
||||
|
||||
suspend fun performOnChainTransfer(transfer: WeightedAssetTransfer, fee: SubmissionFee, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission>
|
||||
|
||||
suspend fun performOnChainTransferAndAwaitExecution(
|
||||
transfer: WeightedAssetTransfer,
|
||||
fee: SubmissionFee,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<TransactionExecution>
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface TokenUseCase {
|
||||
|
||||
suspend fun currentToken(): Token
|
||||
|
||||
fun currentTokenFlow(): Flow<Token>
|
||||
|
||||
suspend fun getToken(chainAssetId: FullChainAssetId): Token
|
||||
}
|
||||
|
||||
fun TokenUseCase.currentAssetFlow(): Flow<Chain.Asset> {
|
||||
return currentTokenFlow().map { it.configuration }
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.fee
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeInspector
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface FeeInteractor {
|
||||
|
||||
suspend fun canPayFeeInAsset(chainAsset: Chain.Asset): Boolean
|
||||
|
||||
suspend fun assetFlow(asset: Chain.Asset): Flow<Asset?>
|
||||
|
||||
suspend fun hasEnoughBalanceToPayFee(feeAsset: Asset, inspectedFeeAmount: FeeInspector.InspectedFeeAmount): Boolean
|
||||
|
||||
suspend fun getToken(chainAsset: Chain.Asset): Token
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.implementations
|
||||
|
||||
import io.novafoundation.nova.common.utils.combineToPair
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.AssetAndOption
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.GenericAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AssetUseCaseImpl<A>(
|
||||
private val walletRepository: WalletRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val sharedState: SelectedAssetOptionSharedState<A>,
|
||||
) : GenericAssetUseCase<A> {
|
||||
|
||||
override fun currentAssetAndOptionFlow(): Flow<AssetAndOption<A>> = combineToPair(
|
||||
accountRepository.selectedMetaAccountFlow(),
|
||||
sharedState.selectedOption,
|
||||
).flatMapLatest { (selectedMetaAccount, selectedOption) ->
|
||||
val (_, chainAsset) = selectedOption.assetWithChain
|
||||
|
||||
walletRepository.assetFlow(
|
||||
metaId = selectedMetaAccount.id,
|
||||
chainAsset = chainAsset
|
||||
).map {
|
||||
AssetAndOption(it, selectedOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.implementations
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.AssetAndOption
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.GenericAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SelectableAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.runtime.ext.alphabeticalOrder
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.mainChainsFirstAscendingOrder
|
||||
import io.novafoundation.nova.runtime.ext.testnetsLastAscendingOrder
|
||||
import io.novafoundation.nova.runtime.state.SelectableAssetAdditionalData
|
||||
import io.novafoundation.nova.runtime.state.SelectableSingleAssetSharedState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SelectableAssetUseCaseImpl<A : SelectableAssetAdditionalData>(
|
||||
private val walletRepository: WalletRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val sharedState: SelectableSingleAssetSharedState<A>,
|
||||
) : GenericAssetUseCase<A> by AssetUseCaseImpl(walletRepository, accountRepository, sharedState),
|
||||
SelectableAssetUseCase<A> {
|
||||
|
||||
override suspend fun availableAssetsToSelect(): List<AssetAndOption<A>> = withContext(Dispatchers.Default) {
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
|
||||
val balancesByChainAssets = walletRepository.getSupportedAssets(metaAccount.id).associateBy { it.token.configuration.fullId }
|
||||
|
||||
sharedState.availableToSelect()
|
||||
.mapNotNull { supportedOption ->
|
||||
val asset = balancesByChainAssets[supportedOption.assetWithChain.asset.fullId]
|
||||
|
||||
asset?.let { AssetAndOption(asset, supportedOption) }
|
||||
}
|
||||
.sortedWith(assetsComparator())
|
||||
}
|
||||
|
||||
private fun assetsComparator(): Comparator<AssetAndOption<A>> {
|
||||
return compareBy<AssetAndOption<A>> { it.option.assetWithChain.chain.mainChainsFirstAscendingOrder }
|
||||
.thenBy { it.option.assetWithChain.chain.testnetsLastAscendingOrder }
|
||||
.thenByDescending { it.asset.token.amountToFiat(it.asset.transferable) }
|
||||
.thenByDescending { it.asset.transferable }
|
||||
.thenBy { it.option.assetWithChain.chain.alphabeticalOrder }
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.implementations
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.TokenUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.state.SelectedAssetOptionSharedState
|
||||
import io.novafoundation.nova.runtime.state.chainAsset
|
||||
import io.novafoundation.nova.runtime.state.selectedAssetFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
|
||||
class SharedStateTokenUseCase(
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val sharedState: SelectedAssetOptionSharedState<*>,
|
||||
) : TokenUseCase {
|
||||
|
||||
override suspend fun currentToken(): Token {
|
||||
val chainAsset = sharedState.chainAsset()
|
||||
|
||||
return tokenRepository.getToken(chainAsset)
|
||||
}
|
||||
|
||||
override fun currentTokenFlow(): Flow<Token> {
|
||||
return sharedState.selectedAssetFlow().flatMapLatest { chainAsset ->
|
||||
tokenRepository.observeToken(chainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getToken(chainAssetId: FullChainAssetId): Token {
|
||||
return tokenRepository.getToken(chainRegistry.asset(chainAssetId))
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.interfaces
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
interface ChainAssetRepository {
|
||||
|
||||
suspend fun setAssetsEnabled(enabled: Boolean, assetIds: List<FullChainAssetId>)
|
||||
|
||||
suspend fun insertCustomAsset(chainAsset: Chain.Asset)
|
||||
|
||||
suspend fun getEnabledAssets(): List<Chain.Asset>
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.interfaces
|
||||
|
||||
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_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferDirection
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.XcmTransferDryRunOrigin
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransfersConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferFeatures
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.time.Duration
|
||||
|
||||
class IncomingDirection(
|
||||
val asset: Asset,
|
||||
val chain: Chain
|
||||
)
|
||||
|
||||
typealias OutcomingDirection = ChainWithAsset
|
||||
|
||||
interface CrossChainTransfersUseCase {
|
||||
|
||||
suspend fun syncCrossChainConfig()
|
||||
|
||||
fun incomingCrossChainDirections(destination: Flow<Chain.Asset?>): Flow<List<IncomingDirection>>
|
||||
|
||||
fun outcomingCrossChainDirectionsFlow(origin: Chain.Asset): Flow<List<OutcomingDirection>>
|
||||
|
||||
suspend fun getConfiguration(): CrossChainTransfersConfiguration
|
||||
|
||||
suspend fun requiredRemainingAmountAfterTransfer(
|
||||
originChain: Chain,
|
||||
sendingAsset: Chain.Asset,
|
||||
destinationChain: Chain,
|
||||
): Balance
|
||||
|
||||
/**
|
||||
* @param cachingScope - a scope that will be registered as a dependency for internal caching. If null is passed, no caching will be used
|
||||
*/
|
||||
suspend fun ExtrinsicService.estimateFee(
|
||||
transfer: AssetTransferBase,
|
||||
cachingScope: CoroutineScope?
|
||||
): CrossChainTransferFee
|
||||
|
||||
suspend fun ExtrinsicService.performTransferOfExactAmount(transfer: AssetTransferBase, computationalScope: CoroutineScope): Result<ExtrinsicSubmission>
|
||||
|
||||
/**
|
||||
* @return result of actual received balance on destination
|
||||
*/
|
||||
suspend fun ExtrinsicService.performTransferAndTrackTransfer(
|
||||
transfer: AssetTransferBase,
|
||||
computationalScope: CoroutineScope
|
||||
): Result<Balance>
|
||||
|
||||
suspend fun maximumExecutionTime(
|
||||
assetTransferDirection: AssetTransferDirection,
|
||||
computationalScope: CoroutineScope
|
||||
): Duration
|
||||
|
||||
suspend fun dryRunTransferIfPossible(
|
||||
transfer: AssetTransferBase,
|
||||
origin: XcmTransferDryRunOrigin,
|
||||
computationalScope: CoroutineScope
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun supportsXcmExecute(
|
||||
originChainId: ChainId,
|
||||
features: DynamicCrossChainTransferFeatures
|
||||
): Boolean
|
||||
}
|
||||
|
||||
fun CrossChainTransfersUseCase.incomingCrossChainDirectionsAvailable(destination: Flow<Chain.Asset?>): Flow<Boolean> {
|
||||
return incomingCrossChainDirections(destination).map { it.isNotEmpty() }
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.interfaces
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TokenRepository {
|
||||
|
||||
/**
|
||||
* Observes tokens for given [chainAssets] associated by [FullChainAssetId].
|
||||
* Emitted map will contain keys for all supplied [chainAssets], even if some prices are currently unknown
|
||||
*/
|
||||
suspend fun observeTokens(chainAssets: List<Chain.Asset>): Flow<Map<FullChainAssetId, Token>>
|
||||
|
||||
suspend fun getTokens(chainAsset: List<Chain.Asset>): Map<FullChainAssetId, Token>
|
||||
|
||||
suspend fun getToken(chainAsset: Chain.Asset): Token
|
||||
|
||||
suspend fun getTokenOrNull(chainAsset: Chain.Asset): Token?
|
||||
|
||||
fun observeToken(chainAsset: Chain.Asset): Flow<Token>
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.interfaces
|
||||
|
||||
enum class TransactionFilter {
|
||||
EXTRINSIC, REWARD, TRANSFER, SWAP
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.interfaces
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import java.math.BigInteger
|
||||
|
||||
interface WalletConstants {
|
||||
|
||||
suspend fun existentialDeposit(chainId: ChainId): BigInteger
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.interfaces
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletRepository {
|
||||
|
||||
fun syncedAssetsFlow(metaId: Long): Flow<List<Asset>>
|
||||
|
||||
suspend fun getSyncedAssets(metaId: Long): List<Asset>
|
||||
suspend fun getSupportedAssets(metaId: Long): List<Asset>
|
||||
|
||||
fun supportedAssetsFlow(metaId: Long, chainAssets: List<Chain.Asset>): Flow<List<Asset>>
|
||||
|
||||
suspend fun syncAssetsRates(currency: Currency)
|
||||
suspend fun syncAssetRates(asset: Chain.Asset, currency: Currency)
|
||||
|
||||
fun assetFlow(
|
||||
accountId: AccountId,
|
||||
chainAsset: Chain.Asset
|
||||
): Flow<Asset>
|
||||
|
||||
fun assetFlow(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset
|
||||
): Flow<Asset>
|
||||
|
||||
fun assetFlowOrNull(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset
|
||||
): Flow<Asset?>
|
||||
|
||||
fun assetsFlow(
|
||||
metaId: Long,
|
||||
chainAssets: List<Chain.Asset>
|
||||
): Flow<List<Asset>>
|
||||
|
||||
suspend fun getAsset(
|
||||
accountId: AccountId,
|
||||
chainAsset: Chain.Asset
|
||||
): Asset?
|
||||
|
||||
suspend fun getAsset(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset
|
||||
): Asset?
|
||||
|
||||
suspend fun insertPendingTransfer(
|
||||
hash: String,
|
||||
assetTransfer: AssetTransfer,
|
||||
fee: SubmissionFee
|
||||
)
|
||||
|
||||
suspend fun clearAssets(assetIds: List<FullChainAssetId>)
|
||||
|
||||
suspend fun updatePhishingAddresses()
|
||||
|
||||
suspend fun isAccountIdFromPhishingList(accountId: AccountId): Boolean
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.domain.balance.EDCountingMode
|
||||
import io.novafoundation.nova.common.domain.balance.TransferableMode
|
||||
import io.novafoundation.nova.common.domain.balance.calculateBalanceCountedTowardsEd
|
||||
import io.novafoundation.nova.common.domain.balance.calculateTransferable
|
||||
import io.novafoundation.nova.common.domain.balance.totalBalance
|
||||
import io.novafoundation.nova.common.utils.sumByBigInteger
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigDecimal
|
||||
|
||||
// TODO we should remove duplication between Asset and ChainAssetBalance in regard to calculation of different balance types
|
||||
data class Asset(
|
||||
val token: Token,
|
||||
|
||||
// Non-reserved part of the balance. There may still be restrictions on
|
||||
// this, but it is the total pool what may in principle be transferred,
|
||||
// reserved.
|
||||
val freeInPlanks: Balance,
|
||||
|
||||
// Balance which is reserved and may not be used at all.
|
||||
// This balance is a 'reserve' balance that different subsystems use in
|
||||
// order to set aside tokens that are still 'owned' by the account
|
||||
// holder, but which are suspendable
|
||||
val reservedInPlanks: Balance,
|
||||
|
||||
// / The amount that `free` may not drop below when withdrawing.
|
||||
val frozenInPlanks: Balance,
|
||||
|
||||
val transferableMode: TransferableMode,
|
||||
val edCountingMode: EDCountingMode,
|
||||
|
||||
// TODO move to runtime storage
|
||||
val bondedInPlanks: Balance,
|
||||
val redeemableInPlanks: Balance,
|
||||
val unbondingInPlanks: Balance
|
||||
) {
|
||||
|
||||
/**
|
||||
* Liquid balance that can be transferred from an account
|
||||
* There are multiple ways it is identified, see [legacyTransferable] and [holdAndFreezesTransferable]
|
||||
*/
|
||||
val transferableInPlanks: Balance = transferableMode.calculateTransferable(freeInPlanks, frozenInPlanks, reservedInPlanks)
|
||||
|
||||
/**
|
||||
* Balance that is counted towards meeting the requirement of Existential Deposit
|
||||
* When the balance
|
||||
*/
|
||||
val balanceCountedTowardsEDInPlanks: Balance = edCountingMode.calculateBalanceCountedTowardsEd(freeInPlanks, reservedInPlanks)
|
||||
|
||||
// Non-reserved plus reserved
|
||||
val totalInPlanks = totalBalance(freeInPlanks, reservedInPlanks)
|
||||
|
||||
// balance that cannot be used for transfers (non-transferable) for any reason
|
||||
val lockedInPlanks = totalInPlanks - transferableInPlanks
|
||||
|
||||
// TODO maybe move to extension fields?
|
||||
// Check affect on performance, if those fields will be recalculated on each usage
|
||||
val total = token.amountFromPlanks(totalInPlanks)
|
||||
val reserved = token.amountFromPlanks(reservedInPlanks)
|
||||
val locked = token.amountFromPlanks(lockedInPlanks)
|
||||
val transferable = token.amountFromPlanks(transferableInPlanks)
|
||||
|
||||
val free = token.amountFromPlanks(freeInPlanks)
|
||||
val frozen = token.amountFromPlanks(frozenInPlanks)
|
||||
|
||||
// TODO move to runtime storage
|
||||
val bonded = token.amountFromPlanks(bondedInPlanks)
|
||||
val redeemable = token.amountFromPlanks(redeemableInPlanks)
|
||||
val unbonding = token.amountFromPlanks(unbondingInPlanks)
|
||||
}
|
||||
|
||||
fun Asset.unlabeledReserves(holds: Collection<BalanceHold>): Balance {
|
||||
return unlabeledReserves(holds.sumByBigInteger { it.amountInPlanks })
|
||||
}
|
||||
|
||||
fun Asset.unlabeledReserves(labeledReserves: Balance): Balance {
|
||||
return reservedInPlanks - labeledReserves
|
||||
}
|
||||
|
||||
fun Asset.balanceCountedTowardsED(): BigDecimal {
|
||||
return token.amountFromPlanks(balanceCountedTowardsEDInPlanks)
|
||||
}
|
||||
|
||||
fun Asset.transferableReplacingFrozen(newFrozen: Balance): Balance {
|
||||
return transferableMode.calculateTransferable(freeInPlanks, newFrozen, reservedInPlanks)
|
||||
}
|
||||
|
||||
fun Asset.regularTransferableBalance(): Balance {
|
||||
return TransferableMode.REGULAR.calculateTransferable(freeInPlanks, frozenInPlanks, reservedInPlanks)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
object BalanceBreakdownIds {
|
||||
|
||||
const val RESERVED = "reserved"
|
||||
|
||||
const val CROWDLOAN = "crowdloan"
|
||||
|
||||
const val NOMINATION_POOL = "nomination-pool"
|
||||
|
||||
const val NOMINATION_POOL_DELEGATED = "DelegatedStaking: StakingDelegation"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Identifiable
|
||||
import io.novafoundation.nova.core_db.model.BalanceHoldLocal
|
||||
import io.novafoundation.nova.core_db.model.BalanceHoldLocal.HoldIdLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold.HoldId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class BalanceHold(
|
||||
val id: HoldId,
|
||||
val amountInPlanks: Balance,
|
||||
val chainAsset: Chain.Asset
|
||||
) : Identifiable {
|
||||
|
||||
class HoldId(val module: String, val reason: String)
|
||||
|
||||
// Keep in tact with `BalanceBreakdownIds`
|
||||
override val identifier: String = "${id.module}: ${id.reason}"
|
||||
}
|
||||
|
||||
fun mapBalanceHoldFromLocal(
|
||||
asset: Chain.Asset,
|
||||
hold: BalanceHoldLocal
|
||||
): BalanceHold {
|
||||
return BalanceHold(
|
||||
id = hold.id.toDomain(),
|
||||
amountInPlanks = hold.amount,
|
||||
chainAsset = asset
|
||||
)
|
||||
}
|
||||
|
||||
private fun HoldIdLocal.toDomain(): HoldId {
|
||||
return HoldId(module, reason)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Identifiable
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.core_db.model.BalanceLockLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class BalanceLock(
|
||||
val id: BalanceLockId,
|
||||
val amountInPlanks: Balance,
|
||||
val chainAsset: Chain.Asset
|
||||
) : Identifiable by id
|
||||
|
||||
@JvmInline
|
||||
value class BalanceLockId private constructor(val value: String) : Identifiable {
|
||||
|
||||
override val identifier: String
|
||||
get() = value
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromPath(vararg pathSegments: String): BalanceLockId {
|
||||
val fullId = pathSegments.joinToString(separator = ": ")
|
||||
return fromFullId(fullId)
|
||||
}
|
||||
|
||||
fun fromFullId(fullId: String): BalanceLockId {
|
||||
return BalanceLockId(fullId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mapBalanceLockFromLocal(
|
||||
asset: Chain.Asset,
|
||||
lock: BalanceLockLocal
|
||||
): BalanceLock {
|
||||
return BalanceLock(
|
||||
id = BalanceLockId.fromFullId(lock.type),
|
||||
amountInPlanks = lock.amount,
|
||||
chainAsset = asset
|
||||
)
|
||||
}
|
||||
|
||||
fun List<BalanceLock>.maxLockReplacing(lockId: BalanceLockId, replaceWith: Balance): Balance {
|
||||
return maxOfOrNull {
|
||||
if (it.id == lockId) replaceWith else it.amountInPlanks
|
||||
}.orZero()
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.binarySearchFloor
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
interface CoinRate {
|
||||
val rate: BigDecimal
|
||||
}
|
||||
|
||||
data class CoinRateChange(val recentRateChange: BigDecimal, override val rate: BigDecimal) : CoinRate
|
||||
|
||||
class HistoricalCoinRate(val timestamp: Long, override val rate: BigDecimal) : CoinRate
|
||||
|
||||
fun CoinRate.convertAmount(amount: BigDecimal) = amount * rate
|
||||
|
||||
fun CoinRate.convertFiatToAmount(fiat: BigDecimal): BigDecimal {
|
||||
if (rate.isZero) return BigDecimal.ZERO
|
||||
|
||||
return fiat / rate
|
||||
}
|
||||
|
||||
fun CoinRate.convertFiatToPlanks(asset: Chain.Asset, fiat: BigDecimal): BigInteger {
|
||||
return asset.planksFromAmount(convertFiatToAmount(fiat))
|
||||
}
|
||||
|
||||
fun CoinRate.convertPlanks(asset: Chain.Asset, amount: BigInteger) = convertAmount(asset.amountFromPlanks(amount))
|
||||
|
||||
fun List<HistoricalCoinRate>.findNearestCoinRate(timestamp: Long): HistoricalCoinRate? {
|
||||
if (isEmpty()) return null
|
||||
if (first().timestamp > timestamp) return null // To support the case when the token started trading later than the desired coin rate
|
||||
|
||||
val index = binarySearchFloor { it.timestamp.compareTo(timestamp) }
|
||||
return getOrNull(index)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class CrossChainTransferFee(
|
||||
/**
|
||||
* Deducted upon initial transaction submission from the origin chain. Asset can be controlled with [FeePaymentCurrency]
|
||||
*/
|
||||
val submissionFee: SubmissionFee,
|
||||
|
||||
/**
|
||||
* Deducted upon initial transaction submission from the origin chain. Cannot be controlled with [FeePaymentCurrency]
|
||||
* and is always paid in native currency
|
||||
*/
|
||||
val postSubmissionByAccount: SubmissionFee?,
|
||||
|
||||
/**
|
||||
* Total sum of all execution and delivery fees paid from holding register throughout xcm transfer
|
||||
* Paid (at the moment) in a sending asset. There might be multiple [Chain.Asset] that represent the same logical asset,
|
||||
* the asset here indicates the first one, on the origin chain
|
||||
*/
|
||||
val postSubmissionFromAmount: FeeBase,
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.sumByBigInteger
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
class ExternalBalance(
|
||||
val chainAssetId: FullChainAssetId,
|
||||
val amount: Balance,
|
||||
val type: Type
|
||||
) {
|
||||
|
||||
enum class Type {
|
||||
CROWDLOAN, NOMINATION_POOL
|
||||
}
|
||||
}
|
||||
|
||||
fun List<ExternalBalance>.aggregatedBalanceByAsset(): Map<FullChainAssetId, Balance> = groupBy { it.chainAssetId }
|
||||
.mapValues { (_, assetExternalBalances) -> assetExternalBalances.sumByBigInteger(ExternalBalance::amount) }
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import java.math.BigDecimal
|
||||
|
||||
class FiatAmount(
|
||||
val currency: Currency,
|
||||
val price: BigDecimal
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun zero(currency: Currency): FiatAmount {
|
||||
return FiatAmount(currency, BigDecimal.ZERO)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
enum class GetAssetOption {
|
||||
RECEIVE, CROSS_CHAIN, BUY
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
data class Operation(
|
||||
val id: String,
|
||||
val address: String,
|
||||
val type: Type,
|
||||
val time: Long,
|
||||
val chainAsset: Chain.Asset,
|
||||
val extrinsicHash: String?,
|
||||
val status: Status,
|
||||
) {
|
||||
|
||||
sealed class Type {
|
||||
|
||||
data class Extrinsic(
|
||||
val content: Content,
|
||||
val fee: BigInteger,
|
||||
val fiatFee: BigDecimal?,
|
||||
) : Type() {
|
||||
|
||||
sealed class Content {
|
||||
|
||||
class SubstrateCall(val module: String, val call: String) : Content()
|
||||
|
||||
class ContractCall(val contractAddress: String, val function: String?) : Content()
|
||||
}
|
||||
}
|
||||
|
||||
data class Reward(
|
||||
val amount: BigInteger,
|
||||
val fiatAmount: BigDecimal?,
|
||||
val isReward: Boolean,
|
||||
val eventId: String,
|
||||
val kind: RewardKind
|
||||
) : Type() {
|
||||
|
||||
sealed class RewardKind {
|
||||
|
||||
class Direct(val era: Int?, val validator: String?) : RewardKind()
|
||||
|
||||
class Pool(val poolId: Int) : RewardKind()
|
||||
}
|
||||
}
|
||||
|
||||
data class Transfer(
|
||||
val myAddress: String,
|
||||
val amount: BigInteger,
|
||||
val fiatAmount: BigDecimal?,
|
||||
val receiver: String,
|
||||
val sender: String,
|
||||
val fee: BigInteger?
|
||||
) : Type()
|
||||
|
||||
data class Swap(
|
||||
val fee: ChainAssetWithAmount,
|
||||
val amountIn: ChainAssetWithAmount,
|
||||
val amountOut: ChainAssetWithAmount,
|
||||
val fiatAmount: BigDecimal?
|
||||
) : Type()
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
PENDING, COMPLETED, FAILED;
|
||||
|
||||
companion object {
|
||||
fun fromSuccess(success: Boolean): Status {
|
||||
return if (success) COMPLETED else FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ChainAssetWithAmount(
|
||||
val chainAsset: Chain.Asset,
|
||||
val amount: Balance,
|
||||
)
|
||||
|
||||
val ChainAssetWithAmount.decimalAmount: BigDecimal
|
||||
get() = chainAsset.amountFromPlanks(amount)
|
||||
|
||||
fun ChainAssetWithAmount.toIdWithAmount(): ChainAssetIdWithAmount {
|
||||
return chainAsset.fullId.withAmount(amount)
|
||||
}
|
||||
|
||||
data class ChainAssetIdWithAmount(
|
||||
val chainAssetId: FullChainAssetId,
|
||||
val amount: Balance,
|
||||
)
|
||||
|
||||
fun FullChainAssetId.withAmount(amount: Balance): ChainAssetIdWithAmount {
|
||||
return ChainAssetIdWithAmount(this, amount)
|
||||
}
|
||||
|
||||
fun Chain.Asset.withAmount(amount: Balance): ChainAssetWithAmount {
|
||||
return ChainAssetWithAmount(this, amount)
|
||||
}
|
||||
|
||||
fun Operation.Type.satisfies(filters: Set<TransactionFilter>): Boolean {
|
||||
return matchingTransactionFilter() in filters
|
||||
}
|
||||
|
||||
fun Operation.isZeroTransfer(): Boolean {
|
||||
return type is Operation.Type.Transfer && type.amount.isZero
|
||||
}
|
||||
|
||||
private fun Operation.Type.matchingTransactionFilter(): TransactionFilter {
|
||||
return when (this) {
|
||||
is Operation.Type.Extrinsic -> TransactionFilter.EXTRINSIC
|
||||
is Operation.Type.Reward -> TransactionFilter.REWARD
|
||||
is Operation.Type.Transfer -> TransactionFilter.TRANSFER
|
||||
is Operation.Type.Swap -> TransactionFilter.SWAP
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.data.model.DataPage
|
||||
|
||||
data class OperationsPageChange(
|
||||
val cursorPage: DataPage<Operation>,
|
||||
val accountChanged: Boolean
|
||||
)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
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.SubstrateFeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.getAmount
|
||||
|
||||
data class OriginFee(
|
||||
val submissionFee: SubmissionFee,
|
||||
val deliveryFee: SubmissionFee?,
|
||||
) {
|
||||
|
||||
val totalInSubmissionAsset: FeeBase = createTotalFeeInSubmissionAsset()
|
||||
|
||||
fun replaceSubmissionFee(submissionFee: SubmissionFee): OriginFee {
|
||||
return copy(submissionFee = submissionFee)
|
||||
}
|
||||
|
||||
private fun createTotalFeeInSubmissionAsset(): FeeBase {
|
||||
val submissionAsset = submissionFee.asset
|
||||
val totalAmount = submissionFee.amount + deliveryFee?.getAmount(submissionAsset).orZero()
|
||||
return SubstrateFeeBase(totalAmount, submissionAsset)
|
||||
}
|
||||
}
|
||||
|
||||
fun OriginFee.intoFeeList(): List<Fee> {
|
||||
return listOfNotNull(submissionFee, deliveryFee)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import java.math.BigDecimal
|
||||
|
||||
class PricedAmount(
|
||||
val amount: BigDecimal,
|
||||
val price: BigDecimal,
|
||||
val currency: Currency
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
class RecipientSearchResult(
|
||||
val myAccounts: List<WalletAccount>,
|
||||
val contacts: List<String>
|
||||
)
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.amountFromPlanks
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.utils.planksFromAmount
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
interface TokenBase {
|
||||
|
||||
val currency: Currency
|
||||
|
||||
val coinRate: CoinRate?
|
||||
|
||||
val configuration: Chain.Asset
|
||||
|
||||
fun amountToFiat(tokenAmount: BigDecimal): BigDecimal = toFiatOrNull(tokenAmount).orZero()
|
||||
|
||||
fun planksToFiat(tokenAmountPlanks: BigInteger): BigDecimal = planksToFiatOrNull(tokenAmountPlanks).orZero()
|
||||
}
|
||||
|
||||
data class Token(
|
||||
override val currency: Currency,
|
||||
override val coinRate: CoinRateChange?,
|
||||
override val configuration: Chain.Asset
|
||||
) : TokenBase {
|
||||
// TODO move out of the class when Context Receivers will be stable
|
||||
fun BigDecimal.toPlanks() = planksFromAmount(this)
|
||||
fun BigInteger.toAmount() = amountFromPlanks(this)
|
||||
}
|
||||
|
||||
fun Token.fiatAmountOf(planks: Balance): FiatAmount {
|
||||
return FiatAmount(
|
||||
currency = currency,
|
||||
price = planksToFiat(planks)
|
||||
)
|
||||
}
|
||||
|
||||
data class HistoricalToken(
|
||||
override val currency: Currency,
|
||||
override val coinRate: HistoricalCoinRate?,
|
||||
override val configuration: Chain.Asset
|
||||
) : TokenBase
|
||||
|
||||
fun TokenBase.toFiatOrNull(tokenAmount: BigDecimal): BigDecimal? = coinRate?.convertAmount(tokenAmount)
|
||||
|
||||
fun TokenBase.planksFromFiatOrZero(fiat: BigDecimal): Balance = coinRate?.convertFiatToPlanks(configuration, fiat).orZero()
|
||||
|
||||
fun TokenBase.planksToFiatOrNull(tokenAmountPlanks: BigInteger): BigDecimal? = coinRate?.convertPlanks(configuration, tokenAmountPlanks)
|
||||
|
||||
fun TokenBase.amountFromPlanks(amountInPlanks: BigInteger) = configuration.amountFromPlanks(amountInPlanks)
|
||||
|
||||
fun TokenBase.planksFromAmount(amount: BigDecimal): BigInteger = configuration.planksFromAmount(amount)
|
||||
|
||||
fun Chain.Asset.amountFromPlanks(amountInPlanks: BigInteger) = amountInPlanks.amountFromPlanks(precision)
|
||||
|
||||
fun Chain.Asset.planksFromAmount(amount: BigDecimal): BigInteger = amount.planksFromAmount(precision)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model
|
||||
|
||||
class WalletAccount(
|
||||
val address: String,
|
||||
val name: String?,
|
||||
)
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.XcmTransferType
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.XcmChain
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.chainLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
sealed interface CrossChainTransferConfiguration : CrossChainTransferConfigurationBase {
|
||||
|
||||
class Legacy(val config: LegacyCrossChainTransferConfiguration) :
|
||||
CrossChainTransferConfiguration,
|
||||
CrossChainTransferConfigurationBase by config
|
||||
|
||||
class Dynamic(val config: DynamicCrossChainTransferConfiguration) :
|
||||
CrossChainTransferConfiguration,
|
||||
CrossChainTransferConfigurationBase by config
|
||||
}
|
||||
|
||||
interface CrossChainTransferConfigurationBase {
|
||||
|
||||
val originChain: XcmChain
|
||||
|
||||
val destinationChain: XcmChain
|
||||
|
||||
val originChainAsset: Chain.Asset
|
||||
|
||||
val transferType: XcmTransferType
|
||||
|
||||
/**
|
||||
* Any info usefully for logging besides fields [CrossChainTransferConfigurationBase] already expose
|
||||
*/
|
||||
fun debugExtraInfo(): String
|
||||
}
|
||||
|
||||
val CrossChainTransferConfigurationBase.originChainLocation: ChainLocation
|
||||
get() = originChain.chainLocation()
|
||||
|
||||
val CrossChainTransferConfigurationBase.destinationChainLocation: ChainLocation
|
||||
get() = destinationChain.chainLocation()
|
||||
|
||||
val CrossChainTransferConfigurationBase.originChainId: ChainId
|
||||
get() = originChainLocation.chainId
|
||||
|
||||
val CrossChainTransferConfigurationBase.destinationChainId: ChainId
|
||||
get() = destinationChainLocation.chainId
|
||||
|
||||
fun CrossChainTransferConfigurationBase.assetLocationOnOrigin(): RelativeMultiLocation {
|
||||
return transferType.assetAbsoluteLocation.fromPointOfViewOf(originChainLocation.location)
|
||||
}
|
||||
|
||||
fun CrossChainTransferConfigurationBase.destinationChainLocationOnOrigin(): RelativeMultiLocation {
|
||||
return destinationChainLocation.location.fromPointOfViewOf(originChainLocation.location)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransfersConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration
|
||||
|
||||
class CrossChainTransfersConfiguration(
|
||||
val dynamic: DynamicCrossChainTransfersConfiguration,
|
||||
val legacy: LegacyCrossChainTransfersConfiguration
|
||||
)
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.graph.Edge
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.availableInDestinations
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.availableOutDestinations
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.hasDeliveryFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.transferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.availableInDestinations
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.availableOutDestinations
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.hasDeliveryFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.transferConfiguration
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.XcmChain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
fun CrossChainTransfersConfiguration.availableOutDestinations(origin: Chain.Asset): List<FullChainAssetId> {
|
||||
val combined = dynamic.availableOutDestinations(origin) + legacy.availableOutDestinations(origin)
|
||||
return combined.distinct()
|
||||
}
|
||||
|
||||
fun CrossChainTransfersConfiguration.availableInDestinations(destination: Chain.Asset): List<FullChainAssetId> {
|
||||
val combined = dynamic.availableInDestinations(destination) + legacy.availableInDestinations(destination)
|
||||
return combined.distinct()
|
||||
}
|
||||
|
||||
fun CrossChainTransfersConfiguration.availableInDestinations(): List<Edge<FullChainAssetId>> {
|
||||
val combined = dynamic.availableInDestinations() + legacy.availableInDestinations()
|
||||
return combined.distinct()
|
||||
}
|
||||
|
||||
fun CrossChainTransfersConfiguration.hasDeliveryFee(
|
||||
origin: FullChainAssetId,
|
||||
destination: FullChainAssetId
|
||||
): Boolean {
|
||||
return dynamic.hasDeliveryFee(origin, destination) ?: legacy.hasDeliveryFee(origin.chainId)
|
||||
}
|
||||
|
||||
suspend fun CrossChainTransfersConfiguration.transferConfiguration(
|
||||
originChain: XcmChain,
|
||||
originAsset: Chain.Asset,
|
||||
destinationChain: XcmChain,
|
||||
): CrossChainTransferConfiguration? {
|
||||
val result = dynamic.transferConfiguration(originChain, originAsset, destinationChain)?.let(CrossChainTransferConfiguration::Dynamic)
|
||||
?: legacy.transferConfiguration(originChain, originAsset, destinationChain)?.let(CrossChainTransferConfiguration::Legacy)
|
||||
|
||||
logTransferConfiguration(originAsset, originChain, destinationChain, result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun logTransferConfiguration(
|
||||
originAsset: Chain.Asset,
|
||||
originChain: XcmChain,
|
||||
destinationChain: XcmChain,
|
||||
result: CrossChainTransferConfiguration?
|
||||
) {
|
||||
val logDirectionLabel = "${originAsset.symbol} ${originChain.chain.name} -> ${destinationChain.chain.name}"
|
||||
if (result == null) {
|
||||
Log.d("CrossChainTransfersConfiguration", "Found no configuration for direction $logDirectionLabel")
|
||||
} else {
|
||||
val message = """
|
||||
Using ${result::class.simpleName} configuration for direction $logDirectionLabel
|
||||
Transfer type: ${result.transferType}
|
||||
${result.debugExtraInfo()}
|
||||
|
||||
""".trimIndent()
|
||||
Log.d("CrossChainTransfersConfiguration", message)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransferConfigurationBase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.XcmTransferType
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.XcmChain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class DynamicCrossChainTransferConfiguration(
|
||||
override val originChain: XcmChain,
|
||||
override val destinationChain: XcmChain,
|
||||
override val transferType: XcmTransferType,
|
||||
override val originChainAsset: Chain.Asset,
|
||||
val features: DynamicCrossChainTransferFeatures,
|
||||
) : CrossChainTransferConfigurationBase {
|
||||
|
||||
override fun debugExtraInfo(): String {
|
||||
return "features=$features"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic
|
||||
|
||||
data class DynamicCrossChainTransferFeatures(
|
||||
val hasDeliveryFee: Boolean,
|
||||
val supportsXcmExecute: Boolean,
|
||||
)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.TokenReserveRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
class DynamicCrossChainTransfersConfiguration(
|
||||
val reserveRegistry: TokenReserveRegistry,
|
||||
val customTeleports: Set<CustomTeleportEntry>,
|
||||
val chains: Map<ChainId, List<AssetTransfers>>
|
||||
) {
|
||||
|
||||
class AssetTransfers(
|
||||
val assetId: ChainAssetId,
|
||||
val destinations: List<TransferDestination>
|
||||
)
|
||||
|
||||
class TransferDestination(
|
||||
val fullChainAssetId: FullChainAssetId,
|
||||
val hasDeliveryFee: Boolean,
|
||||
val supportsXcmExecute: Boolean,
|
||||
)
|
||||
|
||||
data class CustomTeleportEntry(val originChainAssetId: FullChainAssetId, val destinationChainId: ChainId)
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic
|
||||
|
||||
import io.novafoundation.nova.common.utils.graph.Edge
|
||||
import io.novafoundation.nova.common.utils.graph.SimpleEdge
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransfersConfiguration.AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransfersConfiguration.CustomTeleportEntry
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransfersConfiguration.TransferDestination
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.XcmTransferType
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.XcmChain
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
fun DynamicCrossChainTransfersConfiguration.availableOutDestinations(origin: Chain.Asset): List<FullChainAssetId> {
|
||||
val assetTransfers = outComingAssetTransfers(origin.fullId) ?: return emptyList()
|
||||
return assetTransfers.destinations.map { it.fullChainAssetId }
|
||||
}
|
||||
|
||||
fun DynamicCrossChainTransfersConfiguration.availableInDestinations(destination: Chain.Asset): List<FullChainAssetId> {
|
||||
val requiredDestinationId = destination.fullId
|
||||
|
||||
return chains.flatMap { (originChainId, chainTransfers) ->
|
||||
chainTransfers.mapNotNull { originAssetTransfers ->
|
||||
val hasDestination = originAssetTransfers.destinations.any { it.fullChainAssetId == requiredDestinationId }
|
||||
|
||||
if (hasDestination) {
|
||||
FullChainAssetId(originChainId, originAssetTransfers.assetId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DynamicCrossChainTransfersConfiguration.availableInDestinations(): List<Edge<FullChainAssetId>> {
|
||||
return chains.flatMap { (originChainId, chainTransfers) ->
|
||||
chainTransfers.flatMap { originAssetTransfers ->
|
||||
originAssetTransfers.destinations.map {
|
||||
val from = FullChainAssetId(originChainId, originAssetTransfers.assetId)
|
||||
val to = it.fullChainAssetId
|
||||
|
||||
SimpleEdge(from, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DynamicCrossChainTransfersConfiguration.transferFeatures(
|
||||
originAsset: FullChainAssetId,
|
||||
destinationChainId: ChainId
|
||||
): DynamicCrossChainTransferFeatures? {
|
||||
return outComingAssetTransfers(originAsset)?.getDestination(destinationChainId)?.getTransferFeatures()
|
||||
}
|
||||
|
||||
suspend fun DynamicCrossChainTransfersConfiguration.transferConfiguration(
|
||||
originXcmChain: XcmChain,
|
||||
originAsset: Chain.Asset,
|
||||
destinationXcmChain: XcmChain,
|
||||
): DynamicCrossChainTransferConfiguration? {
|
||||
val destinationChain = destinationXcmChain.chain
|
||||
|
||||
val assetTransfers = outComingAssetTransfers(originAsset.fullId) ?: return null
|
||||
val targetTransfer = assetTransfers.getDestination(destinationChain.id) ?: return null
|
||||
|
||||
val reserve = reserveRegistry.getReserve(originAsset)
|
||||
|
||||
return DynamicCrossChainTransferConfiguration(
|
||||
originChain = originXcmChain,
|
||||
destinationChain = destinationXcmChain,
|
||||
originChainAsset = originAsset,
|
||||
transferType = XcmTransferType.determineTransferType(
|
||||
usesTeleports = canUseTeleport(originXcmChain, originAsset, destinationXcmChain),
|
||||
originChain = originXcmChain,
|
||||
destinationChain = destinationXcmChain,
|
||||
reserve = reserve
|
||||
),
|
||||
features = targetTransfer.getTransferFeatures(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun DynamicCrossChainTransfersConfiguration.canUseTeleport(
|
||||
originXcmChain: XcmChain,
|
||||
originAsset: Chain.Asset,
|
||||
destinationXcmChain: XcmChain,
|
||||
): Boolean {
|
||||
val customTeleportEntry = CustomTeleportEntry(originAsset.fullId, destinationXcmChain.chain.id)
|
||||
if (customTeleportEntry in customTeleports) return true
|
||||
|
||||
return XcmTransferType.isSystemTeleport(originXcmChain, destinationXcmChain)
|
||||
}
|
||||
|
||||
private fun AssetTransfers.getDestination(destinationChainId: ChainId): TransferDestination? {
|
||||
return destinations.find { it.fullChainAssetId.chainId == destinationChainId }
|
||||
}
|
||||
|
||||
private fun TransferDestination.getTransferFeatures(): DynamicCrossChainTransferFeatures {
|
||||
return DynamicCrossChainTransferFeatures(
|
||||
hasDeliveryFee = hasDeliveryFee,
|
||||
supportsXcmExecute = supportsXcmExecute,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null if transfer is unknown, true if delivery fee has to be paid, false otherwise
|
||||
*/
|
||||
fun DynamicCrossChainTransfersConfiguration.hasDeliveryFee(
|
||||
origin: FullChainAssetId,
|
||||
destination: FullChainAssetId
|
||||
): Boolean? {
|
||||
val transfers = outComingAssetTransfers(origin) ?: return null
|
||||
val destinationConfig = transfers.destinations.find { it.fullChainAssetId == destination } ?: return null
|
||||
|
||||
return destinationConfig.hasDeliveryFee
|
||||
}
|
||||
|
||||
private fun DynamicCrossChainTransfersConfiguration.outComingAssetTransfers(origin: FullChainAssetId): AssetTransfers? {
|
||||
return chains[origin.chainId]?.find { it.assetId == origin.assetId }
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve
|
||||
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
class TokenReserve(
|
||||
val reserveChainLocation: ChainLocation,
|
||||
val tokenLocation: AbsoluteMultiLocation
|
||||
)
|
||||
|
||||
fun TokenReserve.isRemote(origin: ChainId, destination: ChainId): Boolean {
|
||||
return origin != reserveChainLocation.chainId && destination != reserveChainLocation.chainId
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve
|
||||
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
class TokenReserveConfig(
|
||||
val reserveChainId: ChainId,
|
||||
val tokenReserveLocation: AbsoluteMultiLocation,
|
||||
)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve
|
||||
|
||||
typealias TokenReserveId = String
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.getChainLocation
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.normalizeSymbol
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
|
||||
class TokenReserveRegistry(
|
||||
private val parachainInfoRepository: ParachainInfoRepository,
|
||||
|
||||
private val reservesById: Map<TokenReserveId, TokenReserveConfig>,
|
||||
|
||||
// By default, asset reserve id is equal to its symbol
|
||||
// This mapping allows to override that for cases like multiple reserves (Statemine & Polkadot for DOT)
|
||||
private val assetToReserveIdOverrides: Map<FullChainAssetId, TokenReserveId>
|
||||
) {
|
||||
|
||||
suspend fun getReserve(chainAsset: Chain.Asset): TokenReserve {
|
||||
val reserveId = getReserveId(chainAsset)
|
||||
val reserve = reservesById.getValue(reserveId)
|
||||
return TokenReserve(
|
||||
reserveChainLocation = parachainInfoRepository.getChainLocation(reserve.reserveChainId),
|
||||
tokenLocation = reserve.tokenReserveLocation
|
||||
)
|
||||
}
|
||||
|
||||
private fun getReserveId(chainAsset: Chain.Asset): TokenReserveId {
|
||||
return assetToReserveIdOverrides[chainAsset.fullId] ?: chainAsset.normalizeSymbol()
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve
|
||||
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.XcmChain
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.isRelay
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.isSystemChain
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.ChainLocation
|
||||
|
||||
sealed interface XcmTransferType {
|
||||
|
||||
companion object {
|
||||
|
||||
fun determineTransferType(
|
||||
usesTeleports: Boolean,
|
||||
originChain: XcmChain,
|
||||
destinationChain: XcmChain,
|
||||
reserve: TokenReserve
|
||||
): XcmTransferType {
|
||||
val assetAbsoluteLocation = reserve.tokenLocation
|
||||
|
||||
return when {
|
||||
usesTeleports -> Teleport(assetAbsoluteLocation)
|
||||
originChain.chain.id == reserve.reserveChainLocation.chainId -> Reserve.Origin(assetAbsoluteLocation)
|
||||
destinationChain.chain.id == reserve.reserveChainLocation.chainId -> Reserve.Destination(assetAbsoluteLocation)
|
||||
else -> Reserve.Remote(assetAbsoluteLocation, reserve.reserveChainLocation)
|
||||
}
|
||||
}
|
||||
|
||||
fun isSystemTeleport(originXcmChain: XcmChain, destinationXcmChain: XcmChain): Boolean {
|
||||
val systemToRelay = originXcmChain.isSystemChain() && destinationXcmChain.isRelay()
|
||||
val relayToSystem = originXcmChain.isRelay() && destinationXcmChain.isSystemChain()
|
||||
val systemToSystem = originXcmChain.isSystemChain() && destinationXcmChain.isSystemChain()
|
||||
|
||||
return systemToRelay || relayToSystem || systemToSystem
|
||||
}
|
||||
}
|
||||
|
||||
val assetAbsoluteLocation: AbsoluteMultiLocation
|
||||
|
||||
data class Teleport(override val assetAbsoluteLocation: AbsoluteMultiLocation) : XcmTransferType
|
||||
|
||||
sealed interface Reserve : XcmTransferType {
|
||||
|
||||
data class Origin(override val assetAbsoluteLocation: AbsoluteMultiLocation) : Reserve
|
||||
|
||||
data class Destination(override val assetAbsoluteLocation: AbsoluteMultiLocation) : Reserve
|
||||
|
||||
data class Remote(
|
||||
override val assetAbsoluteLocation: AbsoluteMultiLocation,
|
||||
val remoteReserveLocation: ChainLocation
|
||||
) : Reserve
|
||||
}
|
||||
}
|
||||
|
||||
fun XcmTransferType.remoteReserveLocation(): ChainLocation? {
|
||||
return (this as? XcmTransferType.Reserve.Remote)?.remoteReserveLocation
|
||||
}
|
||||
|
||||
fun XcmTransferType.isRemoteReserve(): Boolean {
|
||||
return this is XcmTransferType.Reserve.Remote
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Weight
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransferConfigurationBase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.XcmTransferType
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration.XcmFee
|
||||
import io.novafoundation.nova.feature_xcm_api.chain.XcmChain
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
class LegacyCrossChainTransferConfiguration(
|
||||
override val originChain: XcmChain,
|
||||
override val destinationChain: XcmChain,
|
||||
override val originChainAsset: Chain.Asset,
|
||||
override val transferType: XcmTransferType,
|
||||
|
||||
// Those 3 fields are duplicated by CrossChainTransferConfigurationBase extensions
|
||||
// But we do not refactor it to avoid unnecessary scope exposure
|
||||
val assetLocation: RelativeMultiLocation,
|
||||
val reserveChainLocation: RelativeMultiLocation,
|
||||
val destinationChainLocation: RelativeMultiLocation,
|
||||
|
||||
val destinationFee: CrossChainFeeConfiguration,
|
||||
val reserveFee: CrossChainFeeConfiguration?,
|
||||
val transferMethod: LegacyXcmTransferMethod,
|
||||
) : CrossChainTransferConfigurationBase {
|
||||
|
||||
override fun debugExtraInfo(): String {
|
||||
return "transferMethod=$transferMethod"
|
||||
}
|
||||
}
|
||||
|
||||
class CrossChainFeeConfiguration(
|
||||
val from: From,
|
||||
val to: To
|
||||
) {
|
||||
|
||||
class From(val chainId: ChainId, val deliveryFeeConfiguration: DeliveryFeeConfiguration?)
|
||||
|
||||
class To(
|
||||
val chainId: ChainId,
|
||||
val instructionWeight: Weight,
|
||||
val xcmFeeType: XcmFee<List<XCMInstructionType>>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user