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:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+37
View File
@@ -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>
@@ -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)
}
@@ -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()
}
@@ -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
)
}
}
@@ -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
)
}
}
@@ -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
)
@@ -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
)
}
@@ -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)
}
}
@@ -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
}
@@ -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
}
@@ -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)
}
@@ -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)
}
@@ -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()
)
}
@@ -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
@@ -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
)
@@ -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()
}
@@ -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,
)
@@ -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
)
@@ -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
}
@@ -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
}
}
}
}
@@ -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)
}
@@ -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
)
}
@@ -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)
@@ -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)
@@ -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
}
@@ -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
)
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types
import java.math.BigInteger
typealias Balance = BigInteger
@@ -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>
}
@@ -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>
}
@@ -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()
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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()
}
@@ -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)
}
@@ -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?>>
}
@@ -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
}
@@ -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
}
@@ -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>>
}
@@ -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>>
}
@@ -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)
}
@@ -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>)
}
@@ -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)
}
@@ -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
}
}
@@ -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>
}
@@ -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>)
}
@@ -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?
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_wallet_api.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.SOURCE)
annotation class BalanceLocks
@@ -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
}
@@ -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
)
}
@@ -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<*>
}
}
@@ -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,
)
}
@@ -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)
}
}
@@ -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))
}
}
@@ -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>>
}
@@ -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()
@@ -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>
}
@@ -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 }
}
@@ -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
}
@@ -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)
}
}
}
@@ -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 }
}
}
@@ -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))
}
}
@@ -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>
}
@@ -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() }
}
@@ -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>
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_wallet_api.domain.interfaces
enum class TransactionFilter {
EXTRINSIC, REWARD, TRANSFER, SWAP
}
@@ -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
}
@@ -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
}
@@ -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)
}
@@ -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"
}
@@ -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)
}
@@ -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()
}
@@ -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)
}
@@ -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,
)
@@ -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) }
@@ -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)
}
}
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_wallet_api.domain.model
enum class GetAssetOption {
RECEIVE, CROSS_CHAIN, BUY
}
@@ -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
}
}
@@ -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
)
@@ -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)
}
@@ -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
)
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_wallet_api.domain.model
class RecipientSearchResult(
val myAccounts: List<WalletAccount>,
val contacts: List<String>
)
@@ -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)
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_wallet_api.domain.model
class WalletAccount(
val address: String,
val name: String?,
)
@@ -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)
}
@@ -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
)
@@ -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)
}
}
@@ -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"
}
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic
data class DynamicCrossChainTransferFeatures(
val hasDeliveryFee: Boolean,
val supportsXcmExecute: Boolean,
)
@@ -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)
}
@@ -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 }
}
@@ -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
}
@@ -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,
)
@@ -0,0 +1,3 @@
package io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve
typealias TokenReserveId = String
@@ -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()
}
}
@@ -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
}
@@ -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