mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 05:38:02 +00:00
Initial commit: Pezkuwi Wallet Android
Security hardened release: - Code obfuscation enabled (minifyEnabled=true, shrinkResources=true) - Sensitive files excluded (google-services.json, keystores) - Branch.io key moved to BuildConfig placeholder - Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77 - Comprehensive ProGuard rules for crypto wallet - Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,92 @@
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: '../tests.gradle'
|
||||
apply from: '../scripts/secrets.gradle'
|
||||
|
||||
android {
|
||||
|
||||
defaultConfig {
|
||||
|
||||
|
||||
|
||||
buildConfigField "String", "EHTERSCAN_API_KEY_MOONBEAM", readStringSecret("EHTERSCAN_API_KEY_MOONBEAM")
|
||||
buildConfigField "String", "EHTERSCAN_API_KEY_MOONRIVER", readStringSecret("EHTERSCAN_API_KEY_MOONRIVER")
|
||||
buildConfigField "String", "EHTERSCAN_API_KEY_ETHEREUM", readStringSecret("EHTERSCAN_API_KEY_ETHEREUM")
|
||||
|
||||
buildConfigField "String", "LEGACY_CROSS_CHAIN_CONFIG_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/refs/heads/master/xcm/v8/transfers_dev.json\""
|
||||
buildConfigField "String", "DYNAMIC_CROSS_CHAIN_CONFIG_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/refs/heads/master/xcm/v8/transfers_dynamic_dev.json\""
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
buildConfigField "String", "LEGACY_CROSS_CHAIN_CONFIG_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/xcm/v8/transfers.json\""
|
||||
buildConfigField "String", "DYNAMIC_CROSS_CHAIN_CONFIG_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/xcm/v8/transfers_dynamic.json\""
|
||||
}
|
||||
}
|
||||
namespace 'io.novafoundation.nova.feature_wallet_impl'
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core-db')
|
||||
implementation project(':common')
|
||||
implementation project(':feature-wallet-api')
|
||||
implementation project(':feature-account-api')
|
||||
implementation project(':feature-currency-api')
|
||||
implementation project(":feature-swap-core")
|
||||
implementation project(':runtime')
|
||||
implementation project(':feature-xcm:api')
|
||||
|
||||
implementation kotlinDep
|
||||
|
||||
implementation androidDep
|
||||
implementation materialDep
|
||||
implementation cardViewDep
|
||||
implementation constraintDep
|
||||
|
||||
implementation permissionsDep
|
||||
|
||||
implementation coroutinesDep
|
||||
implementation coroutinesAndroidDep
|
||||
implementation viewModelKtxDep
|
||||
implementation liveDataKtxDep
|
||||
implementation lifeCycleKtxDep
|
||||
|
||||
implementation daggerDep
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
ksp daggerCompiler
|
||||
|
||||
implementation roomDep
|
||||
ksp roomCompiler
|
||||
|
||||
implementation lifecycleDep
|
||||
ksp lifecycleCompiler
|
||||
|
||||
implementation bouncyCastleDep
|
||||
|
||||
testImplementation jUnitDep
|
||||
testImplementation mockitoDep
|
||||
|
||||
implementation substrateSdkDep
|
||||
|
||||
implementation gsonDep
|
||||
implementation retrofitDep
|
||||
|
||||
implementation wsDep
|
||||
|
||||
implementation zXingCoreDep
|
||||
implementation zXingEmbeddedDep
|
||||
|
||||
implementation insetterDep
|
||||
|
||||
implementation shimmerDep
|
||||
|
||||
androidTestImplementation androidTestRunnerDep
|
||||
androidTestImplementation androidTestRulesDep
|
||||
androidTestImplementation androidJunitDep
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.mappers
|
||||
|
||||
import io.novafoundation.nova.common.domain.balance.EDCountingMode
|
||||
import io.novafoundation.nova.common.domain.balance.TransferableMode
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
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.core_db.model.AssetWithToken
|
||||
import io.novafoundation.nova.core_db.model.CurrencyLocal
|
||||
import io.novafoundation.nova.core_db.model.TokenLocal
|
||||
import io.novafoundation.nova.core_db.model.TokenWithCurrency
|
||||
import io.novafoundation.nova.feature_currency_api.presentation.mapper.mapCurrencyFromLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRateChange
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
fun mapTokenWithCurrencyToToken(
|
||||
tokenWithCurrency: TokenWithCurrency,
|
||||
chainAsset: Chain.Asset,
|
||||
): Token {
|
||||
return mapTokenLocalToToken(
|
||||
tokenWithCurrency.token ?: TokenLocal.createEmpty(chainAsset.symbol.value, tokenWithCurrency.currency.id),
|
||||
tokenWithCurrency.currency,
|
||||
chainAsset
|
||||
)
|
||||
}
|
||||
|
||||
fun mapTokenLocalToToken(
|
||||
tokenLocal: TokenLocal?,
|
||||
currencyLocal: CurrencyLocal,
|
||||
chainAsset: Chain.Asset,
|
||||
): Token {
|
||||
return Token(
|
||||
currency = mapCurrencyFromLocal(currencyLocal),
|
||||
coinRate = tokenLocal?.recentRateChange?.let { CoinRateChange(tokenLocal.recentRateChange.orZero(), tokenLocal.rate.orZero()) },
|
||||
configuration = chainAsset
|
||||
)
|
||||
}
|
||||
|
||||
fun mapAssetLocalToAsset(
|
||||
assetLocal: AssetWithToken,
|
||||
chainAsset: Chain.Asset
|
||||
): Asset {
|
||||
return with(assetLocal) {
|
||||
Asset(
|
||||
token = mapTokenLocalToToken(token, assetLocal.currency, chainAsset),
|
||||
frozenInPlanks = asset?.frozenInPlanks.orZero(),
|
||||
freeInPlanks = asset?.freeInPlanks.orZero(),
|
||||
reservedInPlanks = asset?.reservedInPlanks.orZero(),
|
||||
bondedInPlanks = asset?.bondedInPlanks.orZero(),
|
||||
unbondingInPlanks = asset?.unbondingInPlanks.orZero(),
|
||||
redeemableInPlanks = asset?.redeemableInPlanks.orZero(),
|
||||
transferableMode = mapTransferableModeFromLocal(asset?.transferableMode),
|
||||
edCountingMode = mapEdCountingModeFromLocal(asset?.edCountingMode)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapTransferableModeFromLocal(modeLocal: TransferableModeLocal?): TransferableMode {
|
||||
return when (modeLocal ?: AssetLocal.defaultTransferableMode()) {
|
||||
TransferableModeLocal.REGULAR -> TransferableMode.REGULAR
|
||||
TransferableModeLocal.HOLDS_AND_FREEZES -> TransferableMode.HOLDS_AND_FREEZES
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapEdCountingModeFromLocal(modeLocal: EDCountingModeLocal?): EDCountingMode {
|
||||
return when (modeLocal ?: AssetLocal.defaultEdCountingMode()) {
|
||||
EDCountingModeLocal.TOTAL -> EDCountingMode.TOTAL
|
||||
EDCountingModeLocal.FREE -> EDCountingMode.FREE
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.mappers.crosschain
|
||||
|
||||
import io.novafoundation.nova.common.utils.asGsonParsedNumber
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.JunctionsRemote
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.MultiLocation.Junction
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.toInterior
|
||||
|
||||
private const val PARENTS = "parents"
|
||||
|
||||
fun mapJunctionsRemoteToMultiLocation(
|
||||
junctionsRemote: JunctionsRemote
|
||||
): RelativeMultiLocation {
|
||||
return if (PARENTS in junctionsRemote) {
|
||||
val parents = junctionsRemote.getValue(PARENTS).asGsonParsedNumber().toInt()
|
||||
val withoutParents = junctionsRemote - PARENTS
|
||||
|
||||
RelativeMultiLocation(
|
||||
parents = parents,
|
||||
interior = mapJunctionsRemoteToInterior(withoutParents)
|
||||
)
|
||||
} else {
|
||||
RelativeMultiLocation(
|
||||
parents = 0,
|
||||
interior = mapJunctionsRemoteToInterior(junctionsRemote)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun JunctionsRemote.toAbsoluteLocation(): AbsoluteMultiLocation {
|
||||
return AbsoluteMultiLocation(mapJunctionsRemoteToInterior(this))
|
||||
}
|
||||
|
||||
private fun mapJunctionsRemoteToInterior(
|
||||
junctionsRemote: JunctionsRemote
|
||||
): MultiLocation.Interior {
|
||||
return junctionsRemote.map { (type, value) -> mapJunctionFromRemote(type, value) }
|
||||
.toInterior()
|
||||
}
|
||||
|
||||
fun mapJunctionFromRemote(type: String, value: Any?): Junction {
|
||||
return when (type) {
|
||||
"parachainId" -> Junction.ParachainId(value.asGsonParsedNumber())
|
||||
"generalKey" -> Junction.GeneralKey(value as String)
|
||||
"palletInstance" -> Junction.PalletInstance(value.asGsonParsedNumber())
|
||||
"generalIndex" -> Junction.GeneralIndex(value.asGsonParsedNumber())
|
||||
else -> throw IllegalArgumentException("Unknown junction type: $type")
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.mappers.crosschain
|
||||
|
||||
import io.novafoundation.nova.common.utils.flattenKeys
|
||||
import io.novafoundation.nova.common.utils.mapToSet
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransfersConfiguration
|
||||
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.TokenReserveConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.TokenReserveRegistry
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.CustomTeleportEntryRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.DynamicCrossChainOriginChainRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.DynamicCrossChainTransfersConfigRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.DynamicReserveLocationRemote
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
|
||||
fun DynamicCrossChainTransfersConfigRemote.toDomain(parachainInfoRepository: ParachainInfoRepository): DynamicCrossChainTransfersConfiguration {
|
||||
return DynamicCrossChainTransfersConfiguration(
|
||||
reserveRegistry = constructReserveRegistry(parachainInfoRepository, assetsLocation, reserveIdOverrides),
|
||||
chains = constructChains(chains),
|
||||
customTeleports = constructCustomTeleports(customTeleports)
|
||||
)
|
||||
}
|
||||
|
||||
private fun constructCustomTeleports(
|
||||
customTeleports: List<CustomTeleportEntryRemote>?
|
||||
): Set<CustomTeleportEntry> {
|
||||
return customTeleports.orEmpty().mapToSet { entry ->
|
||||
with(entry) {
|
||||
CustomTeleportEntry(FullChainAssetId(originChain, originAsset), destChain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructReserveRegistry(
|
||||
parachainInfoRepository: ParachainInfoRepository,
|
||||
assetsLocation: Map<String, DynamicReserveLocationRemote>?,
|
||||
reserveIdOverrides: Map<String, Map<Int, String>>?,
|
||||
): TokenReserveRegistry {
|
||||
return TokenReserveRegistry(
|
||||
parachainInfoRepository = parachainInfoRepository,
|
||||
reservesById = assetsLocation.orEmpty().mapValues { (_, reserve) ->
|
||||
reserve.toDomain()
|
||||
},
|
||||
assetToReserveIdOverrides = reserveIdOverrides.orEmpty().flattenKeys(::FullChainAssetId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun constructChains(
|
||||
chains: List<DynamicCrossChainOriginChainRemote>?
|
||||
): Map<ChainId, List<AssetTransfers>> {
|
||||
return chains.orEmpty().associateBy(
|
||||
keySelector = DynamicCrossChainOriginChainRemote::chainId,
|
||||
valueTransform = ::constructTransfersForChain
|
||||
)
|
||||
}
|
||||
|
||||
private fun constructTransfersForChain(configRemote: DynamicCrossChainOriginChainRemote): List<AssetTransfers> {
|
||||
return configRemote.assets.map { assetConfig ->
|
||||
AssetTransfers(
|
||||
assetId = assetConfig.assetId,
|
||||
destinations = assetConfig.xcmTransfers.map { transfer ->
|
||||
TransferDestination(
|
||||
fullChainAssetId = FullChainAssetId(
|
||||
transfer.getDestinationChainId(),
|
||||
transfer.getDestinationAssetId()
|
||||
),
|
||||
hasDeliveryFee = transfer.hasDeliveryFee ?: false,
|
||||
supportsXcmExecute = transfer.supportsXcmExecute ?: false,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DynamicReserveLocationRemote.toDomain(): TokenReserveConfig {
|
||||
return TokenReserveConfig(
|
||||
reserveChainId = chainId,
|
||||
tokenReserveLocation = multiLocation.toAbsoluteLocation()
|
||||
)
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.mappers.crosschain
|
||||
|
||||
import io.novafoundation.nova.common.utils.asGsonParsedNumber
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.TokenReserveConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.TokenReserveRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.AssetLocationPath
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.DeliveryFeeConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration.AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration.ReserveLocation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration.XcmDestination
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration.XcmFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration.XcmTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.XCMInstructionType
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyXcmTransferMethod
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyCrossChainOriginAssetRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyCrossChainTransfersConfigRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyDeliveryFeeConfigRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyNetworkDeliveryFeeRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyReserveLocationRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyXcmDestinationRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyXcmFeeRemote
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyXcmTransferRemote
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.AbsoluteMultiLocation
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.repository.ParachainInfoRepository
|
||||
|
||||
fun LegacyCrossChainTransfersConfigRemote.toDomain(
|
||||
parachainInfoRepository: ParachainInfoRepository
|
||||
): LegacyCrossChainTransfersConfiguration {
|
||||
val assetsLocations = assetsLocation.orEmpty().mapValues { (_, reserveLocationRemote) ->
|
||||
mapReserveLocationFromRemote(reserveLocationRemote)
|
||||
}
|
||||
|
||||
val feeInstructions = instructions.orEmpty().mapValues { (_, instructionsRemote) ->
|
||||
instructionsRemote.map(::mapXcmInstructionFromRemote)
|
||||
}
|
||||
|
||||
val chains = chains.orEmpty().associateBy(
|
||||
keySelector = { it.chainId },
|
||||
valueTransform = { it.assets.map(::mapAssetTransfersFromRemote) }
|
||||
)
|
||||
|
||||
val networkDeliveryFee = networkDeliveryFee.orEmpty().mapValues { (_, networkDeliveryFeeRemote) ->
|
||||
mapNetworkDeliveryFeeFromRemote(networkDeliveryFeeRemote)
|
||||
}
|
||||
|
||||
return LegacyCrossChainTransfersConfiguration(
|
||||
assetLocations = assetsLocations,
|
||||
feeInstructions = feeInstructions,
|
||||
instructionBaseWeights = networkBaseWeight.orEmpty(),
|
||||
deliveryFeeConfigurations = networkDeliveryFee,
|
||||
chains = chains,
|
||||
reserveRegistry = constructLegacyReserveRegistry(parachainInfoRepository, assetsLocations, chains)
|
||||
)
|
||||
}
|
||||
|
||||
private fun constructLegacyReserveRegistry(
|
||||
parachainInfoRepository: ParachainInfoRepository,
|
||||
assetLocations: Map<String, ReserveLocation>,
|
||||
chains: Map<ChainId, List<AssetTransfers>>
|
||||
): TokenReserveRegistry {
|
||||
return TokenReserveRegistry(
|
||||
parachainInfoRepository = parachainInfoRepository,
|
||||
reservesById = assetLocations.mapValues { (_, reserve) ->
|
||||
TokenReserveConfig(
|
||||
reserveChainId = reserve.chainId,
|
||||
// Legacy config uses relative location for reserve, however in fact they are absolute
|
||||
// I decided to not to refactor it but rather simply perform conversion here in-place
|
||||
tokenReserveLocation = AbsoluteMultiLocation(reserve.multiLocation.interior)
|
||||
)
|
||||
},
|
||||
assetToReserveIdOverrides = buildMap {
|
||||
chains.forEach { (chainId, chainAssets) ->
|
||||
chainAssets.map { chainAssetConfig ->
|
||||
val key = FullChainAssetId(chainId, chainAssetConfig.assetId)
|
||||
// We could check that the `assetLocation` differs from the asset symbol to avoid placing redundant overrides...
|
||||
// But we don't since it does not matter much anyway
|
||||
put(key, chainAssetConfig.assetLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapNetworkDeliveryFeeFromRemote(networkDeliveryFeeRemote: LegacyNetworkDeliveryFeeRemote): DeliveryFeeConfiguration {
|
||||
return DeliveryFeeConfiguration(
|
||||
toParent = mapDeliveryFeeConfigFromRemote(networkDeliveryFeeRemote.toParent),
|
||||
toParachain = mapDeliveryFeeConfigFromRemote(networkDeliveryFeeRemote.toParachain)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapDeliveryFeeConfigFromRemote(config: LegacyDeliveryFeeConfigRemote?): DeliveryFeeConfiguration.Type? {
|
||||
if (config == null) return null
|
||||
|
||||
return when (config.type) {
|
||||
"exponential" -> DeliveryFeeConfiguration.Type.Exponential(
|
||||
factorPallet = config.factorPallet,
|
||||
sizeBase = config.sizeBase,
|
||||
sizeFactor = config.sizeFactor,
|
||||
alwaysHoldingPays = config.alwaysHoldingPays ?: false
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown delivery fee config type: ${config.type}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapReserveLocationFromRemote(reserveLocationRemote: LegacyReserveLocationRemote): ReserveLocation {
|
||||
return ReserveLocation(
|
||||
chainId = reserveLocationRemote.chainId,
|
||||
reserveFee = reserveLocationRemote.reserveFee?.let(::mapXcmFeeFromRemote),
|
||||
multiLocation = mapJunctionsRemoteToMultiLocation(reserveLocationRemote.multiLocation)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapAssetTransfersFromRemote(remote: LegacyCrossChainOriginAssetRemote): AssetTransfers {
|
||||
val assetLocationPath = when (remote.assetLocationPath.type) {
|
||||
"absolute" -> AssetLocationPath.Absolute
|
||||
"relative" -> AssetLocationPath.Relative
|
||||
"concrete" -> {
|
||||
val junctionsRemote = remote.assetLocationPath.path!!
|
||||
|
||||
AssetLocationPath.Concrete(mapJunctionsRemoteToMultiLocation(junctionsRemote))
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown asset type")
|
||||
}
|
||||
|
||||
return AssetTransfers(
|
||||
assetId = remote.assetId,
|
||||
assetLocationPath = assetLocationPath,
|
||||
assetLocation = remote.assetLocation,
|
||||
xcmTransfers = remote.xcmTransfers.map(::mapXcmTransferFromRemote)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapXcmTransferFromRemote(remote: LegacyXcmTransferRemote): XcmTransfer {
|
||||
return XcmTransfer(
|
||||
destination = mapXcmDestinationFromRemote(remote.destination),
|
||||
type = mapXcmTransferTypeFromRemote(remote.type)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapXcmTransferTypeFromRemote(remote: String): LegacyXcmTransferMethod {
|
||||
return when (remote) {
|
||||
"xtokens" -> LegacyXcmTransferMethod.X_TOKENS
|
||||
"xcmpallet" -> LegacyXcmTransferMethod.XCM_PALLET_RESERVE
|
||||
"xcmpallet-teleport" -> LegacyXcmTransferMethod.XCM_PALLET_TELEPORT
|
||||
"xcmpallet-transferAssets" -> LegacyXcmTransferMethod.XCM_PALLET_TRANSFER_ASSETS
|
||||
else -> LegacyXcmTransferMethod.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapXcmDestinationFromRemote(remote: LegacyXcmDestinationRemote): XcmDestination {
|
||||
return XcmDestination(
|
||||
chainId = remote.chainId,
|
||||
assetId = remote.assetId,
|
||||
fee = mapXcmFeeFromRemote(remote.fee)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapXcmFeeFromRemote(
|
||||
remote: LegacyXcmFeeRemote
|
||||
): XcmFee<String> {
|
||||
val mode = when (remote.mode.type) {
|
||||
"proportional" -> XcmFee.Mode.Proportional(remote.mode.value.asGsonParsedNumber())
|
||||
"standard" -> XcmFee.Mode.Standard
|
||||
else -> XcmFee.Mode.Unknown
|
||||
}
|
||||
|
||||
return XcmFee(
|
||||
mode = mode,
|
||||
instructions = remote.instructions
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapXcmInstructionFromRemote(instruction: String): XCMInstructionType = runCatching {
|
||||
enumValueOf<XCMInstructionType>(instruction)
|
||||
}.getOrDefault(XCMInstructionType.UNKNOWN)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.AccountInfoRepository
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.queryNonNull
|
||||
import io.novafoundation.nova.runtime.storage.typed.account
|
||||
import io.novafoundation.nova.runtime.storage.typed.system
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
@FeatureScope
|
||||
internal class RealAccountInfoRepository @Inject constructor(
|
||||
@Named(REMOTE_STORAGE_SOURCE) private val remoteStorageSource: StorageDataSource,
|
||||
) : AccountInfoRepository {
|
||||
|
||||
override suspend fun getAccountInfo(
|
||||
chainId: ChainId,
|
||||
accountId: AccountId,
|
||||
): AccountInfo {
|
||||
return remoteStorageSource.query(chainId, applyStorageDefault = true) {
|
||||
metadata.system.account.queryNonNull(accountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.api
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.StatemineAssetDetails
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine.bindAssetDetails
|
||||
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
|
||||
|
||||
typealias UntypedAssetsAssetId = Any
|
||||
|
||||
@JvmInline
|
||||
value class AssetsApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
fun RuntimeMetadata.assets(palletName: String): AssetsApi {
|
||||
return AssetsApi(module(palletName))
|
||||
}
|
||||
|
||||
context(StorageQueryContext)
|
||||
val AssetsApi.asset: QueryableStorageEntry1<UntypedAssetsAssetId, StatemineAssetDetails>
|
||||
get() = storage1("Asset", binding = { decoded, _ -> bindAssetDetails(decoded) })
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets
|
||||
|
||||
import dagger.Lazy
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSource
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector
|
||||
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
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common.orml.OrmlAssetSourceFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.UnsupportedEventDetector
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.evmErc20.EvmErc20EventDetectorFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml.OrmlAssetEventDetectorFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.statemine.StatemineAssetEventDetectorFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility.NativeAssetEventDetector
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class StaticAssetSource(
|
||||
override val transfers: AssetTransfers,
|
||||
override val balance: AssetBalance,
|
||||
override val history: AssetHistory,
|
||||
) : AssetSource
|
||||
|
||||
// Use lazy to resolve possible circular dependencies
|
||||
class TypeBasedAssetSourceRegistry(
|
||||
private val nativeSource: Lazy<AssetSource>,
|
||||
private val statemineSource: Lazy<AssetSource>,
|
||||
private val ormlSourceFactory: Lazy<OrmlAssetSourceFactory>,
|
||||
private val evmErc20Source: Lazy<AssetSource>,
|
||||
private val evmNativeSource: Lazy<AssetSource>,
|
||||
private val equilibriumAssetSource: Lazy<AssetSource>,
|
||||
private val unsupportedBalanceSource: AssetSource,
|
||||
|
||||
private val nativeAssetEventDetector: NativeAssetEventDetector,
|
||||
private val ormlAssetEventDetectorFactory: OrmlAssetEventDetectorFactory,
|
||||
private val statemineAssetEventDetectorFactory: StatemineAssetEventDetectorFactory,
|
||||
private val erc20EventDetectorFactory: EvmErc20EventDetectorFactory
|
||||
) : AssetSourceRegistry {
|
||||
|
||||
override fun sourceFor(chainAsset: Chain.Asset): AssetSource {
|
||||
return when (val type = chainAsset.type) {
|
||||
is Chain.Asset.Type.Native -> nativeSource.get()
|
||||
is Chain.Asset.Type.Statemine -> statemineSource.get()
|
||||
is Chain.Asset.Type.Orml -> ormlSourceFactory.get().getSourceBySubtype(type.subType)
|
||||
is Chain.Asset.Type.EvmErc20 -> evmErc20Source.get()
|
||||
is Chain.Asset.Type.EvmNative -> evmNativeSource.get()
|
||||
is Chain.Asset.Type.Equilibrium -> equilibriumAssetSource.get()
|
||||
Chain.Asset.Type.Unsupported -> unsupportedBalanceSource
|
||||
}
|
||||
}
|
||||
|
||||
override fun allSources(): List<AssetSource> {
|
||||
return buildList {
|
||||
add(nativeSource.get())
|
||||
add(statemineSource.get())
|
||||
addAll(ormlSourceFactory.get().allSources())
|
||||
add(evmNativeSource.get())
|
||||
add(evmErc20Source.get())
|
||||
add(equilibriumAssetSource.get())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getEventDetector(chainAsset: Chain.Asset): AssetEventDetector {
|
||||
return when (chainAsset.type) {
|
||||
is Chain.Asset.Type.Equilibrium,
|
||||
Chain.Asset.Type.EvmNative,
|
||||
|
||||
Chain.Asset.Type.Unsupported -> UnsupportedEventDetector()
|
||||
|
||||
is Chain.Asset.Type.Statemine -> statemineAssetEventDetectorFactory.create(chainAsset)
|
||||
|
||||
is Chain.Asset.Type.Orml -> ormlAssetEventDetectorFactory.create(chainAsset)
|
||||
|
||||
Chain.Asset.Type.Native -> nativeAssetEventDetector
|
||||
|
||||
is Chain.Asset.Type.EvmErc20 -> erc20EventDetectorFactory.create(chainAsset)
|
||||
}
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindString
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.cast
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.utils.second
|
||||
import io.novafoundation.nova.core_db.dao.LockDao
|
||||
import io.novafoundation.nova.core_db.model.BalanceLockLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLockId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
class BlockchainLock(
|
||||
val id: BalanceLockId,
|
||||
val amount: Balance
|
||||
)
|
||||
|
||||
@HelperBinding
|
||||
fun bindEquilibriumBalanceLocks(dynamicInstance: Any?): List<BlockchainLock>? {
|
||||
if (dynamicInstance == null) return null
|
||||
|
||||
return bindList(dynamicInstance) { items ->
|
||||
val item = items.castToList()
|
||||
BlockchainLock(
|
||||
bindLockIdString(item.first().cast()),
|
||||
bindNumber(item.second().cast())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@HelperBinding
|
||||
fun bindBalanceLocks(dynamicInstance: Any?): List<BlockchainLock> {
|
||||
if (dynamicInstance == null) return emptyList()
|
||||
|
||||
return bindList(dynamicInstance) {
|
||||
BlockchainLock(
|
||||
bindLockIdString(it.castToStruct()["id"]),
|
||||
bindNumber(it.castToStruct()["amount"])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun bindBalanceFreezes(dynamicInstance: Any?): List<BlockchainLock> {
|
||||
if (dynamicInstance == null) return emptyList()
|
||||
|
||||
return bindList(dynamicInstance) { item ->
|
||||
val asStruct = item.castToStruct()
|
||||
|
||||
BlockchainLock(
|
||||
bindFreezeId(asStruct["id"]),
|
||||
bindNumber(asStruct["amount"])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindFreezeId(dynamicInstance: Any?): BalanceLockId {
|
||||
val asEnum = dynamicInstance.castToDictEnum()
|
||||
val module = asEnum.name
|
||||
val moduleReason = asEnum.value.castToDictEnum().name
|
||||
|
||||
return BalanceLockId.fromPath(module, moduleReason)
|
||||
}
|
||||
|
||||
private fun bindLockIdString(dynamicInstance: Any?): BalanceLockId {
|
||||
val asString = bindString(dynamicInstance)
|
||||
return BalanceLockId.fromFullId(asString)
|
||||
}
|
||||
|
||||
fun mapBlockchainLockToLocal(
|
||||
metaId: Long,
|
||||
chainId: ChainId,
|
||||
assetId: ChainAssetId,
|
||||
lock: BlockchainLock
|
||||
): BalanceLockLocal {
|
||||
return BalanceLockLocal(metaId, chainId, assetId, lock.id.value, lock.amount)
|
||||
}
|
||||
|
||||
suspend fun LockDao.updateLocks(locks: List<BlockchainLock>, metaId: Long, chainId: ChainId, chainAssetId: ChainAssetId) {
|
||||
val balanceLocksLocal = locks.map { mapBlockchainLockToLocal(metaId, chainId, chainAssetId, it) }
|
||||
updateLocks(balanceLocksLocal, metaId, chainId, chainAssetId)
|
||||
}
|
||||
|
||||
suspend fun LockDao.updateLock(lock: BlockchainLock, metaId: Long, chainId: ChainId, chainAssetId: ChainAssetId) {
|
||||
val balanceLocksLocal = mapBlockchainLockToLocal(metaId, chainId, chainAssetId, lock)
|
||||
updateLocks(listOf(balanceLocksLocal), metaId, chainId, chainAssetId)
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances
|
||||
|
||||
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.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdatePoint
|
||||
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
|
||||
|
||||
class UnsupportedAssetBalance : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
) = unsupported()
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset) = unsupported()
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset) = unsupported()
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId) = unsupported()
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> = unsupported()
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
private fun unsupported(): Nothing = throw UnsupportedOperationException("Unsupported balance source")
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.equilibrium
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.HelperBinding
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.cast
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.getList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.returnType
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.combine
|
||||
import io.novafoundation.nova.common.utils.constantOrNull
|
||||
import io.novafoundation.nova.common.utils.decodeValue
|
||||
import io.novafoundation.nova.common.utils.eqBalances
|
||||
import io.novafoundation.nova.common.utils.getAs
|
||||
import io.novafoundation.nova.common.utils.hasUpdated
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.utils.second
|
||||
import io.novafoundation.nova.common.utils.system
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core_db.dao.AssetDao
|
||||
import io.novafoundation.nova.core_db.dao.LockDao
|
||||
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_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
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_impl.data.network.blockchain.assets.balances.bindEquilibriumBalanceLocks
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.updateLocks
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.ext.requireEquilibrium
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.multiNetwork.withRuntime
|
||||
import io.novafoundation.nova.runtime.network.binding.number
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.StorageEntry
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
|
||||
class EquilibriumAssetBalance(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetCache: AssetCache,
|
||||
private val lockDao: LockDao,
|
||||
private val assetDao: AssetDao,
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
) : AssetBalance {
|
||||
|
||||
private class ReservedAssetBalanceWithBlock(val assetId: Int, val reservedBalance: BigInteger, val block: BlockHash)
|
||||
|
||||
private class FreeAssetBalancesWithBlock(val lock: BigInteger?, val assets: List<FreeAssetBalance>)
|
||||
|
||||
private class FreeAssetBalance(val assetId: Int, val balance: BigInteger)
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
if (!chainAsset.isUtilityAsset) return emptyFlow<Unit>()
|
||||
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
val storage = runtime.metadata.eqBalances().storage("Locked")
|
||||
val key = storage.storageKey(runtime, accountId)
|
||||
|
||||
return subscriptionBuilder.subscribe(key)
|
||||
.map { change ->
|
||||
val balanceLocks = bindEquilibriumBalanceLocks(storage.decodeValue(change.value, runtime)).orEmpty()
|
||||
lockDao.updateLocks(balanceLocks, metaAccount.id, chain.id, chainAsset.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
return if (chainAsset.isUtilityAsset) {
|
||||
chainRegistry.withRuntime(chainAsset.chainId) {
|
||||
runtime.metadata.eqBalances().constantOrNull("ExistentialDepositBasic")?.getAs(number())
|
||||
.orZero()
|
||||
}
|
||||
} else {
|
||||
BigInteger.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
val assetBalances = remoteStorageSource.query(
|
||||
chain.id,
|
||||
keyBuilder = { it.getAccountStorage().storageKey(it, accountId) },
|
||||
binding = { scale, runtimeSnapshot -> bindEquilibriumBalances(chain, scale, runtimeSnapshot) }
|
||||
)
|
||||
|
||||
val onChainAssetId = chainAsset.requireEquilibrium().id
|
||||
val reservedBalance = remoteStorageSource.query(
|
||||
chain.id,
|
||||
keyBuilder = { it.getReservedStorage().storageKey(it, accountId, onChainAssetId) },
|
||||
binding = { scale, runtimeSnapshot -> bindReservedBalance(scale, runtimeSnapshot) }
|
||||
)
|
||||
|
||||
val assetBalance = assetBalances.assets
|
||||
.firstOrNull { it.assetId == chainAsset.id }
|
||||
?.balance
|
||||
.orZero()
|
||||
|
||||
val lockedBalance = assetBalances.lock.orZero().takeIf { chainAsset.isUtilityAsset } ?: BigInteger.ZERO
|
||||
|
||||
return ChainAssetBalance.default(
|
||||
chainAsset = chainAsset,
|
||||
free = assetBalance,
|
||||
reserved = reservedBalance,
|
||||
frozen = lockedBalance
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
if (!chainAsset.isUtilityAsset) return emptyFlow()
|
||||
|
||||
val assetBalancesFlow = subscriptionBuilder.subscribeOnSystemAccount(chain, accountId)
|
||||
val reservedBalanceFlow = subscriptionBuilder.subscribeOnReservedBalance(chain, accountId)
|
||||
|
||||
var oldBlockHash: String? = null
|
||||
|
||||
return combine(assetBalancesFlow, reservedBalanceFlow) { (blockHash, assetBalances), reservedBalancesWithBlocks ->
|
||||
val freeByAssetId = assetBalances.assets.associateBy { it.assetId }
|
||||
val reservedByAssetId = reservedBalancesWithBlocks.associateBy { it.assetId }
|
||||
|
||||
val diff = assetCache.updateAssetsByChain(metaAccount, chain) { asset: Chain.Asset ->
|
||||
val free = freeByAssetId[asset.id]?.balance.orZero()
|
||||
val reserved = reservedByAssetId[asset.id]?.reservedBalance.orZero()
|
||||
val locks = if (asset.isUtilityAsset) assetBalances.lock.orZero() else BigInteger.ZERO
|
||||
AssetLocal(
|
||||
assetId = asset.id,
|
||||
chainId = asset.chainId,
|
||||
metaId = metaAccount.id,
|
||||
freeInPlanks = free,
|
||||
reservedInPlanks = reserved,
|
||||
frozenInPlanks = locks,
|
||||
transferableMode = TransferableModeLocal.REGULAR,
|
||||
edCountingMode = EDCountingModeLocal.TOTAL,
|
||||
redeemableInPlanks = BigInteger.ZERO,
|
||||
bondedInPlanks = BigInteger.ZERO,
|
||||
unbondingInPlanks = BigInteger.ZERO
|
||||
)
|
||||
}
|
||||
|
||||
if (diff.hasUpdated() && oldBlockHash != blockHash) {
|
||||
oldBlockHash = blockHash
|
||||
BalanceSyncUpdate.CauseFetchable(blockHash)
|
||||
} else {
|
||||
BalanceSyncUpdate.NoCause
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun SharedRequestsBuilder.subscribeOnSystemAccount(chain: Chain, accountId: AccountId): Flow<Pair<BlockHash, FreeAssetBalancesWithBlock>> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
val key = try {
|
||||
runtime.getAccountStorage().storageKey(runtime, accountId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Failed to construct account storage key: ${e.message} in ${chain.name}")
|
||||
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
return subscribe(key)
|
||||
.map { it.block to bindEquilibriumBalances(chain, it.value, runtime) }
|
||||
}
|
||||
|
||||
private suspend fun SharedRequestsBuilder.subscribeOnReservedBalance(chain: Chain, accountId: AccountId): Flow<List<ReservedAssetBalanceWithBlock>> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
return chain.assets
|
||||
.filter { it.type is Chain.Asset.Type.Equilibrium }
|
||||
.map { asset ->
|
||||
val equilibriumType = asset.requireEquilibrium()
|
||||
|
||||
val key = try {
|
||||
runtime.getReservedStorage().storageKey(runtime, accountId, equilibriumType.id)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Failed to construct key: ${e.message} in ${chain.name}")
|
||||
|
||||
return@map flowOf(null)
|
||||
}
|
||||
|
||||
subscribe(key)
|
||||
.map { ReservedAssetBalanceWithBlock(asset.id, bindReservedBalance(it.value, runtime), it.block) }
|
||||
.catch<ReservedAssetBalanceWithBlock?> { emit(null) }
|
||||
}.combine()
|
||||
.map { it.filterNotNull() }
|
||||
}
|
||||
|
||||
private fun bindReservedBalance(raw: String?, runtime: RuntimeSnapshot): BigInteger {
|
||||
val type = runtime.getReservedStorage().returnType()
|
||||
|
||||
return raw?.let { type.fromHexOrNull(runtime, it).cast<BigInteger>() } ?: BigInteger.ZERO
|
||||
}
|
||||
|
||||
@UseCaseBinding
|
||||
private fun bindEquilibriumBalances(chain: Chain, scale: String?, runtime: RuntimeSnapshot): FreeAssetBalancesWithBlock {
|
||||
if (scale == null) {
|
||||
return FreeAssetBalancesWithBlock(null, emptyList())
|
||||
}
|
||||
|
||||
val type = runtime.getAccountStorage().returnType()
|
||||
val data = type.fromHexOrNull(runtime, scale)
|
||||
.castToStruct()
|
||||
.get<Any>("data").castToDictEnum()
|
||||
.value
|
||||
.castToStruct()
|
||||
|
||||
val lock = data.get<BigInteger>("lock")
|
||||
val balances = data.getList("balance")
|
||||
|
||||
val onChainAssetIdToAsset = chain.assets
|
||||
.associateBy { it.requireEquilibrium().id }
|
||||
|
||||
val assetBalances = balances.mapNotNull { assetBalance ->
|
||||
val (onChainAssetId, balance) = bindAssetBalance(assetBalance.castToList())
|
||||
val asset = onChainAssetIdToAsset[onChainAssetId]
|
||||
|
||||
asset?.let { FreeAssetBalance(it.id, balance) }
|
||||
}
|
||||
|
||||
return FreeAssetBalancesWithBlock(lock, assetBalances)
|
||||
}
|
||||
|
||||
@HelperBinding
|
||||
private fun bindAssetBalance(dynamicInstance: List<Any?>): Pair<BigInteger, BigInteger> {
|
||||
val onChainAssetId = bindNumber(dynamicInstance.first())
|
||||
val balance = dynamicInstance.second().castToDictEnum()
|
||||
val amount = if (balance.name == "Positive") {
|
||||
bindNumber(balance.value)
|
||||
} else {
|
||||
BigInteger.ZERO
|
||||
}
|
||||
return onChainAssetId to amount
|
||||
}
|
||||
|
||||
private fun RuntimeSnapshot.getAccountStorage(): StorageEntry {
|
||||
return metadata.system().storage("Account")
|
||||
}
|
||||
|
||||
private fun RuntimeSnapshot.getReservedStorage(): StorageEntry {
|
||||
return metadata.eqBalances().storage("Reserved")
|
||||
}
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.evmErc20
|
||||
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
import io.novafoundation.nova.core.ethereum.log.Topic
|
||||
import io.novafoundation.nova.core.updater.EthereumSharedRequestsBuilder
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core.updater.callApiOrThrow
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.updateNonLockableAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
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.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.queryBatched
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.base.querySingle
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Queries
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.requireErc20
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getSubscriptionEthereumApiOrThrow
|
||||
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumAddress
|
||||
import io.novasama.substrate_sdk_android.extensions.toAccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.abi.TypeEncoder
|
||||
import org.web3j.abi.datatypes.Address
|
||||
import org.web3j.protocol.websocket.events.Log
|
||||
import org.web3j.protocol.websocket.events.LogNotification
|
||||
import java.math.BigInteger
|
||||
|
||||
private const val BATCH_ID = "EvmAssetBalance.InitialBalance"
|
||||
|
||||
class EvmErc20AssetBalance(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetCache: AssetCache,
|
||||
private val erc20Standard: Erc20Standard,
|
||||
private val rpcCalls: RpcCalls,
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
// ERC20 tokens doe not support locks
|
||||
return emptyFlow<Nothing>()
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
// ERC20 tokens do not have ED
|
||||
return BigInteger.ZERO
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
val erc20Type = chainAsset.requireErc20()
|
||||
val ethereumApi = chainRegistry.getCallEthereumApiOrThrow(chain.id)
|
||||
val accountAddress = chain.addressOf(accountId)
|
||||
val balance = erc20Standard.querySingle(erc20Type.contractAddress, ethereumApi)
|
||||
.balanceOfAsync(accountAddress)
|
||||
.await()
|
||||
return ChainAssetBalance.fromFree(chainAsset, free = balance)
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
val ethereumApi = chainRegistry.getSubscriptionEthereumApiOrThrow(chain.id)
|
||||
|
||||
val address = chain.addressOf(accountId)
|
||||
val erc20Type = chainAsset.requireErc20()
|
||||
|
||||
return merge(
|
||||
ethereumApi.incomingErcTransfersFlow(address, erc20Type.contractAddress),
|
||||
ethereumApi.outComingErcTransfersFlow(address, erc20Type.contractAddress)
|
||||
).mapLatest { logNotification ->
|
||||
val blockNumber = logNotification.params.result.parsedBlockNumber()
|
||||
val substrateHash = rpcCalls.getBlockHash(chain.id, blockNumber)
|
||||
|
||||
TransferableBalanceUpdatePoint(updatedAt = substrateHash)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
val address = chain.addressOf(accountId)
|
||||
|
||||
val erc20Type = chainAsset.requireErc20()
|
||||
|
||||
val initialBalanceAsync = erc20Standard.queryBatched(erc20Type.contractAddress, BATCH_ID, subscriptionBuilder)
|
||||
.balanceOfAsync(address)
|
||||
|
||||
return subscriptionBuilder.erc20BalanceFlow(address, chainAsset, initialBalanceAsync)
|
||||
.map { balanceUpdate ->
|
||||
assetCache.updateNonLockableAsset(metaAccount.id, chainAsset, balanceUpdate.newBalance)
|
||||
|
||||
if (balanceUpdate.cause != null) {
|
||||
BalanceSyncUpdate.CauseFetched(balanceUpdate.cause)
|
||||
} else {
|
||||
BalanceSyncUpdate.NoCause
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun EthereumSharedRequestsBuilder.erc20BalanceFlow(
|
||||
account: String,
|
||||
chainAsset: Chain.Asset,
|
||||
initialBalanceAsync: Deferred<BigInteger>
|
||||
): Flow<Erc20BalanceUpdate> {
|
||||
val contractAddress = chainAsset.requireErc20().contractAddress
|
||||
|
||||
val changes = accountErcTransfersFlow(account, contractAddress, chainAsset).map { erc20Transfer ->
|
||||
val newBalance = erc20Standard.querySingle(contractAddress, callApiOrThrow)
|
||||
.balanceOfAsync(account)
|
||||
.await()
|
||||
|
||||
Erc20BalanceUpdate(newBalance, cause = erc20Transfer)
|
||||
}
|
||||
|
||||
return flow {
|
||||
val initialBalance = initialBalanceAsync.await()
|
||||
|
||||
emit(Erc20BalanceUpdate(initialBalance, cause = null))
|
||||
|
||||
emitAll(changes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Web3Api.incomingErcTransfersFlow(
|
||||
accountAddress: String,
|
||||
contractAddress: String,
|
||||
): Flow<LogNotification> {
|
||||
return logsNotifications(
|
||||
addresses = listOf(contractAddress),
|
||||
topics = createErc20ReceiveTopics(accountAddress)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Web3Api.outComingErcTransfersFlow(
|
||||
accountAddress: String,
|
||||
contractAddress: String,
|
||||
): Flow<LogNotification> {
|
||||
return logsNotifications(
|
||||
addresses = listOf(contractAddress),
|
||||
topics = createErc20SendTopics(accountAddress)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createErc20ReceiveTopics(accountAddress: String): List<Topic> {
|
||||
val addressTopic = TypeEncoder.encode(Address(accountAddress))
|
||||
val transferEventSignature = EventEncoder.encode(Erc20Queries.TRANSFER_EVENT)
|
||||
|
||||
return createErc20ReceiveTopics(transferEventSignature, addressTopic)
|
||||
}
|
||||
|
||||
private fun createErc20ReceiveTopics(
|
||||
transferEventSignature: String,
|
||||
addressTopic: String,
|
||||
): List<Topic> {
|
||||
return listOf(
|
||||
Topic.Single(transferEventSignature), // zero-th topic is event signature
|
||||
Topic.Any, // anyone is `from`
|
||||
Topic.AnyOf(addressTopic) // our account as `to`
|
||||
)
|
||||
}
|
||||
|
||||
private fun createErc20SendTopics(accountAddress: String): List<Topic> {
|
||||
val addressTopic = TypeEncoder.encode(Address(accountAddress))
|
||||
val transferEventSignature = EventEncoder.encode(Erc20Queries.TRANSFER_EVENT)
|
||||
|
||||
return createErc20SendTopics(transferEventSignature, addressTopic)
|
||||
}
|
||||
|
||||
private fun createErc20SendTopics(
|
||||
transferEventSignature: String,
|
||||
addressTopic: String,
|
||||
): List<Topic> {
|
||||
return listOf(
|
||||
Topic.Single(transferEventSignature), // zero-th topic is event signature
|
||||
Topic.AnyOf(addressTopic), // our account as `from`
|
||||
)
|
||||
}
|
||||
|
||||
private fun EthereumSharedRequestsBuilder.accountErcTransfersFlow(
|
||||
accountAddress: String,
|
||||
contractAddress: String,
|
||||
chainAsset: Chain.Asset,
|
||||
): Flow<RealtimeHistoryUpdate> {
|
||||
val addressTopic = TypeEncoder.encode(Address(accountAddress))
|
||||
|
||||
val transferEvent = Erc20Queries.TRANSFER_EVENT
|
||||
val transferEventSignature = EventEncoder.encode(transferEvent)
|
||||
|
||||
val erc20SendTopic = createErc20SendTopics(transferEventSignature, addressTopic)
|
||||
val erc20ReceiveTopic = createErc20ReceiveTopics(transferEventSignature, addressTopic)
|
||||
|
||||
val receiveTransferNotifications = subscribeEthLogs(contractAddress, erc20ReceiveTopic)
|
||||
val sendTransferNotifications = subscribeEthLogs(contractAddress, erc20SendTopic)
|
||||
|
||||
val transferNotifications = merge(receiveTransferNotifications, sendTransferNotifications)
|
||||
|
||||
return transferNotifications.map { logNotification ->
|
||||
val log = logNotification.params.result
|
||||
val event = Erc20Queries.parseTransferEvent(log)
|
||||
|
||||
RealtimeHistoryUpdate(
|
||||
status = Operation.Status.COMPLETED,
|
||||
txHash = log.transactionHash,
|
||||
type = RealtimeHistoryUpdate.Type.Transfer(
|
||||
senderId = event.from.accountId(),
|
||||
recipientId = event.to.accountId(),
|
||||
amountInPlanks = event.amount.value,
|
||||
chainAsset = chainAsset,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Log.parsedBlockNumber(): BigInteger {
|
||||
return BigInteger(blockNumber.removeHexPrefix(), 16)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Address.accountId() = value.asEthereumAddress().toAccountId().value
|
||||
|
||||
private class Erc20BalanceUpdate(
|
||||
val newBalance: Balance,
|
||||
val cause: RealtimeHistoryUpdate?
|
||||
)
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.evmNative
|
||||
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
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.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.updateNonLockableAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
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.types.Balance
|
||||
import io.novafoundation.nova.runtime.ethereum.sendSuspend
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getSubscriptionEthereumApiOrThrow
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import org.web3j.protocol.core.DefaultBlockParameterName
|
||||
import java.math.BigInteger
|
||||
|
||||
class EvmNativeAssetBalance(
|
||||
private val assetCache: AssetCache,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
// Evm native tokens doe not support locks
|
||||
return emptyFlow<Nothing>()
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
// Evm native tokens do not have ED
|
||||
return BigInteger.ZERO
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
val ethereumApi = chainRegistry.getCallEthereumApiOrThrow(chain.id)
|
||||
|
||||
val balance = ethereumApi.getLatestNativeBalance(chain.addressOf(accountId))
|
||||
return ChainAssetBalance.fromFree(chainAsset, balance)
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
val subscriptionApi = chainRegistry.getSubscriptionEthereumApiOrThrow(chain.id)
|
||||
val callApi = chainRegistry.getCallEthereumApiOrThrow(chain.id)
|
||||
|
||||
val address = chain.addressOf(accountId)
|
||||
|
||||
return balanceSyncUpdateFlow(address, subscriptionApi, callApi).map { balanceUpdate ->
|
||||
assetCache.updateNonLockableAsset(metaAccount.id, chainAsset, balanceUpdate.newBalance)
|
||||
|
||||
balanceUpdate.syncUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private fun balanceSyncUpdateFlow(
|
||||
address: String,
|
||||
subscriptionApi: Web3Api,
|
||||
callApi: Web3Api
|
||||
): Flow<EvmNativeBalanceUpdate> {
|
||||
return flow {
|
||||
val initialBalance = callApi.getLatestNativeBalance(address)
|
||||
emit(EvmNativeBalanceUpdate(initialBalance, BalanceSyncUpdate.NoCause))
|
||||
|
||||
var currentBalance = initialBalance
|
||||
|
||||
val realtimeUpdates = subscriptionApi.newHeadsFlow().transform { newHead ->
|
||||
val blockHash = newHead.params.result.hash
|
||||
val newBalance = callApi.getLatestNativeBalance(address)
|
||||
|
||||
if (newBalance != currentBalance) {
|
||||
currentBalance = newBalance
|
||||
val update = EvmNativeBalanceUpdate(newBalance, BalanceSyncUpdate.CauseFetchable(blockHash))
|
||||
emit(update)
|
||||
}
|
||||
}
|
||||
|
||||
emitAll(realtimeUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Web3Api.getLatestNativeBalance(address: String): Balance {
|
||||
return ethGetBalance(address, DefaultBlockParameterName.LATEST).sendSuspend().balance
|
||||
}
|
||||
}
|
||||
|
||||
private class EvmNativeBalanceUpdate(
|
||||
val newBalance: Balance,
|
||||
val syncUpdate: BalanceSyncUpdate,
|
||||
)
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindOrmlAccountBalanceOrEmpty
|
||||
import io.novafoundation.nova.common.utils.decodeValue
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
import io.novafoundation.nova.common.utils.tokens
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core_db.dao.LockDao
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal.EDCountingModeLocal
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal.TransferableModeLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
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_impl.data.network.blockchain.assets.balances.bindBalanceLocks
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.updateLocks
|
||||
import io.novafoundation.nova.runtime.ext.ormlCurrencyId
|
||||
import io.novafoundation.nova.runtime.ext.requireOrml
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
|
||||
class OrmlAssetBalance(
|
||||
private val assetCache: AssetCache,
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val lockDao: LockDao
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
val storage = runtime.metadata.tokens().storage("Locks")
|
||||
|
||||
val currencyId = chainAsset.ormlCurrencyId(runtime)
|
||||
val key = storage.storageKey(runtime, accountId, currencyId)
|
||||
|
||||
return subscriptionBuilder.subscribe(key)
|
||||
.map { change ->
|
||||
val balanceLocks = bindBalanceLocks(storage.decodeValue(change.value, runtime))
|
||||
lockDao.updateLocks(balanceLocks, metaAccount.id, chain.id, chainAsset.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
return chainAsset.requireOrml().existentialDeposit
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
val balance = remoteStorageSource.query(
|
||||
chainId = chain.id,
|
||||
keyBuilder = { it.ormlBalanceKey(accountId, chainAsset) },
|
||||
binding = { scale, runtime -> bindOrmlAccountBalanceOrEmpty(scale, runtime) }
|
||||
)
|
||||
|
||||
return ChainAssetBalance.default(chainAsset, balance)
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
return remoteStorageSource.subscribe(chain.id) {
|
||||
metadata.tokens().storage("Accounts").observeWithRaw(
|
||||
accountId,
|
||||
chainAsset.ormlCurrencyId(runtime),
|
||||
binding = ::bindOrmlAccountBalanceOrEmpty
|
||||
).map {
|
||||
TransferableBalanceUpdatePoint(it.at!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
return subscriptionBuilder.subscribe(runtime.ormlBalanceKey(accountId, chainAsset))
|
||||
.map {
|
||||
val ormlAccountData = bindOrmlAccountBalanceOrEmpty(it.value, runtime)
|
||||
|
||||
val assetChanged = updateAssetBalance(metaAccount.id, chainAsset, ormlAccountData)
|
||||
|
||||
if (assetChanged) {
|
||||
BalanceSyncUpdate.CauseFetchable(it.block)
|
||||
} else {
|
||||
BalanceSyncUpdate.NoCause
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAssetBalance(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset,
|
||||
ormlAccountData: AccountBalance
|
||||
) = assetCache.updateAsset(metaId, chainAsset) { local ->
|
||||
with(ormlAccountData) {
|
||||
local.copy(
|
||||
frozenInPlanks = frozen,
|
||||
freeInPlanks = free,
|
||||
reservedInPlanks = reserved,
|
||||
transferableMode = TransferableModeLocal.REGULAR,
|
||||
edCountingMode = EDCountingModeLocal.TOTAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RuntimeSnapshot.ormlBalanceKey(accountId: AccountId, chainAsset: Chain.Asset): String {
|
||||
return metadata.tokens().storage("Accounts").storageKey(this, accountId, chainAsset.ormlCurrencyId(this))
|
||||
}
|
||||
|
||||
private fun bindOrmlAccountBalanceOrEmpty(scale: String?, runtime: RuntimeSnapshot): AccountBalance {
|
||||
return scale?.let { bindOrmlAccountData(it, runtime) } ?: AccountBalance.empty()
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindOrmlAccountData
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.returnType
|
||||
import io.novafoundation.nova.common.utils.tokens
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
|
||||
@UseCaseBinding
|
||||
fun bindOrmlAccountData(scale: String, runtime: RuntimeSnapshot): AccountBalance {
|
||||
val type = runtime.metadata.tokens().storage("Accounts").returnType()
|
||||
|
||||
val dynamicInstance = type.fromHexOrNull(runtime, scale)
|
||||
|
||||
return bindOrmlAccountData(dynamicInstance)
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml.hydrationEvm
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.domain.balance.EDCountingMode
|
||||
import io.novafoundation.nova.common.domain.balance.TransferableMode
|
||||
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.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.updateFromChainBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
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_impl.data.network.blockchain.assets.balances.orml.OrmlAssetBalance
|
||||
import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.call.RuntimeCallsApi
|
||||
import io.novafoundation.nova.runtime.ext.currencyId
|
||||
import io.novafoundation.nova.runtime.ext.requireOrml
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
|
||||
import io.novafoundation.nova.runtime.repository.ChainStateRepository
|
||||
import io.novafoundation.nova.runtime.repository.currentRemoteBlockNumberFlow
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Balance source implementation for Hydration ERC20 tokens that are "mostly exposed" via Orml on Substrate side
|
||||
* In particular, we can transact, listen for events, but cannot subscribe storage for balance updates
|
||||
* as balance stays in the smart-contract storage on evm-side. Instead, we use runtime api to poll update once a block
|
||||
*/
|
||||
@FeatureScope
|
||||
class HydrationEvmOrmlAssetBalance @Inject constructor(
|
||||
private val defaultDelegate: OrmlAssetBalance,
|
||||
private val runtimeCallsApi: MultiChainRuntimeCallsApi,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val chainStateRepository: ChainStateRepository,
|
||||
private val assetCache: AssetCache,
|
||||
private val rpcCalls: RpcCalls,
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
return emptyFlow<Nothing>()
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return defaultDelegate.isSelfSufficient(chainAsset)
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
return defaultDelegate.existentialDeposit(chainAsset)
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
return fetchBalance(chainAsset, accountId)
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): Flow<TransferableBalanceUpdatePoint> {
|
||||
return balanceUpdateFlow(chainAsset, accountId, subscriptionBuilder = null)
|
||||
.mapNotNull { (it.update as? BalanceSyncUpdate.CauseFetchable)?.blockHash }
|
||||
.map(::TransferableBalanceUpdatePoint)
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
return balanceUpdateFlow(chainAsset, accountId, subscriptionBuilder).map { (balance, syncUpdate) ->
|
||||
assetCache.updateFromChainBalance(metaAccount.id, balance)
|
||||
|
||||
syncUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun balanceUpdateFlow(
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder?
|
||||
): Flow<BalancePollingUpdate> {
|
||||
val blockNumberFlow = chainStateRepository.currentRemoteBlockNumberFlow(chainAsset.chainId, subscriptionBuilder)
|
||||
|
||||
return flow {
|
||||
val initialBalance = fetchBalance(chainAsset, accountId)
|
||||
val initialBalanceUpdate = BalancePollingUpdate(initialBalance, BalanceSyncUpdate.NoCause)
|
||||
emit(initialBalanceUpdate)
|
||||
|
||||
var currentBalance = initialBalance
|
||||
|
||||
blockNumberFlow.collect { blockNumber ->
|
||||
val newBalance = fetchBalance(chainAsset, accountId)
|
||||
|
||||
if (currentBalance != newBalance) {
|
||||
currentBalance = newBalance
|
||||
|
||||
val balanceUpdatedAt = rpcCalls.getBlockHash(chainAsset.chainId, blockNumber)
|
||||
val syncUpdate = BalanceSyncUpdate.CauseFetchable(balanceUpdatedAt)
|
||||
val balanceUpdate = BalancePollingUpdate(newBalance, syncUpdate)
|
||||
emit(balanceUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchBalance(chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
return runtimeCallsApi.forChain(chainAsset.chainId).fetchBalance(chainAsset, accountId)
|
||||
}
|
||||
|
||||
private suspend fun RuntimeCallsApi.fetchBalance(chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
val runtime = chainRegistry.getRuntime(chainAsset.chainId)
|
||||
val assetId = chainAsset.requireOrml().currencyId(runtime)
|
||||
|
||||
return call(
|
||||
section = "CurrenciesApi",
|
||||
method = "account",
|
||||
arguments = mapOf(
|
||||
"asset_id" to assetId,
|
||||
"who" to accountId
|
||||
),
|
||||
returnBinding = { bindAssetBalance(it, chainAsset) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindAssetBalance(decoded: Any?, chainAsset: Chain.Asset): ChainAssetBalance {
|
||||
val asStruct = decoded.castToStruct()
|
||||
|
||||
return ChainAssetBalance(
|
||||
chainAsset = chainAsset,
|
||||
free = bindNumber(asStruct["free"]),
|
||||
frozen = bindNumber(asStruct["frozen"]),
|
||||
reserved = bindNumber(asStruct["reserved"]),
|
||||
transferableMode = TransferableMode.REGULAR,
|
||||
edCountingMode = EDCountingMode.TOTAL
|
||||
)
|
||||
}
|
||||
|
||||
private data class BalancePollingUpdate(
|
||||
val assetBalance: ChainAssetBalance,
|
||||
val update: BalanceSyncUpdate
|
||||
)
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.decodeValue
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal.EDCountingModeLocal
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal.TransferableModeLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
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.StatemineAssetDetails
|
||||
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.balances.model.transfersFrozen
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.StatemineAssetsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common.bindAssetAccountOrEmpty
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common.statemineModule
|
||||
import io.novafoundation.nova.runtime.ext.requireStatemine
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.prepareIdForEncoding
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
|
||||
// TODO Migrate low-level subscription to storage to AssetsApi
|
||||
class StatemineAssetBalance(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetCache: AssetCache,
|
||||
private val remoteStorage: StorageDataSource,
|
||||
private val statemineAssetsRepository: StatemineAssetsRepository,
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<List<BalanceLock>> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return runCatching {
|
||||
chainAsset.requireStatemine().isSufficient
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
return runCatching {
|
||||
queryAssetDetails(chainAsset).minimumBalance
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to query existential deposit for ${chainAsset.symbol}: ${error.message}")
|
||||
BigInteger.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
return runCatching {
|
||||
val statemineType = chainAsset.requireStatemine()
|
||||
|
||||
val assetAccount = remoteStorage.query(chain.id) {
|
||||
val encodableId = statemineType.prepareIdForEncoding(runtime)
|
||||
|
||||
runtime.metadata.statemineModule(statemineType).storage("Account").query(
|
||||
encodableId,
|
||||
accountId,
|
||||
binding = ::bindAssetAccountOrEmpty
|
||||
)
|
||||
}
|
||||
|
||||
val accountBalance = assetAccount.toAccountBalance()
|
||||
ChainAssetBalance.default(chainAsset, accountBalance)
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to query balance for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
ChainAssetBalance.fromFree(chainAsset, BigInteger.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
return runCatching {
|
||||
val statemineType = chainAsset.requireStatemine()
|
||||
|
||||
remoteStorage.subscribe(chain.id) {
|
||||
val encodableId = statemineType.prepareIdForEncoding(runtime)
|
||||
|
||||
runtime.metadata.statemineModule(statemineType).storage("Account").observeWithRaw(
|
||||
encodableId,
|
||||
accountId,
|
||||
binding = ::bindAssetAccountOrEmpty
|
||||
).map {
|
||||
TransferableBalanceUpdatePoint(it.at!!)
|
||||
}
|
||||
}.catch { error ->
|
||||
Log.e(LOG_TAG, "Balance subscription failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
}
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to setup balance subscription for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun AssetAccount.toAccountBalance(): AccountBalance {
|
||||
val frozenBalance = if (isBalanceFrozen) {
|
||||
balance
|
||||
} else {
|
||||
BigInteger.ZERO
|
||||
}
|
||||
|
||||
return AccountBalance(
|
||||
free = balance,
|
||||
reserved = BigInteger.ZERO,
|
||||
frozen = frozenBalance
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
return runCatching {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
val statemineType = chainAsset.requireStatemine()
|
||||
val encodableAssetId = statemineType.prepareIdForEncoding(runtime)
|
||||
|
||||
val module = runtime.metadata.statemineModule(statemineType)
|
||||
|
||||
val assetAccountStorage = module.storage("Account")
|
||||
val assetAccountKey = assetAccountStorage.storageKey(runtime, encodableAssetId, accountId)
|
||||
|
||||
val assetDetailsFlow = statemineAssetsRepository.subscribeAndSyncAssetDetails(chain.id, statemineType, subscriptionBuilder)
|
||||
|
||||
combine(
|
||||
subscriptionBuilder.subscribe(assetAccountKey),
|
||||
assetDetailsFlow.map { it.status.transfersFrozen }
|
||||
) { balanceStorageChange, isAssetFrozen ->
|
||||
val assetAccountDecoded = assetAccountStorage.decodeValue(balanceStorageChange.value, runtime)
|
||||
val assetAccount = bindAssetAccountOrEmpty(assetAccountDecoded)
|
||||
|
||||
val assetChanged = updateAssetBalance(metaAccount.id, chainAsset, isAssetFrozen, assetAccount)
|
||||
|
||||
if (assetChanged) {
|
||||
BalanceSyncUpdate.CauseFetchable(balanceStorageChange.block)
|
||||
} else {
|
||||
BalanceSyncUpdate.NoCause
|
||||
}
|
||||
}.catch { error ->
|
||||
Log.e(LOG_TAG, "Balance sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
emit(BalanceSyncUpdate.NoCause)
|
||||
}
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to start balance sync for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryAssetDetails(chainAsset: Chain.Asset): StatemineAssetDetails {
|
||||
val statemineType = chainAsset.requireStatemine()
|
||||
return statemineAssetsRepository.getAssetDetails(chainAsset.chainId, statemineType)
|
||||
}
|
||||
|
||||
private suspend fun updateAssetBalance(
|
||||
metaId: Long,
|
||||
chainAsset: Chain.Asset,
|
||||
isAssetFrozen: Boolean,
|
||||
assetAccount: AssetAccount
|
||||
) = assetCache.updateAsset(metaId, chainAsset) {
|
||||
val frozenBalance = if (isAssetFrozen || assetAccount.isBalanceFrozen) {
|
||||
assetAccount.balance
|
||||
} else {
|
||||
BigInteger.ZERO
|
||||
}
|
||||
val freeBalance = assetAccount.balance
|
||||
|
||||
it.copy(
|
||||
frozenInPlanks = frozenBalance,
|
||||
freeInPlanks = freeBalance,
|
||||
transferableMode = TransferableModeLocal.REGULAR,
|
||||
edCountingMode = EDCountingModeLocal.TOTAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.UseCaseBinding
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindBoolean
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindCollectionEnum
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.cast
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.incompatible
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.StatemineAssetDetails
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine.AssetAccount.AccountStatus
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
|
||||
import java.math.BigInteger
|
||||
|
||||
@UseCaseBinding
|
||||
fun bindAssetDetails(decoded: Any?): StatemineAssetDetails {
|
||||
val dynamicInstance = decoded.cast<Struct.Instance>()
|
||||
|
||||
return StatemineAssetDetails(
|
||||
status = bindAssetStatus(dynamicInstance),
|
||||
isSufficient = bindBoolean(dynamicInstance["isSufficient"]),
|
||||
minimumBalance = bindNumber(dynamicInstance["minBalance"]),
|
||||
issuer = bindAccountIdKey(dynamicInstance["issuer"])
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindAssetStatus(assetStruct: Struct.Instance): StatemineAssetDetails.Status {
|
||||
return when {
|
||||
assetStruct.get<Any>("isFrozen") != null -> bindIsFrozen(bindBoolean(assetStruct["isFrozen"]))
|
||||
assetStruct.get<Any>("status") != null -> bindCollectionEnum(assetStruct["status"])
|
||||
else -> incompatible()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindIsFrozen(isFrozen: Boolean): StatemineAssetDetails.Status {
|
||||
return if (isFrozen) StatemineAssetDetails.Status.Frozen else StatemineAssetDetails.Status.Live
|
||||
}
|
||||
|
||||
class AssetAccount(
|
||||
val balance: BigInteger,
|
||||
val status: AccountStatus
|
||||
) {
|
||||
|
||||
enum class AccountStatus {
|
||||
Liquid, Frozen, Blocked
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun empty() = AssetAccount(
|
||||
balance = BigInteger.ZERO,
|
||||
status = AccountStatus.Liquid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val AssetAccount.isBalanceFrozen: Boolean
|
||||
get() = status == AccountStatus.Blocked || status == AccountStatus.Frozen
|
||||
|
||||
val AssetAccount.canAcceptFunds: Boolean
|
||||
get() = status != AccountStatus.Blocked
|
||||
|
||||
@UseCaseBinding
|
||||
fun bindAssetAccount(decoded: Any): AssetAccount {
|
||||
val dynamicInstance = decoded.cast<Struct.Instance>()
|
||||
|
||||
val status = when {
|
||||
// old version of assets pallet - isFrozen flag
|
||||
"isFrozen" in dynamicInstance.mapping -> {
|
||||
val isFrozen = bindBoolean(dynamicInstance["isFrozen"])
|
||||
if (isFrozen) AccountStatus.Frozen else AccountStatus.Liquid
|
||||
}
|
||||
|
||||
// new version of assets pallet - status enum
|
||||
"status" in dynamicInstance.mapping -> {
|
||||
bindCollectionEnum(dynamicInstance["status"])
|
||||
}
|
||||
|
||||
else -> incompatible()
|
||||
}
|
||||
|
||||
return AssetAccount(
|
||||
balance = bindNumber(dynamicInstance["balance"]),
|
||||
status = status,
|
||||
)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.utility
|
||||
|
||||
import io.novafoundation.nova.common.utils.balances
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.BlockchainLock
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bindBalanceFreezes
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bindBalanceLocks
|
||||
import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableModule
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1
|
||||
import io.novafoundation.nova.runtime.storage.source.query.api.storage1
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
|
||||
|
||||
@JvmInline
|
||||
value class BalancesRuntimeApi(override val module: Module) : QueryableModule
|
||||
|
||||
context(StorageQueryContext)
|
||||
val RuntimeMetadata.balances: BalancesRuntimeApi
|
||||
get() = BalancesRuntimeApi(balances())
|
||||
|
||||
context(StorageQueryContext)
|
||||
val BalancesRuntimeApi.locks: QueryableStorageEntry1<AccountId, List<BlockchainLock>>
|
||||
get() = storage1("Locks", binding = { decoded, _ -> bindBalanceLocks(decoded) })
|
||||
|
||||
context(StorageQueryContext)
|
||||
val BalancesRuntimeApi.freezes: QueryableStorageEntry1<AccountId, List<BlockchainLock>>
|
||||
get() = storage1("Freezes", binding = { decoded, _ -> bindBalanceFreezes(decoded) })
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.utility
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToDictEnum
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.balances
|
||||
import io.novafoundation.nova.common.utils.decodeValue
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
import io.novafoundation.nova.common.utils.numberConstant
|
||||
import io.novafoundation.nova.common.utils.system
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core_db.dao.HoldsDao
|
||||
import io.novafoundation.nova.core_db.dao.LockDao
|
||||
import io.novafoundation.nova.core_db.model.BalanceHoldLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.bindAccountInfoOrDefault
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.updateAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
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.balances.model.toChainAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.AccountInfoRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.updateLocks
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
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 io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novafoundation.nova.runtime.storage.typed.account
|
||||
import io.novafoundation.nova.runtime.storage.typed.system
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
|
||||
class NativeAssetBalance(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetCache: AssetCache,
|
||||
private val accountInfoRepository: AccountInfoRepository,
|
||||
private val remoteStorage: StorageDataSource,
|
||||
private val lockDao: LockDao,
|
||||
private val holdsDao: HoldsDao,
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
return runCatching {
|
||||
remoteStorage.subscribe(chain.id, subscriptionBuilder) {
|
||||
combine(
|
||||
metadata.balances.locks.observe(accountId),
|
||||
metadata.balances.freezes.observe(accountId)
|
||||
) { locks, freezes ->
|
||||
val all = locks.orEmpty() + freezes.orEmpty()
|
||||
|
||||
lockDao.updateLocks(all, metaAccount.id, chain.id, chainAsset.id)
|
||||
}
|
||||
}.catch { error ->
|
||||
Log.e(LOG_TAG, "Balance locks sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
}
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to start balance locks sync for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalanceHolds(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
return runCatching {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
val storage = runtime.metadata.balances().storageOrNull("Holds") ?: return emptyFlow<Nothing>()
|
||||
val key = storage.storageKey(runtime, accountId)
|
||||
|
||||
subscriptionBuilder.subscribe(key)
|
||||
.map { change ->
|
||||
val holds = bindBalanceHolds(storage.decodeValue(change.value, runtime)).orEmpty()
|
||||
holdsDao.updateHolds(holds, metaAccount.id, chain.id, chainAsset.id)
|
||||
}
|
||||
.catch { error ->
|
||||
Log.e(LOG_TAG, "Balance holds sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
}
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to start balance holds sync for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
return runCatching {
|
||||
val runtime = chainRegistry.getRuntime(chainAsset.chainId)
|
||||
runtime.metadata.balances().numberConstant("ExistentialDeposit", runtime)
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to query existential deposit for ${chainAsset.symbol}: ${error.message}")
|
||||
BigInteger.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
return runCatching {
|
||||
accountInfoRepository.getAccountInfo(chain.id, accountId).data.toChainAssetBalance(chainAsset)
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to query balance for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
ChainAssetBalance.fromFree(chainAsset, BigInteger.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
return runCatching {
|
||||
remoteStorage.subscribe(chain.id) {
|
||||
metadata.system.account.observeWithRaw(accountId).map {
|
||||
TransferableBalanceUpdatePoint(it.at!!)
|
||||
}
|
||||
}.catch { error ->
|
||||
Log.e(LOG_TAG, "Balance subscription failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
}
|
||||
}.getOrElse { error ->
|
||||
Log.e(LOG_TAG, "Failed to setup balance subscription for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
val key = try {
|
||||
runtime.metadata.system().storage("Account").storageKey(runtime, accountId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Failed to construct account storage key: ${e.message} in ${chain.name}")
|
||||
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
return subscriptionBuilder.subscribe(key)
|
||||
.map { change ->
|
||||
val accountInfo = bindAccountInfoOrDefault(change.value, runtime)
|
||||
val assetChanged = assetCache.updateAsset(metaAccount.id, chain.utilityAsset, accountInfo)
|
||||
|
||||
if (assetChanged) {
|
||||
BalanceSyncUpdate.CauseFetchable(change.block)
|
||||
} else {
|
||||
BalanceSyncUpdate.NoCause
|
||||
}
|
||||
}
|
||||
.catch { error ->
|
||||
Log.e(LOG_TAG, "Balance sync failed for ${chainAsset.symbol} on ${chain.name}: ${error.message}")
|
||||
emit(BalanceSyncUpdate.NoCause)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindBalanceHolds(dynamicInstance: Any?): List<BlockchainHold>? {
|
||||
if (dynamicInstance == null) return null
|
||||
|
||||
return bindList(dynamicInstance) {
|
||||
BlockchainHold(
|
||||
id = bindHoldId(it.castToStruct()["id"]),
|
||||
amount = bindNumber(it.castToStruct()["amount"])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindHoldId(id: Any?): BalanceHold.HoldId {
|
||||
val module = id.castToDictEnum()
|
||||
val reason = module.value.castToDictEnum()
|
||||
|
||||
return BalanceHold.HoldId(module.name, reason.name)
|
||||
}
|
||||
|
||||
private suspend fun HoldsDao.updateHolds(holds: List<BlockchainHold>, metaId: Long, chainId: ChainId, chainAssetId: ChainAssetId) {
|
||||
val balanceLocksLocal = holds.map {
|
||||
BalanceHoldLocal(
|
||||
metaId = metaId,
|
||||
chainId = chainId,
|
||||
assetId = chainAssetId,
|
||||
id = BalanceHoldLocal.HoldIdLocal(module = it.id.module, reason = it.id.reason),
|
||||
amount = it.amount
|
||||
)
|
||||
}
|
||||
updateHolds(balanceLocksLocal, metaId, chainId, chainAssetId)
|
||||
}
|
||||
|
||||
private class BlockchainHold(val id: BalanceHold.HoldId, val amount: Balance)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine.AssetAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine.bindAssetAccount
|
||||
import io.novafoundation.nova.runtime.ext.palletNameOrDefault
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.RuntimeMetadata
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module
|
||||
|
||||
fun RuntimeMetadata.statemineModule(statemineType: Chain.Asset.Type.Statemine) = module(statemineType.palletNameOrDefault())
|
||||
|
||||
fun bindAssetAccountOrEmpty(decoded: Any?): AssetAccount {
|
||||
return decoded?.let(::bindAssetAccount) ?: AssetAccount.empty()
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common.orml
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSource
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.StaticAssetSource
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml.OrmlAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.orml.hydrationEvm.HydrationEvmOrmlAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.orml.OrmlAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.orml.OrmlAssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.orml.hydrationEvm.HydrationEvmAssetTransfers
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class OrmlAssetSourceFactory @Inject constructor(
|
||||
defaultBalance: OrmlAssetBalance,
|
||||
defaultHistory: OrmlAssetHistory,
|
||||
defaultTransfers: OrmlAssetTransfers,
|
||||
|
||||
hydrationEvmOrmlAssetBalance: HydrationEvmOrmlAssetBalance,
|
||||
hydrationEvmAssetTransfers: HydrationEvmAssetTransfers
|
||||
) {
|
||||
|
||||
private val defaultSource = StaticAssetSource(defaultTransfers, defaultBalance, defaultHistory)
|
||||
private val hydrationEvmSource = StaticAssetSource(hydrationEvmAssetTransfers, hydrationEvmOrmlAssetBalance, defaultHistory)
|
||||
|
||||
fun allSources(): List<AssetSource> {
|
||||
return listOf(defaultSource, hydrationEvmSource)
|
||||
}
|
||||
|
||||
fun getSourceBySubtype(subType: Chain.Asset.Type.Orml.SubType): AssetSource {
|
||||
return when (subType) {
|
||||
Chain.Asset.Type.Orml.SubType.DEFAULT -> defaultSource
|
||||
Chain.Asset.Type.Orml.SubType.HYDRATION_EVM -> hydrationEvmSource
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector
|
||||
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
|
||||
|
||||
class UnsupportedEventDetector : AssetEventDetector {
|
||||
|
||||
override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.evmErc20
|
||||
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindByteArray
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.ethereumAddressToAccountId
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Queries
|
||||
import io.novafoundation.nova.runtime.ext.requireErc20
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class EvmErc20EventDetectorFactory @Inject constructor() {
|
||||
|
||||
fun create(chainAsset: Chain.Asset): AssetEventDetector {
|
||||
return EvmErc20EventDetector(chainAsset.requireErc20())
|
||||
}
|
||||
}
|
||||
|
||||
class EvmErc20EventDetector(
|
||||
private val erc20AssetType: Chain.Asset.Type.EvmErc20
|
||||
) : AssetEventDetector {
|
||||
|
||||
override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? {
|
||||
return parseEvmLogEvent(event)?.toDepositEvent()
|
||||
}
|
||||
|
||||
private fun parseEvmLogEvent(event: GenericEvent.Instance): Erc20Queries.Transfer? {
|
||||
if (!event.instanceOf(Modules.EVM, "Log")) return null
|
||||
|
||||
val args = event.arguments.first().castToStruct()
|
||||
|
||||
val address = bindAccountIdKey(args["address"])
|
||||
val contractAddress = erc20AssetType.contractAddress.ethereumAddressToAccountId().intoKey()
|
||||
|
||||
if (contractAddress != address) return null
|
||||
|
||||
val topics = bindList(args["topics"], ::bindHexString)
|
||||
|
||||
val eventSignature = topics[0]
|
||||
if (eventSignature != Erc20Queries.transferEventSignature()) return null
|
||||
|
||||
return Erc20Queries.parseTransferEvent(
|
||||
topic1 = topics[1],
|
||||
topic2 = topics[2],
|
||||
data = bindHexString(args["data"])
|
||||
)
|
||||
}
|
||||
|
||||
private fun Erc20Queries.Transfer.toDepositEvent(): DepositEvent {
|
||||
return DepositEvent(
|
||||
destination = to.value.ethereumAddressToAccountId().intoKey(),
|
||||
amount = amount.value
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindHexString(decoded: Any?): String {
|
||||
return bindByteArray(decoded).toHexString(withPrefix = true)
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.orml
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent
|
||||
import io.novafoundation.nova.runtime.ext.requireOrml
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHexUntyped
|
||||
|
||||
class OrmlAssetEventDetectorFactory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) {
|
||||
|
||||
suspend fun create(chainAsset: Chain.Asset): AssetEventDetector {
|
||||
val ormlType = chainAsset.requireOrml()
|
||||
val runtime = chainRegistry.getRuntime(chainAsset.chainId)
|
||||
|
||||
return OrmlAssetEventDetector(runtime, ormlType)
|
||||
}
|
||||
}
|
||||
|
||||
private class OrmlAssetEventDetector(
|
||||
private val runtimeSnapshot: RuntimeSnapshot,
|
||||
private val ormlType: Chain.Asset.Type.Orml,
|
||||
) : AssetEventDetector {
|
||||
|
||||
private val targetCurrencyId = ormlType.currencyIdScale.requireHexPrefix()
|
||||
|
||||
override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? {
|
||||
return detectTokensDeposited(event)
|
||||
}
|
||||
|
||||
private fun detectTokensDeposited(event: GenericEvent.Instance): DepositEvent? {
|
||||
if (!event.instanceOf(Modules.TOKENS, "Deposited")) return null
|
||||
|
||||
val (currencyId, who, amount) = event.arguments
|
||||
|
||||
val currencyIdType = runtimeSnapshot.typeRegistry[ormlType.currencyIdType]!!
|
||||
val currencyIdEncoded = currencyIdType.toHexUntyped(runtimeSnapshot, currencyId).requireHexPrefix()
|
||||
if (currencyIdEncoded != targetCurrencyId) return null
|
||||
|
||||
return DepositEvent(
|
||||
destination = bindAccountIdKey(who),
|
||||
amount = bindNumber(amount)
|
||||
)
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.statemine
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.model.DepositEvent
|
||||
import io.novafoundation.nova.runtime.ext.palletNameOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.requireStatemine
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.StatemineAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHexUntyped
|
||||
import java.math.BigInteger
|
||||
|
||||
class StatemineAssetEventDetectorFactory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
) {
|
||||
|
||||
suspend fun create(chainAsset: Chain.Asset): AssetEventDetector {
|
||||
val assetType = chainAsset.requireStatemine()
|
||||
val runtime = chainRegistry.getRuntime(chainAsset.chainId)
|
||||
|
||||
return StatemineAssetEventDetector(runtime, assetType)
|
||||
}
|
||||
}
|
||||
|
||||
class StatemineAssetEventDetector(
|
||||
private val runtimeSnapshot: RuntimeSnapshot,
|
||||
private val assetType: Chain.Asset.Type.Statemine,
|
||||
) : AssetEventDetector {
|
||||
|
||||
private val targetAssetId = assetType.id.stringAssetId()
|
||||
|
||||
override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? {
|
||||
return detectTokensDeposited(event)
|
||||
}
|
||||
|
||||
private fun detectTokensDeposited(event: GenericEvent.Instance): DepositEvent? {
|
||||
if (!event.instanceOf(assetType.palletNameOrDefault(), "Issued")) return null
|
||||
|
||||
val (assetId, who, amount) = event.arguments
|
||||
|
||||
val assetIdType = event.event.arguments.first()!!
|
||||
val assetIdAsString = decodedAssetItToString(assetId, assetIdType)
|
||||
if (assetIdAsString != targetAssetId) return null
|
||||
|
||||
return DepositEvent(
|
||||
destination = bindAccountIdKey(who),
|
||||
amount = bindNumber(amount)
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodedAssetItToString(assetId: Any?, assetIdType: RuntimeType<*, *>): String {
|
||||
return if (assetId is BigInteger) {
|
||||
assetId.toString()
|
||||
} else {
|
||||
assetIdType.toHexUntyped(runtimeSnapshot, assetId).requireHexPrefix()
|
||||
}
|
||||
}
|
||||
|
||||
private fun StatemineAssetId.stringAssetId(): String {
|
||||
return when (this) {
|
||||
is StatemineAssetId.Number -> value.toString()
|
||||
is StatemineAssetId.ScaleEncoded -> scaleHex
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.events.utility
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.events.AssetEventDetector
|
||||
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
|
||||
|
||||
class NativeAssetEventDetector : AssetEventDetector {
|
||||
|
||||
override fun detectDeposit(event: GenericEvent.Instance): DepositEvent? {
|
||||
return detectMinted(event)
|
||||
?: detectBalancesDeposit(event)
|
||||
}
|
||||
|
||||
private fun detectMinted(event: GenericEvent.Instance): DepositEvent? {
|
||||
if (!event.instanceOf(Modules.BALANCES, "Minted")) return null
|
||||
|
||||
val (who, amount) = event.arguments
|
||||
|
||||
return DepositEvent(
|
||||
destination = bindAccountIdKey(who),
|
||||
amount = bindNumber(amount)
|
||||
)
|
||||
}
|
||||
|
||||
private fun detectBalancesDeposit(event: GenericEvent.Instance): DepositEvent? {
|
||||
if (!event.instanceOf(Modules.BALANCES, "Deposit")) return null
|
||||
|
||||
val (who, amount) = event.arguments
|
||||
|
||||
return DepositEvent(
|
||||
destination = bindAccountIdKey(who),
|
||||
amount = bindNumber(amount)
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history
|
||||
|
||||
import io.novafoundation.nova.feature_currency_api.domain.model.Currency
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.AssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.getAllCoinPriceHistory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
abstract class BaseAssetHistory(internal val coinPriceRepository: CoinPriceRepository) : AssetHistory {
|
||||
|
||||
protected suspend fun getPriceHistory(
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): List<HistoricalCoinRate> {
|
||||
return runCatching { coinPriceRepository.getAllCoinPriceHistory(chainAsset.priceId!!, currency) }
|
||||
.getOrNull()
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.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.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.satisfies
|
||||
import io.novafoundation.nova.runtime.ext.externalApi
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
|
||||
private const val FIRST_PAGE_INDEX = 1
|
||||
private const val SECOND_PAGE_INDEX = 2
|
||||
|
||||
abstract class EvmAssetHistory(
|
||||
coinPriceRepository: CoinPriceRepository
|
||||
) : BaseAssetHistory(coinPriceRepository) {
|
||||
|
||||
abstract suspend fun fetchEtherscanOperations(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
apiUrl: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
currency: Currency
|
||||
): List<Operation>
|
||||
|
||||
override suspend fun additionalFirstPageSync(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
page: Result<DataPage<Operation>>
|
||||
) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
override suspend fun getOperations(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): DataPage<Operation> {
|
||||
val evmTransfersApi = chain.evmTransfersApi() ?: return DataPage.empty()
|
||||
|
||||
return getOperationsEtherscan(
|
||||
pageSize,
|
||||
pageOffset,
|
||||
filters,
|
||||
accountId,
|
||||
chain,
|
||||
chainAsset,
|
||||
evmTransfersApi.url,
|
||||
currency
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getSyncedPageOffset(
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): PageOffset {
|
||||
val evmTransfersApi = chain.evmTransfersApi()
|
||||
|
||||
return if (evmTransfersApi != null) {
|
||||
PageOffset.Loadable.PageNumber(page = SECOND_PAGE_INDEX)
|
||||
} else {
|
||||
PageOffset.FullData
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOperationsEtherscan(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
apiUrl: String,
|
||||
currency: Currency
|
||||
): DataPage<Operation> {
|
||||
val page = when (pageOffset) {
|
||||
PageOffset.Loadable.FirstPage -> FIRST_PAGE_INDEX
|
||||
is PageOffset.Loadable.PageNumber -> pageOffset.page
|
||||
else -> error("Etherscan requires page number pagination")
|
||||
}
|
||||
|
||||
val operations = fetchEtherscanOperations(chain, chainAsset, accountId, apiUrl, page, pageSize, currency)
|
||||
|
||||
val newPageOffset = if (operations.size < pageSize) {
|
||||
PageOffset.FullData
|
||||
} else {
|
||||
PageOffset.Loadable.PageNumber(page + 1)
|
||||
}
|
||||
|
||||
val filteredOperations = operations.filter { it.type.satisfies(filters) }
|
||||
|
||||
return DataPage(newPageOffset, filteredOperations)
|
||||
}
|
||||
|
||||
fun Chain.evmTransfersApi(): Chain.ExternalApi.Transfers.Evm? {
|
||||
return externalApi()
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history
|
||||
|
||||
import io.novafoundation.nova.common.data.model.CursorOrFull
|
||||
import io.novafoundation.nova.common.data.model.DataPage
|
||||
import io.novafoundation.nova.common.data.model.PageOffset
|
||||
import io.novafoundation.nova.common.data.model.asCursorOrNull
|
||||
import io.novafoundation.nova.common.utils.nullIfEmpty
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
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.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.convertPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.findNearestCoinRate
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.model.AssetsBySubQueryId
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.model.assetsBySubQueryId
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.model.request.SubqueryHistoryRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.model.response.SubqueryHistoryElementResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.externalApi
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.legacyAddressOfOrNull
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
abstract class SubstrateAssetHistory(
|
||||
private val subqueryApi: SubQueryOperationsApi,
|
||||
private val cursorStorage: TransferCursorStorage,
|
||||
private val realtimeOperationFetcherFactory: SubstrateRealtimeOperationFetcher.Factory,
|
||||
coinPriceRepository: CoinPriceRepository
|
||||
) : BaseAssetHistory(coinPriceRepository) {
|
||||
|
||||
abstract fun realtimeFetcherSources(chain: Chain): List<SubstrateRealtimeOperationFetcher.Factory.Source>
|
||||
|
||||
override suspend fun fetchOperationsForBalanceChange(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
blockHash: String,
|
||||
accountId: AccountId
|
||||
): List<RealtimeHistoryUpdate> {
|
||||
val sources = realtimeFetcherSources(chain)
|
||||
val realtimeOperationFetcher = realtimeOperationFetcherFactory.create(sources)
|
||||
|
||||
return realtimeOperationFetcher.extractRealtimeHistoryUpdates(chain, chainAsset, blockHash)
|
||||
}
|
||||
|
||||
override suspend fun additionalFirstPageSync(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
pageResult: Result<DataPage<Operation>>
|
||||
) {
|
||||
pageResult
|
||||
.onSuccess { page ->
|
||||
val newCursor = page.nextOffset.asCursorOrNull()?.value
|
||||
cursorStorage.saveCursor(chain.id, chainAsset.id, accountId, newCursor)
|
||||
}
|
||||
.onFailure {
|
||||
// Empty cursor means we haven't yet synced any data for this asset
|
||||
// However we still want to store null cursor on failure to show items
|
||||
// that came not from the remote (e.g. local pending operations)
|
||||
if (!cursorStorage.hasCursor(chain.id, chainAsset.id, accountId)) {
|
||||
cursorStorage.saveCursor(chain.id, chainAsset.id, accountId, cursor = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getOperations(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency,
|
||||
): DataPage<Operation> {
|
||||
val substrateTransfersApi = chain.substrateTransfersApi()
|
||||
|
||||
return if (substrateTransfersApi != null) {
|
||||
getOperationsInternal(
|
||||
pageSize = pageSize,
|
||||
pageOffset = pageOffset,
|
||||
filters = filters,
|
||||
accountId = accountId,
|
||||
apiUrl = substrateTransfersApi.url,
|
||||
chainAsset = chainAsset,
|
||||
chain = chain,
|
||||
currency = currency
|
||||
)
|
||||
} else {
|
||||
DataPage.empty()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSyncedPageOffset(accountId: AccountId, chain: Chain, chainAsset: Chain.Asset): PageOffset {
|
||||
val substrateTransfersApi = chain.substrateTransfersApi()
|
||||
|
||||
return if (substrateTransfersApi != null) {
|
||||
val cursor = cursorStorage.awaitCursor(chain.id, chainAsset.id, accountId)
|
||||
|
||||
PageOffset.CursorOrFull(cursor)
|
||||
} else {
|
||||
PageOffset.FullData
|
||||
}
|
||||
}
|
||||
|
||||
override fun isOperationSafe(operation: Operation): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun getOperationsInternal(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency,
|
||||
apiUrl: String
|
||||
): DataPage<Operation> {
|
||||
val cursor = when (pageOffset) {
|
||||
is PageOffset.Loadable.Cursor -> pageOffset.value
|
||||
PageOffset.Loadable.FirstPage -> null
|
||||
else -> error("SubQuery requires cursor pagination")
|
||||
}
|
||||
|
||||
val request = SubqueryHistoryRequest(
|
||||
accountAddress = chain.addressOf(accountId),
|
||||
legacyAccountAddress = chain.legacyAddressOfOrNull(accountId),
|
||||
pageSize = pageSize,
|
||||
cursor = cursor,
|
||||
filters = filters,
|
||||
asset = chainAsset,
|
||||
chain = chain
|
||||
)
|
||||
|
||||
val subqueryResponse = subqueryApi.getOperationsHistory(apiUrl, request).data.query
|
||||
|
||||
val priceHistory = getPriceHistory(chainAsset, currency)
|
||||
|
||||
val assetsBySubQueryId = chain.assetsBySubQueryId()
|
||||
|
||||
val operations = subqueryResponse.historyElements.nodes.mapNotNull { node ->
|
||||
val coinRate = priceHistory.findNearestCoinRate(node.timestamp)
|
||||
mapNodeToOperation(node, coinRate, chainAsset, assetsBySubQueryId)
|
||||
}
|
||||
|
||||
val pageInfo = subqueryResponse.historyElements.pageInfo
|
||||
val newPageOffset = PageOffset.CursorOrFull(pageInfo.endCursor)
|
||||
|
||||
return DataPage(newPageOffset, operations)
|
||||
}
|
||||
|
||||
private fun Chain.substrateTransfersApi(): Chain.ExternalApi.Transfers.Substrate? {
|
||||
return externalApi()
|
||||
}
|
||||
|
||||
private fun mapNodeToOperation(
|
||||
node: SubqueryHistoryElementResponse.Query.HistoryElements.Node,
|
||||
coinRate: CoinRate?,
|
||||
chainAsset: Chain.Asset,
|
||||
chainAssetsBySubQueryId: AssetsBySubQueryId
|
||||
): Operation? {
|
||||
val type: Operation.Type
|
||||
val status: Operation.Status
|
||||
|
||||
when {
|
||||
node.reward != null -> with(node.reward) {
|
||||
val planks = amount?.toBigIntegerOrNull().orZero()
|
||||
|
||||
type = Operation.Type.Reward(
|
||||
amount = planks,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, planks),
|
||||
isReward = isReward,
|
||||
kind = Operation.Type.Reward.RewardKind.Direct(
|
||||
era = era,
|
||||
validator = validator.nullIfEmpty(),
|
||||
),
|
||||
eventId = eventId(node.blockNumber, node.reward.eventIdx)
|
||||
)
|
||||
status = Operation.Status.COMPLETED
|
||||
}
|
||||
|
||||
node.poolReward != null -> with(node.poolReward) {
|
||||
type = Operation.Type.Reward(
|
||||
amount = amount,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, amount),
|
||||
isReward = isReward,
|
||||
kind = Operation.Type.Reward.RewardKind.Pool(
|
||||
poolId = poolId
|
||||
),
|
||||
eventId = eventId(node.blockNumber, node.poolReward.eventIdx)
|
||||
)
|
||||
status = Operation.Status.COMPLETED
|
||||
}
|
||||
|
||||
node.extrinsic != null -> with(node.extrinsic) {
|
||||
type = Operation.Type.Extrinsic(
|
||||
content = Operation.Type.Extrinsic.Content.SubstrateCall(module, call),
|
||||
fee = fee,
|
||||
fiatFee = coinRate?.convertPlanks(chainAsset, fee),
|
||||
)
|
||||
status = Operation.Status.fromSuccess(success)
|
||||
}
|
||||
|
||||
node.transfer != null -> with(node.transfer) {
|
||||
type = Operation.Type.Transfer(
|
||||
myAddress = node.address,
|
||||
amount = amount,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, amount),
|
||||
receiver = to,
|
||||
sender = from,
|
||||
fee = fee,
|
||||
)
|
||||
status = Operation.Status.fromSuccess(success)
|
||||
}
|
||||
|
||||
node.assetTransfer != null -> with(node.assetTransfer) {
|
||||
type = Operation.Type.Transfer(
|
||||
myAddress = node.address,
|
||||
amount = amount,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, amount),
|
||||
receiver = to,
|
||||
sender = from,
|
||||
fee = fee,
|
||||
)
|
||||
status = Operation.Status.fromSuccess(success)
|
||||
}
|
||||
|
||||
node.swap != null -> with(node.swap) {
|
||||
val assetIn = chainAssetsBySubQueryId[assetIdIn] ?: return null
|
||||
val assetOut = chainAssetsBySubQueryId[assetIdOut] ?: return null
|
||||
val assetFee = chainAssetsBySubQueryId[assetIdFee] ?: return null
|
||||
|
||||
val amount = if (assetIn.fullId == chainAsset.fullId) amountIn else amountOut
|
||||
|
||||
type = Operation.Type.Swap(
|
||||
fee = ChainAssetWithAmount(
|
||||
chainAsset = assetFee,
|
||||
amount = fee,
|
||||
),
|
||||
amountIn = ChainAssetWithAmount(
|
||||
chainAsset = assetIn,
|
||||
amount = amountIn,
|
||||
),
|
||||
amountOut = ChainAssetWithAmount(
|
||||
chainAsset = assetOut,
|
||||
amount = amountOut,
|
||||
),
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, amount)
|
||||
)
|
||||
status = Operation.Status.fromSuccess(success ?: true)
|
||||
}
|
||||
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return Operation(
|
||||
id = node.id,
|
||||
address = node.address,
|
||||
type = type,
|
||||
time = node.timestamp.seconds.inWholeMilliseconds,
|
||||
chainAsset = chainAsset,
|
||||
extrinsicHash = node.extrinsicHash,
|
||||
status = status
|
||||
)
|
||||
}
|
||||
|
||||
private fun eventId(blockNumber: Long, eventIdx: Int): String {
|
||||
return "$blockNumber-$eventIdx"
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.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.AssetHistory
|
||||
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
|
||||
|
||||
class UnsupportedAssetHistory : AssetHistory {
|
||||
|
||||
override suspend fun fetchOperationsForBalanceChange(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
blockHash: String,
|
||||
accountId: AccountId
|
||||
): List<RealtimeHistoryUpdate> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter> {
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
override suspend fun additionalFirstPageSync(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId, page: Result<DataPage<Operation>>) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override suspend fun getOperations(
|
||||
pageSize: Int,
|
||||
pageOffset: PageOffset.Loadable,
|
||||
filters: Set<TransactionFilter>,
|
||||
accountId: AccountId,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
currency: Currency
|
||||
): DataPage<Operation> {
|
||||
return DataPage.empty()
|
||||
}
|
||||
|
||||
override suspend fun getSyncedPageOffset(accountId: AccountId, chain: Chain, chainAsset: Chain.Asset): PageOffset {
|
||||
return PageOffset.FullData
|
||||
}
|
||||
|
||||
override fun isOperationSafe(operation: Operation): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.equilibrium
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.eqBalances
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
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
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Factory.Source
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.asSource
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.SubstrateAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.call
|
||||
|
||||
class EquilibriumAssetHistory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
walletOperationsApi: SubQueryOperationsApi,
|
||||
cursorStorage: TransferCursorStorage,
|
||||
coinPriceRepository: CoinPriceRepository,
|
||||
realtimeOperationFetcherFactory: SubstrateRealtimeOperationFetcher.Factory
|
||||
) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) {
|
||||
|
||||
override fun realtimeFetcherSources(chain: Chain): List<Source> {
|
||||
return listOf(TransferExtractor().asSource())
|
||||
}
|
||||
|
||||
override fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter> {
|
||||
return setOfNotNull(
|
||||
TransactionFilter.TRANSFER,
|
||||
TransactionFilter.EXTRINSIC.takeIf { asset.isUtilityAsset }
|
||||
)
|
||||
}
|
||||
|
||||
private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor {
|
||||
|
||||
override suspend fun extractRealtimeHistoryUpdates(
|
||||
extrinsicVisit: ExtrinsicVisit,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): RealtimeHistoryUpdate.Type? {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
val call = extrinsicVisit.call
|
||||
if (!call.isTransfer(runtime)) return null
|
||||
|
||||
val amount = bindNumber(call.arguments["value"])
|
||||
|
||||
return RealtimeHistoryUpdate.Type.Transfer(
|
||||
senderId = extrinsicVisit.origin,
|
||||
recipientId = bindAccountIdentifier(call.arguments["to"]),
|
||||
amountInPlanks = amount,
|
||||
chainAsset = chainAsset,
|
||||
)
|
||||
}
|
||||
|
||||
private fun GenericCall.Instance.isTransfer(runtime: RuntimeSnapshot): Boolean {
|
||||
val balances = runtime.metadata.eqBalances()
|
||||
|
||||
return instanceOf(balances.call("transfer"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.evmErc20
|
||||
|
||||
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.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.convertPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.findNearestCoinRate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.isZeroTransfer
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.EvmAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.EtherscanTransactionsApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanAccountTransfer
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.feeUsed
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.requireErc20
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class EvmErc20AssetHistory(
|
||||
private val etherscanTransactionsApi: EtherscanTransactionsApi,
|
||||
coinPriceRepository: CoinPriceRepository
|
||||
) : EvmAssetHistory(coinPriceRepository) {
|
||||
|
||||
override suspend fun fetchEtherscanOperations(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
apiUrl: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
currency: Currency
|
||||
): List<Operation> {
|
||||
val erc20Config = chainAsset.requireErc20()
|
||||
val accountAddress = chain.addressOf(accountId)
|
||||
|
||||
val response = etherscanTransactionsApi.getErc20Transfers(
|
||||
baseUrl = apiUrl,
|
||||
contractAddress = erc20Config.contractAddress,
|
||||
accountAddress = accountAddress,
|
||||
pageNumber = page,
|
||||
pageSize = pageSize,
|
||||
chainId = chain.id
|
||||
)
|
||||
|
||||
val priceHistory = getPriceHistory(chainAsset, currency)
|
||||
|
||||
return response.result.map {
|
||||
val coinRate = priceHistory.findNearestCoinRate(it.timeStamp)
|
||||
mapRemoteTransferToOperation(it, chainAsset, accountAddress, coinRate)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchOperationsForBalanceChange(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
blockHash: String,
|
||||
accountId: AccountId,
|
||||
): List<RealtimeHistoryUpdate> {
|
||||
// we fetch transfers alongside with balance updates in EvmAssetBalance
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter> {
|
||||
return setOf(TransactionFilter.TRANSFER)
|
||||
}
|
||||
|
||||
override fun isOperationSafe(operation: Operation): Boolean {
|
||||
return !operation.isZeroTransfer()
|
||||
}
|
||||
|
||||
private fun mapRemoteTransferToOperation(
|
||||
remote: EtherscanAccountTransfer,
|
||||
chainAsset: Chain.Asset,
|
||||
accountAddress: String,
|
||||
coinRate: CoinRate?
|
||||
): Operation {
|
||||
return Operation(
|
||||
id = remote.hash,
|
||||
address = accountAddress,
|
||||
extrinsicHash = remote.hash,
|
||||
type = Operation.Type.Transfer(
|
||||
myAddress = accountAddress,
|
||||
amount = remote.value,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, remote.value),
|
||||
receiver = remote.to,
|
||||
sender = remote.from,
|
||||
fee = remote.feeUsed
|
||||
),
|
||||
time = remote.timeStamp.seconds.inWholeMilliseconds,
|
||||
chainAsset = chainAsset,
|
||||
status = Operation.Status.COMPLETED,
|
||||
)
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.evmNative
|
||||
|
||||
import io.novafoundation.nova.common.utils.ethereumAddressToAccountId
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
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.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.CoinRate
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Operation.Type.Extrinsic.Content
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.convertPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.findNearestCoinRate
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.EvmAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.EtherscanTransactionsApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanNormalTxResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.feeUsed
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.isTransfer
|
||||
import io.novafoundation.nova.runtime.ethereum.sendSuspend
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import org.web3j.protocol.core.methods.response.EthBlock
|
||||
import org.web3j.protocol.core.methods.response.EthBlock.TransactionResult
|
||||
import org.web3j.protocol.core.methods.response.Transaction
|
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt
|
||||
import java.math.BigInteger
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class EvmNativeAssetHistory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val etherscanTransactionsApi: EtherscanTransactionsApi,
|
||||
coinPriceRepository: CoinPriceRepository
|
||||
) : EvmAssetHistory(coinPriceRepository) {
|
||||
|
||||
override suspend fun fetchEtherscanOperations(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
apiUrl: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
currency: Currency
|
||||
): List<Operation> {
|
||||
val accountAddress = chain.addressOf(accountId)
|
||||
|
||||
val response = etherscanTransactionsApi.getNormalTxsHistory(
|
||||
baseUrl = apiUrl,
|
||||
accountAddress = accountAddress,
|
||||
pageNumber = page,
|
||||
pageSize = pageSize,
|
||||
chainId = chain.id
|
||||
)
|
||||
|
||||
val priceHistory = getPriceHistory(chainAsset, currency)
|
||||
|
||||
return response.result
|
||||
.map {
|
||||
val coinRate = priceHistory.findNearestCoinRate(it.timeStamp)
|
||||
mapRemoteNormalTxToOperation(it, chainAsset, accountAddress, coinRate)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override suspend fun fetchOperationsForBalanceChange(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
blockHash: String,
|
||||
accountId: AccountId,
|
||||
): List<RealtimeHistoryUpdate> {
|
||||
val ethereumApi = chainRegistry.getCallEthereumApiOrThrow(chain.id)
|
||||
|
||||
val block = ethereumApi.ethGetBlockByHash(blockHash, true).sendSuspend()
|
||||
val txs = block.block.transactions as List<TransactionResult<EthBlock.TransactionObject>>
|
||||
|
||||
return txs.mapNotNull {
|
||||
val tx = it.get()
|
||||
|
||||
val isTransfer = tx.input.removeHexPrefix().isEmpty()
|
||||
val relatesToUs = tx.relatesTo(accountId)
|
||||
|
||||
if (!(isTransfer && relatesToUs)) return@mapNotNull null
|
||||
|
||||
val txReceipt = ethereumApi.ethGetTransactionReceipt(tx.hash).sendSuspend().transactionReceipt.getOrNull()
|
||||
|
||||
RealtimeHistoryUpdate(
|
||||
status = txReceipt.extrinsicStatus(),
|
||||
txHash = tx.hash,
|
||||
type = RealtimeHistoryUpdate.Type.Transfer(
|
||||
senderId = chain.accountIdOf(tx.from),
|
||||
recipientId = chain.accountIdOf(tx.to),
|
||||
amountInPlanks = tx.value,
|
||||
chainAsset = chainAsset,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter> {
|
||||
return setOf(TransactionFilter.TRANSFER, TransactionFilter.EXTRINSIC)
|
||||
}
|
||||
|
||||
override fun isOperationSafe(operation: Operation): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun TransactionReceipt?.extrinsicStatus(): Operation.Status {
|
||||
return when (this?.isStatusOK) {
|
||||
true -> Operation.Status.COMPLETED
|
||||
false -> Operation.Status.FAILED
|
||||
null -> Operation.Status.PENDING
|
||||
}
|
||||
}
|
||||
|
||||
private fun Transaction.relatesTo(accountId: AccountId): Boolean {
|
||||
return from.ethAccountIdMatches(accountId) || to.ethAccountIdMatches(accountId)
|
||||
}
|
||||
|
||||
private fun String?.ethAccountIdMatches(other: AccountId): Boolean {
|
||||
return other.contentEquals(this?.ethereumAddressToAccountId())
|
||||
}
|
||||
|
||||
private fun mapRemoteNormalTxToOperation(
|
||||
remote: EtherscanNormalTxResponse,
|
||||
chainAsset: Chain.Asset,
|
||||
accountAddress: String,
|
||||
coinRate: CoinRate?
|
||||
): Operation {
|
||||
val type = if (remote.isTransfer) {
|
||||
mapNativeTransferToTransfer(remote, accountAddress, chainAsset, coinRate)
|
||||
} else {
|
||||
mapContractCallToExtrinsic(remote, chainAsset, coinRate)
|
||||
}
|
||||
|
||||
return Operation(
|
||||
id = remote.hash,
|
||||
address = accountAddress,
|
||||
type = type,
|
||||
time = remote.timeStamp.seconds.inWholeMilliseconds,
|
||||
chainAsset = chainAsset,
|
||||
extrinsicHash = remote.hash,
|
||||
status = remote.operationStatus(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapNativeTransferToTransfer(
|
||||
remote: EtherscanNormalTxResponse,
|
||||
accountAddress: String,
|
||||
chainAsset: Chain.Asset,
|
||||
coinRate: CoinRate?
|
||||
): Operation.Type.Transfer {
|
||||
return Operation.Type.Transfer(
|
||||
myAddress = accountAddress,
|
||||
amount = remote.value,
|
||||
fiatAmount = coinRate?.convertPlanks(chainAsset, remote.value),
|
||||
receiver = remote.to,
|
||||
sender = remote.from,
|
||||
fee = remote.feeUsed
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapContractCallToExtrinsic(
|
||||
remote: EtherscanNormalTxResponse,
|
||||
chainAsset: Chain.Asset,
|
||||
coinRate: CoinRate?
|
||||
): Operation.Type.Extrinsic {
|
||||
return Operation.Type.Extrinsic(
|
||||
content = Content.ContractCall(
|
||||
contractAddress = remote.to,
|
||||
function = remote.functionName,
|
||||
),
|
||||
fee = remote.feeUsed,
|
||||
fiatFee = coinRate?.convertPlanks(chainAsset, remote.feeUsed),
|
||||
)
|
||||
}
|
||||
|
||||
private fun EtherscanNormalTxResponse.operationStatus(): Operation.Status {
|
||||
return if (txReceiptStatus == BigInteger.ONE) {
|
||||
Operation.Status.COMPLETED
|
||||
} else {
|
||||
Operation.Status.FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.orml
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.currenciesOrNull
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.common.utils.tokens
|
||||
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
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Factory.Source
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.asSource
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.SubstrateAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage
|
||||
import io.novafoundation.nova.runtime.ext.findAssetByOrmlCurrencyId
|
||||
import io.novafoundation.nova.runtime.ext.hydraDxSupported
|
||||
import io.novafoundation.nova.runtime.ext.isSwapSupported
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.call
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.callOrNull
|
||||
|
||||
class OrmlAssetHistory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
realtimeOperationFetcherFactory: SubstrateRealtimeOperationFetcher.Factory,
|
||||
walletOperationsApi: SubQueryOperationsApi,
|
||||
cursorStorage: TransferCursorStorage,
|
||||
coinPriceRepository: CoinPriceRepository
|
||||
) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) {
|
||||
|
||||
override fun realtimeFetcherSources(chain: Chain): List<Source> {
|
||||
return buildList {
|
||||
add(TransferExtractor().asSource())
|
||||
|
||||
if (chain.swap.hydraDxSupported()) {
|
||||
add(Source.Known.Id.HYDRA_DX_SWAP.asSource())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter> {
|
||||
return setOfNotNull(
|
||||
TransactionFilter.TRANSFER,
|
||||
TransactionFilter.EXTRINSIC.takeIf { asset.isUtilityAsset },
|
||||
TransactionFilter.SWAP.takeIf { chain.isSwapSupported() }
|
||||
)
|
||||
}
|
||||
|
||||
private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor {
|
||||
|
||||
override suspend fun extractRealtimeHistoryUpdates(
|
||||
extrinsicVisit: ExtrinsicVisit,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): RealtimeHistoryUpdate.Type? {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
val call = extrinsicVisit.call
|
||||
if (!call.isTransfer(runtime)) return null
|
||||
|
||||
val inferredAsset = chain.findAssetByOrmlCurrencyId(runtime, call.arguments["currency_id"]) ?: return null
|
||||
val amount = bindNumber(call.arguments["amount"])
|
||||
|
||||
return RealtimeHistoryUpdate.Type.Transfer(
|
||||
senderId = extrinsicVisit.origin,
|
||||
recipientId = bindAccountIdentifier(call.arguments["dest"]),
|
||||
amountInPlanks = amount,
|
||||
chainAsset = inferredAsset,
|
||||
)
|
||||
}
|
||||
|
||||
private fun GenericCall.Instance.isTransfer(runtime: RuntimeSnapshot): Boolean {
|
||||
val transferCall = runtime.metadata.currenciesOrNull()?.callOrNull("transfer")
|
||||
?: runtime.metadata.tokens().call("transfer")
|
||||
|
||||
return instanceOf(transferCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate
|
||||
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.bindMultiLocation
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
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
|
||||
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_xcm_api.converter.MultiLocationConverter
|
||||
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverterFactory
|
||||
import io.novafoundation.nova.runtime.ext.commissionAsset
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.assetTxFeePaidEvent
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvents
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.requireNativeFee
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class AssetConversionSwapExtractor(
|
||||
private val multiLocationConverterFactory: MultiLocationConverterFactory,
|
||||
) : SubstrateRealtimeOperationFetcher.Extractor {
|
||||
|
||||
private val calls = listOf("swap_exact_tokens_for_tokens", "swap_tokens_for_exact_tokens")
|
||||
|
||||
override suspend fun extractRealtimeHistoryUpdates(
|
||||
extrinsicVisit: ExtrinsicVisit,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): RealtimeHistoryUpdate.Type? {
|
||||
val call = extrinsicVisit.call
|
||||
val callArgs = call.arguments
|
||||
|
||||
if (!call.isSwap()) return null
|
||||
|
||||
val scope = CoroutineScope(coroutineContext)
|
||||
val multiLocationConverter = multiLocationConverterFactory.defaultAsync(chain, scope)
|
||||
|
||||
val path = bindList(callArgs["path"], ::bindMultiLocation)
|
||||
val assetIn = multiLocationConverter.toChainAsset(path.first()) ?: return null
|
||||
val assetOut = multiLocationConverter.toChainAsset(path.last()) ?: return null
|
||||
|
||||
val (amountIn, amountOut) = extrinsicVisit.extractSwapAmounts()
|
||||
|
||||
val sendTo = bindAccountId(callArgs["send_to"])
|
||||
|
||||
val fee = extrinsicVisit.extractFee(chain, multiLocationConverter)
|
||||
|
||||
return RealtimeHistoryUpdate.Type.Swap(
|
||||
amountIn = ChainAssetWithAmount(assetIn, amountIn),
|
||||
amountOut = ChainAssetWithAmount(assetOut, amountOut),
|
||||
amountFee = fee,
|
||||
senderId = extrinsicVisit.origin,
|
||||
receiverId = sendTo
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExtrinsicVisit.extractSwapAmounts(): Pair<Balance, Balance> {
|
||||
// We check for custom fee usage from root extrinsic since `extrinsicVisit` will cut it out when nested calls are present
|
||||
val isCustomFeeTokenUsed = rootExtrinsic.events.assetTxFeePaidEvent() != null
|
||||
val allSwaps = events.findEvents(Modules.ASSET_CONVERSION, "SwapExecuted")
|
||||
|
||||
val swapExecutedEvent = when {
|
||||
!success -> null // we wont be able to extract swap from event
|
||||
|
||||
isCustomFeeTokenUsed -> {
|
||||
// Swaps with custom fee token produce up to free SwapExecuted events, in the following order:
|
||||
// SwapExecuted (Swap custom token fee to native token) - always present
|
||||
// SwapExecuted (Real swap) - always present
|
||||
// SwapExecuted (Refund remaining fee back to custom token)
|
||||
// So we need to take the middle one
|
||||
|
||||
allSwaps.getOrNull(1)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Only one swap is possible in case
|
||||
allSwaps.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
// successful swap, extract from event
|
||||
swapExecutedEvent != null -> {
|
||||
val (_, _, amountIn, amountOut) = swapExecutedEvent.arguments
|
||||
|
||||
bindNumber(amountIn) to bindNumber(amountOut)
|
||||
}
|
||||
|
||||
// failed swap, extract from call args
|
||||
call.function.name == "swap_exact_tokens_for_tokens" -> {
|
||||
val amountIn = bindNumber(call.arguments["amount_in"])
|
||||
val amountOutMin = bindNumber(call.arguments["amount_out_min"])
|
||||
|
||||
amountIn to amountOutMin
|
||||
}
|
||||
|
||||
call.function.name == "swap_tokens_for_exact_tokens" -> {
|
||||
val amountOut = bindNumber(call.arguments["amount_out"])
|
||||
val amountInMax = bindNumber(call.arguments["amount_in_max"])
|
||||
|
||||
amountInMax to amountOut
|
||||
}
|
||||
|
||||
else -> error("Unknown call")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicVisit.extractFee(
|
||||
chain: Chain,
|
||||
multiLocationConverter: MultiLocationConverter
|
||||
): ChainAssetWithAmount {
|
||||
// We check for fee usage from root extrinsic since `extrinsicVisit` will cut it out when nested calls are present
|
||||
val assetFee = rootExtrinsic.events.assetFee(multiLocationConverter)
|
||||
if (assetFee != null) return assetFee
|
||||
|
||||
val nativeFee = rootExtrinsic.events.requireNativeFee()
|
||||
return ChainAssetWithAmount(chain.commissionAsset, nativeFee)
|
||||
}
|
||||
|
||||
private fun GenericCall.Instance.isSwap(): Boolean {
|
||||
return module.name == Modules.ASSET_CONVERSION &&
|
||||
function.name in calls
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate
|
||||
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.bindMultiLocation
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.assetTxFeePaidEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
|
||||
suspend fun List<GenericEvent.Instance>.assetFee(multiLocationConverter: MultiLocationConverter): ChainAssetWithAmount? {
|
||||
val event = assetTxFeePaidEvent() ?: return null
|
||||
val (_, actualFee, tip, assetId) = event.arguments
|
||||
val totalFee = bindNumber(actualFee) + bindNumber(tip)
|
||||
val chainAsset = multiLocationConverter.toChainAsset(bindMultiLocation(assetId)) ?: return null
|
||||
|
||||
return ChainAssetWithAmount(chainAsset, totalFee)
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate
|
||||
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter
|
||||
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
|
||||
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.feature_wallet_api.domain.model.Operation
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate.hydraDx.HydraDxOmniPoolSwapExtractor
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate.hydraDx.HydraDxRouterSwapExtractor
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicWalk
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.walkToList
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.feature_xcm_api.converter.MultiLocationConverterFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository
|
||||
|
||||
internal class SubstrateRealtimeOperationFetcherFactory(
|
||||
private val multiLocationConverterFactory: MultiLocationConverterFactory,
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
private val eventsRepository: EventsRepository,
|
||||
private val extrinsicWalk: ExtrinsicWalk,
|
||||
) : Factory {
|
||||
|
||||
override fun create(sources: List<Factory.Source>): SubstrateRealtimeOperationFetcher {
|
||||
val extractors = sources.flatMap { it.extractors() }
|
||||
|
||||
return RealSubstrateRealtimeOperationFetcher(eventsRepository, extractors, extrinsicWalk)
|
||||
}
|
||||
|
||||
private fun Factory.Source.extractors(): List<Extractor> {
|
||||
return when (this) {
|
||||
is Factory.Source.FromExtractor -> listOf(extractor)
|
||||
is Factory.Source.Known -> id.extractors()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Factory.Source.Known.Id.extractors(): List<Extractor> {
|
||||
return when (this) {
|
||||
Factory.Source.Known.Id.ASSET_CONVERSION_SWAP -> listOf(assetConversionSwap())
|
||||
Factory.Source.Known.Id.HYDRA_DX_SWAP -> listOf(hydraDxOmniPoolSwap(), hydraDxRouterSwap())
|
||||
}
|
||||
}
|
||||
|
||||
private fun assetConversionSwap(): Extractor {
|
||||
return AssetConversionSwapExtractor(multiLocationConverterFactory)
|
||||
}
|
||||
|
||||
private fun hydraDxOmniPoolSwap(): Extractor {
|
||||
return HydraDxOmniPoolSwapExtractor(hydraDxAssetIdConverter)
|
||||
}
|
||||
|
||||
private fun hydraDxRouterSwap(): Extractor {
|
||||
return HydraDxRouterSwapExtractor(hydraDxAssetIdConverter)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealSubstrateRealtimeOperationFetcher(
|
||||
private val repository: EventsRepository,
|
||||
private val extractors: List<Extractor>,
|
||||
private val callWalk: ExtrinsicWalk,
|
||||
) : SubstrateRealtimeOperationFetcher {
|
||||
|
||||
override suspend fun extractRealtimeHistoryUpdates(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
blockHash: String
|
||||
): List<RealtimeHistoryUpdate> {
|
||||
val extrinsicWithEvents = repository.getBlockEvents(chain.id, blockHash).applyExtrinsic
|
||||
|
||||
return extrinsicWithEvents.flatMap { extrinsic ->
|
||||
val visits = runCatching { callWalk.walkToList(extrinsic, chain.id) }.getOrElse { emptyList() }
|
||||
|
||||
visits.flatMap { extrinsicVisit ->
|
||||
extractors.mapNotNull {
|
||||
val type = runCatching { it.extractRealtimeHistoryUpdates(extrinsicVisit, chain, chainAsset) }.getOrNull() ?: return@mapNotNull null
|
||||
|
||||
RealtimeHistoryUpdate(
|
||||
txHash = extrinsic.extrinsicHash,
|
||||
status = Operation.Status.fromSuccess(extrinsicVisit.success),
|
||||
type = type
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate.hydraDx
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetId
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter
|
||||
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
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmount
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findLastEvent
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.requireNativeFee
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
abstract class BaseHydraDxSwapExtractor(
|
||||
private val hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
) : SubstrateRealtimeOperationFetcher.Extractor {
|
||||
|
||||
abstract fun isSwap(call: GenericCall.Instance): Boolean
|
||||
|
||||
protected abstract fun ExtrinsicVisit.extractSwapArgs(): SwapArgs
|
||||
|
||||
override suspend fun extractRealtimeHistoryUpdates(
|
||||
extrinsicVisit: ExtrinsicVisit,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): RealtimeHistoryUpdate.Type? {
|
||||
if (!isSwap(extrinsicVisit.call)) return null
|
||||
|
||||
val (assetIdIn, assetIdOut, amountIn, amountOut) = extrinsicVisit.extractSwapArgs()
|
||||
|
||||
val assetIn = hydraDxAssetIdConverter.toChainAssetOrNull(chain, assetIdIn) ?: return null
|
||||
val assetOut = hydraDxAssetIdConverter.toChainAssetOrNull(chain, assetIdOut) ?: return null
|
||||
|
||||
val fee = extrinsicVisit.extractFee(chain)
|
||||
|
||||
return RealtimeHistoryUpdate.Type.Swap(
|
||||
amountIn = ChainAssetWithAmount(assetIn, amountIn),
|
||||
amountOut = ChainAssetWithAmount(assetOut, amountOut),
|
||||
amountFee = fee,
|
||||
senderId = extrinsicVisit.origin,
|
||||
receiverId = extrinsicVisit.origin
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicVisit.extractFee(chain: Chain): ChainAssetWithAmount {
|
||||
val feeDepositEvent = rootExtrinsic.events.findLastEvent(Modules.CURRENCIES, "Deposited") ?: return nativeFee(chain)
|
||||
|
||||
val (currencyIdRaw, _, amountRaw) = feeDepositEvent.arguments
|
||||
val currencyId = bindNumber(currencyIdRaw)
|
||||
|
||||
val feeAsset = hydraDxAssetIdConverter.toChainAssetOrNull(chain, currencyId) ?: return nativeFee(chain)
|
||||
|
||||
return ChainAssetWithAmount(feeAsset, bindNumber(amountRaw))
|
||||
}
|
||||
|
||||
private fun ExtrinsicVisit.nativeFee(chain: Chain): ChainAssetWithAmount {
|
||||
return ChainAssetWithAmount(chain.utilityAsset, rootExtrinsic.events.requireNativeFee())
|
||||
}
|
||||
|
||||
protected data class SwapArgs(
|
||||
val assetIn: HydraDxAssetId,
|
||||
val assetOut: HydraDxAssetId,
|
||||
val amountIn: HydraDxAssetId,
|
||||
val amountOut: HydraDxAssetId
|
||||
)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate.hydraDx
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
class HydraDxOmniPoolSwapExtractor(
|
||||
hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
) : BaseHydraDxSwapExtractor(hydraDxAssetIdConverter) {
|
||||
|
||||
private val calls = listOf("buy", "sell")
|
||||
|
||||
override fun isSwap(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.OMNIPOOL &&
|
||||
call.function.name in calls
|
||||
}
|
||||
|
||||
override fun ExtrinsicVisit.extractSwapArgs(): SwapArgs {
|
||||
val swapExecutedEvent = events.findEvent(Modules.OMNIPOOL, "BuyExecuted")
|
||||
?: events.findEvent(Modules.OMNIPOOL, "SellExecuted")
|
||||
|
||||
return when {
|
||||
// successful swap, extract from event
|
||||
swapExecutedEvent != null -> {
|
||||
val (_, assetIn, assetOut, amountIn, amountOut) = swapExecutedEvent.arguments
|
||||
|
||||
SwapArgs(
|
||||
assetIn = bindNumber(assetIn),
|
||||
assetOut = bindNumber(assetOut),
|
||||
amountIn = bindNumber(amountIn),
|
||||
amountOut = bindNumber(amountOut)
|
||||
)
|
||||
}
|
||||
|
||||
// failed swap, extract from call args
|
||||
call.function.name == "sell" -> {
|
||||
SwapArgs(
|
||||
assetIn = bindNumber(call.arguments["asset_in"]),
|
||||
assetOut = bindNumber(call.arguments["asset_out"]),
|
||||
amountIn = bindNumber(call.arguments["amount"]),
|
||||
amountOut = bindNumber(call.arguments["min_buy_amount"])
|
||||
)
|
||||
}
|
||||
|
||||
call.function.name == "buy" -> {
|
||||
SwapArgs(
|
||||
assetIn = bindNumber(call.arguments["asset_in"]),
|
||||
assetOut = bindNumber(call.arguments["asset_out"]),
|
||||
amountIn = bindNumber(call.arguments["max_sell_amount"]),
|
||||
amountOut = bindNumber(call.arguments["amount"])
|
||||
)
|
||||
}
|
||||
|
||||
else -> error("Unknown call")
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate.hydraDx
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.feature_swap_core_api.data.network.HydraDxAssetIdConverter
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
class HydraDxRouterSwapExtractor(
|
||||
hydraDxAssetIdConverter: HydraDxAssetIdConverter,
|
||||
) : BaseHydraDxSwapExtractor(hydraDxAssetIdConverter) {
|
||||
|
||||
private val calls = listOf("buy", "sell")
|
||||
|
||||
override fun isSwap(call: GenericCall.Instance): Boolean {
|
||||
return call.module.name == Modules.ROUTER &&
|
||||
call.function.name in calls
|
||||
}
|
||||
|
||||
override fun ExtrinsicVisit.extractSwapArgs(): SwapArgs {
|
||||
val swapExecutedEvent = events.findEvent(Modules.ROUTER, "RouteExecuted")
|
||||
?: events.findEvent(Modules.ROUTER, "Executed")
|
||||
|
||||
return when {
|
||||
// successful swap, extract from event
|
||||
swapExecutedEvent != null -> {
|
||||
val (assetIn, assetOut, amountIn, amountOut) = swapExecutedEvent.arguments
|
||||
|
||||
SwapArgs(
|
||||
assetIn = bindNumber(assetIn),
|
||||
assetOut = bindNumber(assetOut),
|
||||
amountIn = bindNumber(amountIn),
|
||||
amountOut = bindNumber(amountOut)
|
||||
)
|
||||
}
|
||||
|
||||
// failed swap, extract from call args
|
||||
call.function.name == "sell" -> {
|
||||
SwapArgs(
|
||||
assetIn = bindNumber(call.arguments["asset_in"]),
|
||||
assetOut = bindNumber(call.arguments["asset_out"]),
|
||||
amountIn = bindNumber(call.arguments["amount_in"]),
|
||||
amountOut = bindNumber(call.arguments["min_amount_out"])
|
||||
)
|
||||
}
|
||||
|
||||
call.function.name == "buy" -> {
|
||||
SwapArgs(
|
||||
assetIn = bindNumber(call.arguments["asset_in"]),
|
||||
assetOut = bindNumber(call.arguments["asset_out"]),
|
||||
amountIn = bindNumber(call.arguments["max_amount_in"]),
|
||||
amountOut = bindNumber(call.arguments["amount_out"])
|
||||
)
|
||||
}
|
||||
|
||||
else -> error("Unknown call")
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.statemine
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.oneOf
|
||||
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
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Factory.Source
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.asSource
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.SubstrateAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage
|
||||
import io.novafoundation.nova.runtime.ext.assetConversionSupported
|
||||
import io.novafoundation.nova.runtime.ext.isSwapSupported
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.ext.palletNameOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.requireStatemine
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.hasSameId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.call
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module
|
||||
|
||||
class StatemineAssetHistory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
realtimeOperationFetcherFactory: SubstrateRealtimeOperationFetcher.Factory,
|
||||
walletOperationsApi: SubQueryOperationsApi,
|
||||
cursorStorage: TransferCursorStorage,
|
||||
coinPriceRepository: CoinPriceRepository
|
||||
) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) {
|
||||
|
||||
override fun realtimeFetcherSources(chain: Chain): List<Source> {
|
||||
return buildList {
|
||||
add(TransferExtractor().asSource())
|
||||
|
||||
if (chain.swap.assetConversionSupported()) {
|
||||
add(Source.Known.Id.ASSET_CONVERSION_SWAP.asSource())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter> {
|
||||
return setOfNotNull(
|
||||
TransactionFilter.TRANSFER,
|
||||
TransactionFilter.EXTRINSIC.takeIf { asset.isUtilityAsset },
|
||||
TransactionFilter.SWAP.takeIf { chain.isSwapSupported() }
|
||||
)
|
||||
}
|
||||
|
||||
private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor {
|
||||
|
||||
override suspend fun extractRealtimeHistoryUpdates(
|
||||
extrinsicVisit: ExtrinsicVisit,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): RealtimeHistoryUpdate.Type? {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
val call = extrinsicVisit.call
|
||||
if (!call.isTransfer(runtime, chainAsset)) return null
|
||||
|
||||
val amount = bindNumber(call.arguments["amount"])
|
||||
|
||||
return RealtimeHistoryUpdate.Type.Transfer(
|
||||
senderId = extrinsicVisit.origin,
|
||||
recipientId = bindAccountIdentifier(call.arguments["target"]),
|
||||
amountInPlanks = amount,
|
||||
chainAsset = chainAsset,
|
||||
)
|
||||
}
|
||||
|
||||
private fun GenericCall.Instance.isTransfer(runtime: RuntimeSnapshot, chainAsset: Chain.Asset): Boolean {
|
||||
val statemineType = chainAsset.requireStatemine()
|
||||
val moduleName = statemineType.palletNameOrDefault()
|
||||
val module = runtime.metadata.module(moduleName)
|
||||
|
||||
val matchingCall = oneOf(
|
||||
module.call("transfer"),
|
||||
module.call("transfer_keep_alive"),
|
||||
)
|
||||
|
||||
return matchingCall && statemineType.hasSameId(runtime, dynamicInstanceId = arguments["id"])
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.utility
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.balances
|
||||
import io.novafoundation.nova.common.utils.oneOf
|
||||
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
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcher.Factory.Source
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.substrate.asSource
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.SubstrateAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.subquery.SubQueryOperationsApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.storage.TransferCursorStorage
|
||||
import io.novafoundation.nova.runtime.ext.assetConversionSupported
|
||||
import io.novafoundation.nova.runtime.ext.hydraDxSupported
|
||||
import io.novafoundation.nova.runtime.ext.isSwapSupported
|
||||
import io.novafoundation.nova.runtime.extrinsic.visitor.extrinsic.api.ExtrinsicVisit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.callOrNull
|
||||
|
||||
class NativeAssetHistory(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
realtimeOperationFetcherFactory: SubstrateRealtimeOperationFetcher.Factory,
|
||||
walletOperationsApi: SubQueryOperationsApi,
|
||||
cursorStorage: TransferCursorStorage,
|
||||
coinPriceRepository: CoinPriceRepository
|
||||
) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) {
|
||||
|
||||
override fun realtimeFetcherSources(chain: Chain): List<Source> {
|
||||
return buildList {
|
||||
add(TransferExtractor().asSource())
|
||||
|
||||
if (chain.swap.assetConversionSupported()) {
|
||||
Source.Known.Id.ASSET_CONVERSION_SWAP.asSource()
|
||||
}
|
||||
|
||||
if (chain.swap.hydraDxSupported()) {
|
||||
add(Source.Known.Id.HYDRA_DX_SWAP.asSource())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun availableOperationFilters(chain: Chain, asset: Chain.Asset): Set<TransactionFilter> {
|
||||
return setOfNotNull(
|
||||
TransactionFilter.TRANSFER,
|
||||
TransactionFilter.EXTRINSIC,
|
||||
TransactionFilter.REWARD,
|
||||
TransactionFilter.SWAP.takeIf { chain.isSwapSupported() }
|
||||
)
|
||||
}
|
||||
|
||||
private inner class TransferExtractor : SubstrateRealtimeOperationFetcher.Extractor {
|
||||
|
||||
override suspend fun extractRealtimeHistoryUpdates(
|
||||
extrinsicVisit: ExtrinsicVisit,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset
|
||||
): RealtimeHistoryUpdate.Type? {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
|
||||
val call = extrinsicVisit.call
|
||||
if (!call.isTransfer(runtime)) return null
|
||||
|
||||
val transferEvent = extrinsicVisit.events.findEvent(Modules.BALANCES, "Transfer") ?: return null
|
||||
val (_, toRaw, amountRaw) = transferEvent.arguments
|
||||
|
||||
return RealtimeHistoryUpdate.Type.Transfer(
|
||||
senderId = extrinsicVisit.origin,
|
||||
recipientId = bindAccountIdentifier(toRaw),
|
||||
amountInPlanks = bindNumber(amountRaw),
|
||||
chainAsset = chainAsset,
|
||||
)
|
||||
}
|
||||
|
||||
private fun GenericCall.Instance.isTransfer(runtime: RuntimeSnapshot): Boolean {
|
||||
val balances = runtime.metadata.balances()
|
||||
|
||||
return oneOf(
|
||||
balances.callOrNull("transfer"),
|
||||
balances.callOrNull("transfer_keep_alive"),
|
||||
balances.callOrNull("transfer_allow_death"),
|
||||
balances.callOrNull("transfer_all")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin
|
||||
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.extrinsic.createDefault
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder
|
||||
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 io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDepositInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInCommissionAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notPhishingRecipient
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientCommissionBalanceToStayAboveED
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress
|
||||
import io.novafoundation.nova.feature_wallet_impl.domain.validaiton.recipientCanAcceptTransfer
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.callOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.moduleOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
abstract class BaseAssetTransfers(
|
||||
internal val chainRegistry: ChainRegistry,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val extrinsicServiceFactory: ExtrinsicService.Factory,
|
||||
private val phishingValidationFactory: PhishingValidationFactory,
|
||||
private val enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory
|
||||
) : AssetTransfers {
|
||||
|
||||
protected abstract fun ExtrinsicBuilder.transfer(transfer: AssetTransfer)
|
||||
|
||||
/**
|
||||
* Format: [(Module, Function)]
|
||||
* Transfers will be enabled if at least one function exists
|
||||
*/
|
||||
protected abstract suspend fun transferFunctions(chainAsset: Chain.Asset): List<Pair<String, String>>
|
||||
|
||||
override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission> {
|
||||
val submissionOptions = ExtrinsicService.SubmissionOptions(transfer.feePaymentCurrency)
|
||||
|
||||
return extrinsicServiceFactory
|
||||
.createDefault(coroutineScope)
|
||||
.submitExtrinsic(transfer.originChain, transfer.sender.intoOrigin(), submissionOptions = submissionOptions) {
|
||||
transfer(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performTransferAndAwaitExecution(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<TransactionExecution> {
|
||||
val submissionOptions = ExtrinsicService.SubmissionOptions(transfer.feePaymentCurrency)
|
||||
|
||||
return extrinsicServiceFactory
|
||||
.createDefault(coroutineScope)
|
||||
.submitExtrinsicAndAwaitExecution(transfer.originChain, transfer.sender.intoOrigin(), submissionOptions = submissionOptions) {
|
||||
transfer(transfer)
|
||||
}
|
||||
.requireOk()
|
||||
.map(TransactionExecution::Substrate)
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee {
|
||||
val submissionOptions = ExtrinsicService.SubmissionOptions(transfer.feePaymentCurrency)
|
||||
|
||||
return extrinsicServiceFactory
|
||||
.createDefault(coroutineScope)
|
||||
.estimateFee(transfer.originChain, transfer.sender.intoOrigin(), submissionOptions = submissionOptions) {
|
||||
transfer(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
val runtime = chainRegistry.getRuntime(chainAsset.chainId)
|
||||
|
||||
return transferFunctions(chainAsset).any { (module, function) ->
|
||||
runtime.metadata.moduleOrNull(module)?.callOrNull(function) != null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope) = ValidationSystem {
|
||||
validAddress()
|
||||
recipientIsNotSystemAccount()
|
||||
|
||||
notPhishingRecipient(phishingValidationFactory)
|
||||
|
||||
positiveAmount()
|
||||
|
||||
sufficientBalanceInUsedAsset()
|
||||
sufficientTransferableBalanceToPayOriginFee()
|
||||
|
||||
sufficientCommissionBalanceToStayAboveED(enoughTotalToStayAboveEDValidationFactory)
|
||||
|
||||
notDeadRecipientInUsedAsset(assetSourceRegistry)
|
||||
notDeadRecipientInCommissionAsset(assetSourceRegistry)
|
||||
|
||||
doNotCrossExistentialDeposit()
|
||||
|
||||
recipientCanAcceptTransfer(assetSourceRegistry)
|
||||
}
|
||||
|
||||
private fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit() = doNotCrossExistentialDepositInUsedAsset(
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
extraAmount = { it.transfer.amount },
|
||||
)
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers
|
||||
|
||||
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.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem
|
||||
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 io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
open class UnsupportedAssetTransfers : AssetTransfers {
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope): AssetTransfersValidationSystem {
|
||||
throw UnsupportedOperationException("Unsupported")
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee {
|
||||
throw UnsupportedOperationException("Unsupported")
|
||||
}
|
||||
|
||||
override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission> {
|
||||
return Result.failure(UnsupportedOperationException("Unsupported"))
|
||||
}
|
||||
|
||||
override suspend fun performTransferAndAwaitExecution(
|
||||
transfer: WeightedAssetTransfer,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<TransactionExecution> {
|
||||
return Result.failure(UnsupportedOperationException("Unsupported"))
|
||||
}
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.equilibrium
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindBoolean
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.returnType
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.eqBalances
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.BaseAssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress
|
||||
import io.novafoundation.nova.feature_wallet_impl.domain.validaiton.recipientCanAcceptTransfer
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.requireEquilibrium
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHexOrNull
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
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.module.StorageEntry
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
private const val TRANSFER_CALL = "transfer"
|
||||
|
||||
class EquilibriumAssetTransfers(
|
||||
chainRegistry: ChainRegistry,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
extrinsicServiceFactory: ExtrinsicService.Factory,
|
||||
phishingValidationFactory: PhishingValidationFactory,
|
||||
private val remoteStorageSource: StorageDataSource,
|
||||
private val enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory
|
||||
) : BaseAssetTransfers(chainRegistry, assetSourceRegistry, extrinsicServiceFactory, phishingValidationFactory, enoughTotalToStayAboveEDValidationFactory) {
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope) = ValidationSystem {
|
||||
validAddress()
|
||||
recipientIsNotSystemAccount()
|
||||
|
||||
positiveAmount()
|
||||
sufficientBalanceInUsedAsset()
|
||||
sufficientTransferableBalanceToPayOriginFee()
|
||||
|
||||
notDeadRecipientInUsedAsset(assetSourceRegistry)
|
||||
recipientCanAcceptTransfer(assetSourceRegistry)
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun ExtrinsicBuilder.transfer(transfer: AssetTransfer) {
|
||||
if (transfer.originChainAsset.type !is Chain.Asset.Type.Equilibrium) return
|
||||
|
||||
val accountId = transfer.originChain.accountIdOrDefault(transfer.recipient)
|
||||
val amount = transfer.originChainAsset.planksFromAmount(transfer.amount)
|
||||
|
||||
call(
|
||||
moduleName = Modules.EQ_BALANCES,
|
||||
callName = TRANSFER_CALL,
|
||||
arguments = mapOf(
|
||||
"asset" to transfer.originChainAsset.requireEquilibrium().id,
|
||||
"to" to accountId,
|
||||
"value" to amount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun transferFunctions(chainAsset: Chain.Asset): List<Pair<String, String>> {
|
||||
return listOf(Modules.EQ_BALANCES to TRANSFER_CALL)
|
||||
}
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
if (chainAsset.type !is Chain.Asset.Type.Equilibrium) return false
|
||||
|
||||
return queryIsTransferEnabledStorage(chainAsset) ?: super.areTransfersEnabled(chainAsset)
|
||||
}
|
||||
|
||||
private suspend fun queryIsTransferEnabledStorage(chainAsset: Chain.Asset): Boolean? {
|
||||
return remoteStorageSource.query(
|
||||
chainAsset.chainId,
|
||||
keyBuilder = { it.getTransferEnabledStorage().storageKey() },
|
||||
binding = { scale, runtimeSnapshot ->
|
||||
if (scale == null) return@query null
|
||||
val returnType = runtimeSnapshot.getTransferEnabledStorage().returnType()
|
||||
bindBoolean(returnType.fromHexOrNull(runtimeSnapshot, scale))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun RuntimeSnapshot.getTransferEnabledStorage(): StorageEntry {
|
||||
return metadata.eqBalances().storage("IsTransfersEnabled")
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.evmErc20
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin
|
||||
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.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers
|
||||
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 io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.checkForFeeChanges
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress
|
||||
import io.novafoundation.nova.feature_wallet_impl.domain.validaiton.recipientCanAcceptTransfer
|
||||
import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard
|
||||
import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder
|
||||
import io.novafoundation.nova.runtime.ethereum.transaction.builder.contractCall
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.requireErc20
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
// a conservative upper limit. Usually transfer takes around 30-50k
|
||||
private val ERC_20_UPPER_GAS_LIMIT = 200_000.toBigInteger()
|
||||
|
||||
class EvmErc20AssetTransfers(
|
||||
private val evmTransactionService: EvmTransactionService,
|
||||
private val erc20Standard: Erc20Standard,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : AssetTransfers {
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope) = ValidationSystem {
|
||||
validAddress()
|
||||
recipientIsNotSystemAccount()
|
||||
|
||||
positiveAmount()
|
||||
|
||||
sufficientBalanceInUsedAsset()
|
||||
sufficientTransferableBalanceToPayOriginFee()
|
||||
|
||||
recipientCanAcceptTransfer(assetSourceRegistry)
|
||||
|
||||
checkForFeeChanges(assetSourceRegistry, coroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee {
|
||||
return evmTransactionService.calculateFee(
|
||||
transfer.originChain.id,
|
||||
fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT,
|
||||
origin = transfer.sender.intoOrigin()
|
||||
) {
|
||||
transfer(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission> {
|
||||
return evmTransactionService.transact(
|
||||
chainId = transfer.originChain.id,
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT,
|
||||
origin = transfer.sender.intoOrigin()
|
||||
) {
|
||||
transfer(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performTransferAndAwaitExecution(
|
||||
transfer: WeightedAssetTransfer,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<TransactionExecution> {
|
||||
return evmTransactionService.transactAndAwaitExecution(
|
||||
chainId = transfer.originChain.id,
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT,
|
||||
origin = transfer.sender.intoOrigin()
|
||||
) {
|
||||
transfer(transfer)
|
||||
}.map { TransactionExecution.Ethereum(it) }
|
||||
}
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun EvmTransactionBuilder.transfer(transfer: AssetTransfer) {
|
||||
val erc20 = transfer.originChainAsset.requireErc20()
|
||||
val recipient = transfer.originChain.accountIdOrDefault(transfer.recipient)
|
||||
|
||||
contractCall(erc20.contractAddress, erc20Standard) {
|
||||
transfer(recipient = recipient, amount = transfer.amountInPlanks)
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.evmNative
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin
|
||||
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.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.TransactionExecution
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.checkForFeeChanges
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress
|
||||
import io.novafoundation.nova.feature_wallet_impl.domain.validaiton.recipientCanAcceptTransfer
|
||||
import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
// native coin transfer has a fixed fee
|
||||
private val NATIVE_COIN_TRANSFER_GAS_LIMIT = 21_000.toBigInteger()
|
||||
|
||||
class EvmNativeAssetTransfers(
|
||||
private val evmTransactionService: EvmTransactionService,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : AssetTransfers {
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope) = ValidationSystem {
|
||||
validAddress()
|
||||
recipientIsNotSystemAccount()
|
||||
|
||||
positiveAmount()
|
||||
|
||||
sufficientBalanceInUsedAsset()
|
||||
sufficientTransferableBalanceToPayOriginFee()
|
||||
|
||||
recipientCanAcceptTransfer(assetSourceRegistry)
|
||||
|
||||
checkForFeeChanges(assetSourceRegistry, coroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee {
|
||||
return evmTransactionService.calculateFee(
|
||||
transfer.originChain.id,
|
||||
fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT,
|
||||
origin = transfer.sender.intoOrigin()
|
||||
) {
|
||||
nativeTransfer(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission> {
|
||||
return evmTransactionService.transact(
|
||||
chainId = transfer.originChain.id,
|
||||
fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT,
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
origin = transfer.sender.intoOrigin()
|
||||
) {
|
||||
nativeTransfer(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performTransferAndAwaitExecution(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<TransactionExecution> {
|
||||
return evmTransactionService.transactAndAwaitExecution(
|
||||
chainId = transfer.originChain.id,
|
||||
fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT,
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
origin = transfer.sender.intoOrigin()
|
||||
) {
|
||||
nativeTransfer(transfer)
|
||||
}.map { TransactionExecution.Ethereum(it) }
|
||||
}
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun EvmTransactionBuilder.nativeTransfer(transfer: AssetTransfer) {
|
||||
val recipient = transfer.originChain.accountIdOrDefault(transfer.recipient)
|
||||
|
||||
nativeTransfer(transfer.amountInPlanks, recipient)
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.orml
|
||||
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.firstExistingCall
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.BaseAssetTransfers
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.findAssetByOrmlCurrencyId
|
||||
import io.novafoundation.nova.runtime.ext.ormlCurrencyId
|
||||
import io.novafoundation.nova.runtime.ext.requireOrml
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novafoundation.nova.common.utils.PezkuwiAddressConstructor
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
|
||||
import java.math.BigInteger
|
||||
|
||||
open class OrmlAssetTransfers(
|
||||
chainRegistry: ChainRegistry,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
extrinsicServiceFactory: ExtrinsicService.Factory,
|
||||
phishingValidationFactory: PhishingValidationFactory,
|
||||
enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory
|
||||
) : BaseAssetTransfers(chainRegistry, assetSourceRegistry, extrinsicServiceFactory, phishingValidationFactory, enoughTotalToStayAboveEDValidationFactory) {
|
||||
|
||||
open val transferFunctions = listOf(
|
||||
Modules.CURRENCIES to "transfer",
|
||||
Modules.TOKENS to "transfer"
|
||||
)
|
||||
|
||||
override fun ExtrinsicBuilder.transfer(transfer: AssetTransfer) {
|
||||
ormlTransfer(
|
||||
chainAsset = transfer.originChainAsset,
|
||||
target = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
amount = transfer.amountInPlanks
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun transferFunctions(chainAsset: Chain.Asset) = transferFunctions
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
// flag from chains json AND existence of module & function in runtime metadata
|
||||
return chainAsset.requireOrml().transfersEnabled && super.areTransfersEnabled(chainAsset)
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
val isOurs = transferFunctions.any { call.instanceOf(it.first, it.second) }
|
||||
if (!isOurs) return null
|
||||
|
||||
val onChainAssetId = call.arguments["currency_id"]
|
||||
val chainAsset = determineAsset(chain, onChainAssetId) ?: return null
|
||||
val amount = bindNumber(call.arguments["amount"])
|
||||
val destination = bindAccountIdentifier(call.arguments["dest"]).intoKey()
|
||||
|
||||
return TransferParsedFromCall(
|
||||
amount = chainAsset.withAmount(amount),
|
||||
destination = destination
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun determineAsset(chain: Chain, onChainAssetId: Any?): Chain.Asset? {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
return chain.findAssetByOrmlCurrencyId(runtime, onChainAssetId)
|
||||
}
|
||||
|
||||
private fun ExtrinsicBuilder.ormlTransfer(
|
||||
chainAsset: Chain.Asset,
|
||||
target: AccountId,
|
||||
amount: BigInteger
|
||||
) {
|
||||
val (moduleIndex, callIndex) = runtime.metadata.firstExistingCall(transferFunctions).index
|
||||
|
||||
call(
|
||||
moduleIndex = moduleIndex,
|
||||
callIndex = callIndex,
|
||||
arguments = mapOf(
|
||||
"dest" to PezkuwiAddressConstructor.constructInstance(runtime.typeRegistry, target),
|
||||
"currency_id" to chainAsset.ormlCurrencyId(runtime),
|
||||
"amount" to amount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.orml.hydrationEvm
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.orml.OrmlAssetTransfers
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class HydrationEvmAssetTransfers @Inject constructor(
|
||||
chainRegistry: ChainRegistry,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
extrinsicServiceFactory: ExtrinsicService.Factory,
|
||||
phishingValidationFactory: PhishingValidationFactory,
|
||||
enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory
|
||||
) : OrmlAssetTransfers(
|
||||
chainRegistry = chainRegistry,
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
extrinsicServiceFactory = extrinsicServiceFactory,
|
||||
phishingValidationFactory = phishingValidationFactory,
|
||||
enoughTotalToStayAboveEDValidationFactory = enoughTotalToStayAboveEDValidationFactory
|
||||
) {
|
||||
|
||||
// Force Hydration Evm implementation to always use Currencies.transfer
|
||||
// Since Tokens.transfer fail for such tokens
|
||||
override val transferFunctions = listOf(
|
||||
Modules.CURRENCIES to "transfer",
|
||||
)
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.statemine
|
||||
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.statemine.canAcceptFunds
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common.bindAssetAccountOrEmpty
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.common.statemineModule
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.BaseAssetTransfers
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.findAssetByStatemineAssetId
|
||||
import io.novafoundation.nova.runtime.ext.findStatemineAssets
|
||||
import io.novafoundation.nova.runtime.ext.palletNameOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.requireStatemine
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.prepareIdForEncoding
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novafoundation.nova.common.utils.PezkuwiAddressConstructor
|
||||
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.storage
|
||||
import java.math.BigInteger
|
||||
|
||||
class StatemineAssetTransfers(
|
||||
chainRegistry: ChainRegistry,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
extrinsicServiceFactory: ExtrinsicService.Factory,
|
||||
phishingValidationFactory: PhishingValidationFactory,
|
||||
enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory,
|
||||
private val remoteStorage: StorageDataSource
|
||||
) : BaseAssetTransfers(chainRegistry, assetSourceRegistry, extrinsicServiceFactory, phishingValidationFactory, enoughTotalToStayAboveEDValidationFactory) {
|
||||
|
||||
override suspend fun transferFunctions(chainAsset: Chain.Asset): List<Pair<String, String>> {
|
||||
val type = chainAsset.requireStatemine()
|
||||
|
||||
return listOf(type.palletNameOrDefault() to "transfer")
|
||||
}
|
||||
|
||||
override fun ExtrinsicBuilder.transfer(transfer: AssetTransfer) {
|
||||
val chainAssetType = transfer.originChainAsset.type
|
||||
require(chainAssetType is Chain.Asset.Type.Statemine)
|
||||
|
||||
statemineTransfer(
|
||||
assetType = chainAssetType,
|
||||
target = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
amount = transfer.amountInPlanks
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun recipientCanAcceptTransfer(chainAsset: Chain.Asset, recipient: AccountId): Boolean {
|
||||
val statemineType = chainAsset.requireStatemine()
|
||||
|
||||
val assetAccount = remoteStorage.query(chainAsset.chainId) {
|
||||
runtime.metadata.statemineModule(statemineType).storage("Account").query(
|
||||
statemineType.prepareIdForEncoding(runtime),
|
||||
recipient,
|
||||
binding = ::bindAssetAccountOrEmpty
|
||||
)
|
||||
}
|
||||
|
||||
return assetAccount.canAcceptFunds
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
if (!checkIsOurCall(call, chain)) return null
|
||||
|
||||
val onChainAssetId = call.arguments["id"]
|
||||
val chainAsset = determineAsset(chain, onChainAssetId) ?: return null
|
||||
val amount = bindNumber(call.arguments["amount"])
|
||||
val destination = bindAccountIdentifier(call.arguments["target"]).intoKey()
|
||||
|
||||
return TransferParsedFromCall(
|
||||
amount = chainAsset.withAmount(amount),
|
||||
destination = destination
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun determineAsset(chain: Chain, onChainAssetId: Any?): Chain.Asset? {
|
||||
val runtime = chainRegistry.getRuntime(chain.id)
|
||||
return chain.findAssetByStatemineAssetId(runtime, onChainAssetId)
|
||||
}
|
||||
|
||||
private fun checkIsOurCall(call: GenericCall.Instance, chain: Chain): Boolean {
|
||||
if (call.function.name != "transfer") return false
|
||||
|
||||
val allStatemineAssetsOnChain = chain.findStatemineAssets()
|
||||
return allStatemineAssetsOnChain.any { it.requireStatemine().palletNameOrDefault() == call.module.name }
|
||||
}
|
||||
|
||||
private fun ExtrinsicBuilder.statemineTransfer(
|
||||
assetType: Chain.Asset.Type.Statemine,
|
||||
target: AccountId,
|
||||
amount: BigInteger
|
||||
) {
|
||||
call(
|
||||
moduleName = assetType.palletNameOrDefault(),
|
||||
callName = "transfer",
|
||||
arguments = mapOf(
|
||||
"id" to assetType.prepareIdForEncoding(runtime),
|
||||
"target" to PezkuwiAddressConstructor.constructInstance(runtime.typeRegistry, target),
|
||||
"amount" to amount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.utility
|
||||
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountIdentifier
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountInfo
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.TransferMode
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.nativeTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.BaseAssetTransfers
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageKey
|
||||
|
||||
class NativeAssetTransfers(
|
||||
chainRegistry: ChainRegistry,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
extrinsicServiceFactory: ExtrinsicService.Factory,
|
||||
phishingValidationFactory: PhishingValidationFactory,
|
||||
enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory,
|
||||
private val storageDataSource: StorageDataSource,
|
||||
private val accountRepository: AccountRepository,
|
||||
) : BaseAssetTransfers(chainRegistry, assetSourceRegistry, extrinsicServiceFactory, phishingValidationFactory, enoughTotalToStayAboveEDValidationFactory) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TRANSFER_ALL = "transfer_all"
|
||||
private const val TRANSFER = "transfer"
|
||||
private const val TRANSFER_KEEP_ALIVE = "transfer_keep_alive"
|
||||
private const val TRANSFER_ALLOW_DEATH = "transfer_allow_death"
|
||||
}
|
||||
|
||||
private val parsableCalls = listOf(TRANSFER, TRANSFER_KEEP_ALIVE, TRANSFER_ALLOW_DEATH)
|
||||
|
||||
override suspend fun totalCanDropBelowMinimumBalance(chainAsset: Chain.Asset): Boolean {
|
||||
val chain = chainRegistry.getChain(chainAsset.chainId)
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
|
||||
val accountInfo = storageDataSource.query(
|
||||
chainAsset.chainId,
|
||||
keyBuilder = { getAccountInfoStorageKey(metaAccount, chain, it) },
|
||||
binding = { it, runtime -> it?.let { bindAccountInfo(it, runtime) } }
|
||||
)
|
||||
|
||||
return accountInfo != null && accountInfo.consumers.isZero
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
val isOurs = parsableCalls.any { call.instanceOf(Modules.BALANCES, it) }
|
||||
if (!isOurs) return null
|
||||
|
||||
val asset = chain.utilityAsset
|
||||
val amount = bindNumber(call.arguments["value"])
|
||||
val recipient = bindAccountIdentifier(call.arguments["dest"]).intoKey()
|
||||
|
||||
return TransferParsedFromCall(asset.withAmount(amount), recipient)
|
||||
}
|
||||
|
||||
override fun ExtrinsicBuilder.transfer(transfer: AssetTransfer) {
|
||||
nativeTransfer(
|
||||
accountId = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
amount = transfer.originChainAsset.planksFromAmount(transfer.amount),
|
||||
mode = transfer.transferMode
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun transferFunctions(chainAsset: Chain.Asset) = listOf(
|
||||
Modules.BALANCES to TRANSFER,
|
||||
Modules.BALANCES to TRANSFER_ALLOW_DEATH,
|
||||
Modules.BALANCES to TRANSFER_ALL
|
||||
)
|
||||
|
||||
private fun getAccountInfoStorageKey(metaAccount: MetaAccount, chain: Chain, runtime: RuntimeSnapshot): String {
|
||||
val accountId = metaAccount.requireAccountIdIn(chain)
|
||||
return runtime.metadata.module(Modules.SYSTEM).storage("Account").storageKey(runtime, accountId)
|
||||
}
|
||||
|
||||
private val AssetTransfer.transferMode: TransferMode
|
||||
get() = if (transferringMaxAmount) {
|
||||
TransferMode.ALL
|
||||
} else {
|
||||
TransferMode.ALLOW_DEATH
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.validation.notSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDeposit
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure.WillRemoveAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.commissionChainAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeList
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeListInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.recipientOrNull
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.sendingAmountInCommissionAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.balanceCountedTowardsED
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.AmountProducer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForFeeChanges
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.doNotCrossExistentialDepositMultiFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.notPhishingAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.sufficientBalanceMultiFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.validAddress
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.validate
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.Type
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.positiveAmount() = positiveAmount(
|
||||
amount = { it.transfer.amount },
|
||||
error = { AssetTransferValidationFailure.NonPositiveAmount }
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.notPhishingRecipient(
|
||||
factory: PhishingValidationFactory
|
||||
) = notPhishingAccount(
|
||||
factory = factory,
|
||||
address = { it.transfer.recipient },
|
||||
chain = { it.transfer.destinationChain },
|
||||
warning = AssetTransferValidationFailure::PhishingRecipient
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.validAddress() = validAddress(
|
||||
address = { it.transfer.recipient },
|
||||
chain = { it.transfer.destinationChain },
|
||||
error = { AssetTransferValidationFailure.InvalidRecipientAddress(it.transfer.destinationChain) }
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.sufficientCommissionBalanceToStayAboveED(
|
||||
enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory
|
||||
) {
|
||||
enoughTotalToStayAboveEDValidationFactory.validate(
|
||||
fee = { it.originFee.submissionFee },
|
||||
balance = { it.originCommissionAsset.balanceCountedTowardsED() },
|
||||
chainWithAsset = { ChainWithAsset(it.transfer.originChain, it.commissionChainAsset) },
|
||||
error = { payload, error -> AssetTransferValidationFailure.NotEnoughFunds.ToStayAboveED(payload.commissionChainAsset, error) }
|
||||
)
|
||||
}
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.checkForFeeChanges(
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
coroutineScope: CoroutineScope
|
||||
) = checkForFeeChanges(
|
||||
calculateFee = { payload ->
|
||||
val transfers = assetSourceRegistry.sourceFor(payload.transfer.originChainAsset).transfers
|
||||
transfers.calculateFee(payload.transfer, coroutineScope)
|
||||
},
|
||||
currentFee = { it.originFee.submissionFee },
|
||||
chainAsset = { it.commissionChainAsset },
|
||||
error = AssetTransferValidationFailure::FeeChangeDetected
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDepositInUsedAsset(
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
extraAmount: AmountProducer<AssetTransferPayload>,
|
||||
) = doNotCrossExistentialDepositMultiFee(
|
||||
countableTowardsEdBalance = { it.originUsedAsset.balanceCountedTowardsED() },
|
||||
fee = { it.originFeeListInUsedAsset },
|
||||
extraAmount = extraAmount,
|
||||
existentialDeposit = { assetSourceRegistry.existentialDepositForUsedAsset(it.transfer) },
|
||||
error = { remainingAmount, payload -> payload.transfer.originChainAsset.existentialDepositError(remainingAmount) }
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.sufficientTransferableBalanceToPayOriginFee() = sufficientBalanceMultiFee(
|
||||
available = { it.originCommissionAsset.transferable },
|
||||
amount = { it.sendingAmountInCommissionAsset },
|
||||
feeExtractor = { it.originFeeList },
|
||||
error = { context ->
|
||||
AssetTransferValidationFailure.NotEnoughFunds.InCommissionAsset(
|
||||
chainAsset = context.payload.commissionChainAsset,
|
||||
fee = context.fee,
|
||||
maxUsable = context.maxUsable
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.sufficientBalanceInUsedAsset() = sufficientBalance(
|
||||
available = { it.originUsedAsset.transferable },
|
||||
amount = { it.transfer.amount },
|
||||
fee = { null },
|
||||
error = { AssetTransferValidationFailure.NotEnoughFunds.InUsedAsset }
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.recipientIsNotSystemAccount() = notSystemAccount(
|
||||
accountId = { it.transfer.recipientOrNull() },
|
||||
error = { AssetTransferValidationFailure.RecipientIsSystemAccount }
|
||||
)
|
||||
|
||||
private suspend fun AssetSourceRegistry.existentialDepositForUsedAsset(transfer: AssetTransfer): BigDecimal {
|
||||
return existentialDeposit(transfer.originChainAsset)
|
||||
}
|
||||
|
||||
private fun Chain.Asset.existentialDepositError(amount: BigDecimal): WillRemoveAccount = when (type) {
|
||||
is Type.Native -> WillRemoveAccount.WillBurnDust
|
||||
is Type.Orml -> WillRemoveAccount.WillBurnDust
|
||||
is Type.Statemine -> WillRemoveAccount.WillTransferDust(amount)
|
||||
is Type.EvmErc20, is Type.EvmNative -> WillRemoveAccount.WillBurnDust
|
||||
is Type.Equilibrium -> WillRemoveAccount.WillBurnDust
|
||||
Type.Unsupported -> throw IllegalArgumentException("Unsupported")
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.valid
|
||||
import io.novafoundation.nova.common.validation.validOrError
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidation
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.receivingAmountInCommissionAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PlanksProducer
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.ext.commissionAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigInteger
|
||||
|
||||
class DeadRecipientValidation(
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val addingAmount: PlanksProducer<AssetTransferPayload>,
|
||||
private val assetToCheck: (AssetTransferPayload) -> Chain.Asset,
|
||||
private val skipIf: suspend (AssetTransferPayload) -> Boolean,
|
||||
private val failure: (AssetTransferPayload) -> AssetTransferValidationFailure.DeadRecipient,
|
||||
) : AssetTransfersValidation {
|
||||
|
||||
override suspend fun validate(value: AssetTransferPayload): ValidationStatus<AssetTransferValidationFailure> {
|
||||
if (skipIf(value)) {
|
||||
return valid()
|
||||
}
|
||||
|
||||
val chain = value.transfer.destinationChain
|
||||
val chainAsset = assetToCheck(value)
|
||||
|
||||
val balanceSource = assetSourceRegistry.sourceFor(chainAsset).balance
|
||||
|
||||
val existentialDeposit = balanceSource.existentialDeposit(chainAsset)
|
||||
val recipientAccountId = value.transfer.destinationChain.accountIdOf(value.transfer.recipient)
|
||||
|
||||
val recipientBalance = balanceSource.queryAccountBalance(chain, chainAsset, recipientAccountId).countedTowardsEd
|
||||
|
||||
return validOrError(recipientBalance + addingAmount(value) >= existentialDeposit) {
|
||||
failure(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.notDeadRecipientInCommissionAsset(
|
||||
assetSourceRegistry: AssetSourceRegistry
|
||||
) = notDeadRecipient(
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
assetToCheck = { it.transfer.destinationChain.commissionAsset },
|
||||
addingAmount = { it.receivingAmountInCommissionAsset },
|
||||
skipIf = { assetSourceRegistry.isAssetSelfSufficient(it.transfer.destinationChainAsset) },
|
||||
failure = { AssetTransferValidationFailure.DeadRecipient.InCommissionAsset(commissionAsset = it.transfer.destinationChain.commissionAsset) }
|
||||
)
|
||||
|
||||
private suspend fun AssetSourceRegistry.isAssetSelfSufficient(asset: Chain.Asset) = sourceFor(asset).balance.isSelfSufficient(asset)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.notDeadRecipientInUsedAsset(
|
||||
assetSourceRegistry: AssetSourceRegistry
|
||||
) = notDeadRecipient(
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
assetToCheck = { it.transfer.destinationChainAsset },
|
||||
addingAmount = { it.transfer.amountInPlanks },
|
||||
failure = { AssetTransferValidationFailure.DeadRecipient.InUsedAsset }
|
||||
)
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.notDeadRecipient(
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
failure: (AssetTransferPayload) -> AssetTransferValidationFailure.DeadRecipient,
|
||||
assetToCheck: (AssetTransferPayload) -> Chain.Asset,
|
||||
addingAmount: PlanksProducer<AssetTransferPayload> = { BigInteger.ZERO },
|
||||
skipIf: suspend (AssetTransferPayload) -> Boolean = { false }
|
||||
) = validate(
|
||||
DeadRecipientValidation(
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
addingAmount = addingAmount,
|
||||
assetToCheck = assetToCheck,
|
||||
failure = failure,
|
||||
skipIf = skipIf
|
||||
)
|
||||
)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.calls
|
||||
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.composeCall
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.OriginCaller
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
fun RuntimeSnapshot.composeDispatchAs(
|
||||
call: GenericCall.Instance,
|
||||
origin: OriginCaller
|
||||
): GenericCall.Instance {
|
||||
return composeCall(
|
||||
moduleName = Modules.UTILITY,
|
||||
callName = "dispatch_as",
|
||||
arguments = mapOf(
|
||||
"as_origin" to origin.toEncodableInstance(),
|
||||
"call" to call
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun RuntimeSnapshot.composeBatchAll(
|
||||
calls: List<GenericCall.Instance>,
|
||||
): GenericCall.Instance {
|
||||
return composeCall(
|
||||
moduleName = Modules.UTILITY,
|
||||
callName = "batch_all",
|
||||
arguments = mapOf(
|
||||
"calls" to calls
|
||||
)
|
||||
)
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.updaters.balance
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.mergeIfMultiple
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core.updater.Updater
|
||||
import io.novafoundation.nova.core_db.dao.OperationDao
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationBaseLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.OperationLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
|
||||
import io.novafoundation.nova.feature_wallet_api.data.mappers.mapAssetWithAmountToLocal
|
||||
import io.novafoundation.nova.feature_wallet_api.data.mappers.mapOperationStatusToOperationLocalStatus
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.AssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.enabledAssets
|
||||
import io.novafoundation.nova.runtime.ext.localId
|
||||
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.catch
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
internal class FullSyncPaymentUpdater(
|
||||
private val operationDao: OperationDao,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
override val scope: AccountUpdateScope,
|
||||
private val chain: Chain,
|
||||
) : Updater<MetaAccount> {
|
||||
|
||||
override val requiredModules: List<String> = emptyList()
|
||||
|
||||
override suspend fun listenForUpdates(
|
||||
storageSubscriptionBuilder: SharedRequestsBuilder,
|
||||
scopeValue: MetaAccount,
|
||||
): Flow<Updater.SideEffect> {
|
||||
val accountId = scopeValue.requireAccountIdIn(chain)
|
||||
|
||||
return chain.enabledAssets().mapNotNull { chainAsset ->
|
||||
syncAsset(chainAsset, scopeValue, accountId, storageSubscriptionBuilder)
|
||||
}
|
||||
.mergeIfMultiple()
|
||||
.noSideAffects()
|
||||
}
|
||||
|
||||
private suspend fun syncAsset(
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
storageSubscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate>? {
|
||||
val assetSource = assetSourceRegistry.sourceFor(chainAsset)
|
||||
|
||||
val assetUpdateFlow = runCatching {
|
||||
assetSource.balance.startSyncingBalance(chain, chainAsset, metaAccount, accountId, storageSubscriptionBuilder)
|
||||
}
|
||||
.onFailure { logSyncError(chain, chainAsset, error = it) }
|
||||
.getOrNull()
|
||||
?: return null
|
||||
|
||||
return assetUpdateFlow.onEach { balanceUpdate ->
|
||||
assetSource.history.syncOperationsForBalanceChange(chainAsset, balanceUpdate, accountId)
|
||||
}
|
||||
.catch { logSyncError(chain, chainAsset, error = it) }
|
||||
}
|
||||
|
||||
private fun logSyncError(chain: Chain, chainAsset: Chain.Asset, error: Throwable) {
|
||||
Log.e(LOG_TAG, "Failed to sync balance for ${chainAsset.symbol} in ${chain.name}", error)
|
||||
}
|
||||
|
||||
private suspend fun AssetHistory.syncOperationsForBalanceChange(
|
||||
chainAsset: Chain.Asset,
|
||||
balanceSyncUpdate: BalanceSyncUpdate,
|
||||
accountId: AccountId,
|
||||
) {
|
||||
when (balanceSyncUpdate) {
|
||||
is BalanceSyncUpdate.CauseFetchable -> runCatching { fetchOperationsForBalanceChange(chain, chainAsset, balanceSyncUpdate.blockHash, accountId) }
|
||||
.onSuccess { blockOperations ->
|
||||
val localOperations = blockOperations
|
||||
.filter { it.type.relates(accountId) }
|
||||
.map { operation -> createOperationLocal(chainAsset, operation, accountId) }
|
||||
|
||||
operationDao.insertAll(localOperations)
|
||||
}.onFailure {
|
||||
Log.e(LOG_TAG, "Failed to retrieve transactions from block (${chain.name}.${chainAsset.symbol})", it)
|
||||
}
|
||||
|
||||
is BalanceSyncUpdate.CauseFetched -> {
|
||||
val local = createOperationLocal(chainAsset, balanceSyncUpdate.cause, accountId)
|
||||
operationDao.insert(local)
|
||||
}
|
||||
|
||||
BalanceSyncUpdate.NoCause -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createOperationLocal(
|
||||
chainAsset: Chain.Asset,
|
||||
historyUpdate: RealtimeHistoryUpdate,
|
||||
accountId: ByteArray,
|
||||
): OperationLocal {
|
||||
return when (val type = historyUpdate.type) {
|
||||
is RealtimeHistoryUpdate.Type.Swap -> createSwapOperation(chainAsset, historyUpdate, type, accountId)
|
||||
is RealtimeHistoryUpdate.Type.Transfer -> createTransferOperation(chainAsset, historyUpdate, type, accountId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSwapOperation(
|
||||
chainAsset: Chain.Asset,
|
||||
historyUpdate: RealtimeHistoryUpdate,
|
||||
swap: RealtimeHistoryUpdate.Type.Swap,
|
||||
accountId: ByteArray,
|
||||
): OperationLocal {
|
||||
return OperationLocal.manualSwap(
|
||||
hash = historyUpdate.txHash,
|
||||
originAddress = chain.addressOf(accountId),
|
||||
assetId = chainAsset.localId,
|
||||
fee = mapAssetWithAmountToLocal(swap.amountFee),
|
||||
amountIn = mapAssetWithAmountToLocal(swap.amountIn),
|
||||
amountOut = mapAssetWithAmountToLocal(swap.amountOut),
|
||||
status = mapOperationStatusToOperationLocalStatus(historyUpdate.status),
|
||||
source = OperationBaseLocal.Source.BLOCKCHAIN
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createTransferOperation(
|
||||
chainAsset: Chain.Asset,
|
||||
historyUpdate: RealtimeHistoryUpdate,
|
||||
transfer: RealtimeHistoryUpdate.Type.Transfer,
|
||||
accountId: ByteArray,
|
||||
): OperationLocal {
|
||||
val localStatus = mapOperationStatusToOperationLocalStatus(historyUpdate.status)
|
||||
val address = chain.addressOf(accountId)
|
||||
|
||||
val localCopy = operationDao.getTransferType(historyUpdate.txHash, address, chain.id, chainAsset.id)
|
||||
|
||||
return OperationLocal.manualTransfer(
|
||||
hash = historyUpdate.txHash,
|
||||
chainId = chain.id,
|
||||
address = address,
|
||||
chainAssetId = chainAsset.id,
|
||||
amount = transfer.amountInPlanks,
|
||||
senderAddress = chain.addressOf(transfer.senderId),
|
||||
receiverAddress = chain.addressOf(transfer.recipientId),
|
||||
fee = localCopy?.fee,
|
||||
status = localStatus,
|
||||
source = OperationBaseLocal.Source.BLOCKCHAIN,
|
||||
)
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.updaters.balance
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core.updater.Updater
|
||||
import io.novafoundation.nova.core_db.model.AssetLocal
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.hash.Hasher.blake2b128Concat
|
||||
import io.novasama.substrate_sdk_android.hash.Hasher.xxHash128
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* Runtime-independent updater that watches on-chain account presence and switches to full sync mode if account is present
|
||||
*/
|
||||
class LightSyncPaymentUpdater(
|
||||
override val scope: AccountUpdateScope,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetCache: AssetCache,
|
||||
private val chain: Chain
|
||||
) : Updater<MetaAccount> {
|
||||
|
||||
override val requiredModules: List<String> = emptyList()
|
||||
|
||||
override suspend fun listenForUpdates(storageSubscriptionBuilder: SharedRequestsBuilder, scopeValue: MetaAccount): Flow<Updater.SideEffect> {
|
||||
val accountId = scopeValue.accountIdIn(chain) ?: return emptyFlow()
|
||||
val storageKey = systemAccountStorageKey(accountId)
|
||||
|
||||
return storageSubscriptionBuilder.subscribe(storageKey).onEach { storageChange ->
|
||||
if (storageChange.value != null) {
|
||||
switchToFullSync()
|
||||
|
||||
Log.d("ConnectionState", "Detected balance during light sync for ${chain.name}, switching to full sync mode")
|
||||
} else {
|
||||
insertEmptyBalances(scopeValue)
|
||||
}
|
||||
}.noSideAffects()
|
||||
}
|
||||
|
||||
private suspend fun insertEmptyBalances(metaAccount: MetaAccount) {
|
||||
assetCache.updateAssetsByChain(metaAccount, chain) { chainAsset ->
|
||||
AssetLocal.createEmpty(chainAsset.id, chainAsset.chainId, metaAccount.id)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun switchToFullSync() {
|
||||
chainRegistry.enableFullSync(chain.id)
|
||||
}
|
||||
|
||||
private fun systemAccountStorageKey(accountId: AccountId): String {
|
||||
val keyBytes = "System".xxHash128() + "Account".xxHash128() + accountId.blake2b128Concat()
|
||||
|
||||
return keyBytes.toHexString(withPrefix = true)
|
||||
}
|
||||
|
||||
private fun String.xxHash128() = toByteArray().xxHash128()
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.updaters.balance
|
||||
|
||||
import io.novafoundation.nova.core.updater.Updater
|
||||
import io.novafoundation.nova.core_db.dao.OperationDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
|
||||
import io.novafoundation.nova.feature_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.PaymentUpdaterFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class RealPaymentUpdaterFactory(
|
||||
private val operationDao: OperationDao,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val scope: AccountUpdateScope,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetCache: AssetCache
|
||||
) : PaymentUpdaterFactory {
|
||||
|
||||
override fun createFullSync(chain: Chain): Updater<MetaAccount> {
|
||||
return FullSyncPaymentUpdater(
|
||||
operationDao = operationDao,
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
scope = scope,
|
||||
chain = chain,
|
||||
)
|
||||
}
|
||||
|
||||
override fun createLightSync(chain: Chain): Updater<MetaAccount> {
|
||||
return LightSyncPaymentUpdater(
|
||||
scope = scope,
|
||||
chainRegistry = chainRegistry,
|
||||
chain = chain,
|
||||
assetCache = assetCache
|
||||
)
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.updaters.locks
|
||||
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.core.updater.Updater
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.BalanceLocksUpdaterFactory
|
||||
import io.novafoundation.nova.runtime.ext.enabledAssets
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.merge
|
||||
|
||||
class BalanceLocksUpdaterFactoryImpl(
|
||||
private val scope: AccountUpdateScope,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : BalanceLocksUpdaterFactory {
|
||||
|
||||
override fun create(chain: Chain): Updater<MetaAccount> {
|
||||
return BalanceLocksUpdater(
|
||||
scope,
|
||||
assetSourceRegistry,
|
||||
chain
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class BalanceLocksUpdater(
|
||||
override val scope: AccountUpdateScope,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val chain: Chain
|
||||
) : Updater<MetaAccount> {
|
||||
|
||||
override val requiredModules: List<String> = emptyList()
|
||||
|
||||
override suspend fun listenForUpdates(
|
||||
storageSubscriptionBuilder: SharedRequestsBuilder,
|
||||
scopeValue: MetaAccount,
|
||||
): Flow<Updater.SideEffect> {
|
||||
val metaAccount = scopeValue
|
||||
val accountId = metaAccount.accountIdIn(chain) ?: return emptyFlow()
|
||||
|
||||
val flows = buildList {
|
||||
chain.enabledAssets().forEach { chainAsset ->
|
||||
val assetSource = assetSourceRegistry.sourceFor(chainAsset)
|
||||
|
||||
val locksFlow = assetSource.balance.startSyncingBalanceLocks(metaAccount, chain, chainAsset, accountId, storageSubscriptionBuilder)
|
||||
val holdsFlow = assetSource.balance.startSyncingBalanceHolds(metaAccount, chain, chainAsset, accountId, storageSubscriptionBuilder)
|
||||
|
||||
add(locksFlow)
|
||||
add(holdsFlow)
|
||||
}
|
||||
}
|
||||
|
||||
return flows
|
||||
.merge()
|
||||
.noSideAffects()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain
|
||||
|
||||
typealias JunctionsRemote = Map<String, Any?>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_impl.BuildConfig
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface CrossChainConfigApi {
|
||||
|
||||
@GET(BuildConfig.LEGACY_CROSS_CHAIN_CONFIG_URL)
|
||||
suspend fun getLegacyCrossChainConfig(): String
|
||||
|
||||
@GET(BuildConfig.DYNAMIC_CROSS_CHAIN_CONFIG_URL)
|
||||
suspend fun getDynamicCrossChainConfig(): String
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Weight
|
||||
import io.novafoundation.nova.common.utils.argument
|
||||
import io.novafoundation.nova.common.utils.requireActualType
|
||||
import io.novafoundation.nova.common.utils.structOf
|
||||
import io.novafoundation.nova.common.utils.xcmPalletName
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.primitives.NumberType
|
||||
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 io.novasama.substrate_sdk_android.runtime.metadata.module
|
||||
|
||||
fun ExtrinsicBuilder.xcmExecute(
|
||||
message: VersionedXcmMessage,
|
||||
maxWeight: Weight,
|
||||
): ExtrinsicBuilder {
|
||||
return call(
|
||||
moduleName = runtime.metadata.xcmPalletName(),
|
||||
callName = "execute",
|
||||
arguments = mapOf(
|
||||
"message" to message.toEncodableInstance(),
|
||||
"max_weight" to runtime.prepareWeightForEncoding(maxWeight)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun RuntimeSnapshot.prepareWeightForEncoding(weight: Weight): Any {
|
||||
val moduleName = metadata.xcmPalletName()
|
||||
|
||||
val weightArgumentType = metadata.module(moduleName)
|
||||
.call("execute")
|
||||
.argument("max_weight")
|
||||
.requireActualType()
|
||||
|
||||
return when {
|
||||
weightArgumentType.isWeightV1() -> weight
|
||||
else -> weight.encodeWeightV2()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Weight.encodeWeightV2(): Struct.Instance {
|
||||
return structOf("refTime" to this, "proofSize" to Balance.ZERO)
|
||||
}
|
||||
|
||||
private fun Type<*>.isWeightV1(): Boolean {
|
||||
return this is NumberType
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.flatMap
|
||||
import io.novafoundation.nova.common.utils.instanceOf
|
||||
import io.novafoundation.nova.common.utils.transformResult
|
||||
import io.novafoundation.nova.common.utils.wrapInResult
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
|
||||
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.extrinsic.execution.ExtrinsicExecutionResult
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
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.events.tryDetectDeposit
|
||||
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.data.network.crosschain.CrossChainTransactor
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.destinationChainId
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferFeatures
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.remoteReserveLocation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainId
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.DynamicCrossChainTransactor
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyCrossChainTransactor
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.findRelayChainOrThrow
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.BlockEvents
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.hasEvent
|
||||
import io.novafoundation.nova.runtime.repository.ChainStateRepository
|
||||
import io.novafoundation.nova.runtime.repository.expectedBlockTime
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.ZERO
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class RealCrossChainTransactor(
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val eventsRepository: EventsRepository,
|
||||
private val chainStateRepository: ChainStateRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val dynamic: DynamicCrossChainTransactor,
|
||||
private val legacy: LegacyCrossChainTransactor,
|
||||
) : CrossChainTransactor {
|
||||
|
||||
context(ExtrinsicService)
|
||||
override suspend fun estimateOriginFee(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase
|
||||
): Fee {
|
||||
return estimateFee(
|
||||
chain = transfer.originChain,
|
||||
origin = TransactionOrigin.SelectedWallet,
|
||||
submissionOptions = ExtrinsicService.SubmissionOptions(
|
||||
feePaymentCurrency = transfer.feePaymentCurrency
|
||||
)
|
||||
) {
|
||||
crossChainTransfer(configuration, transfer, crossChainFee = Balance.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
context(ExtrinsicService)
|
||||
override suspend fun performTransfer(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
): Result<ExtrinsicSubmission> {
|
||||
return submitExtrinsic(
|
||||
chain = transfer.originChain,
|
||||
origin = TransactionOrigin.SelectedWallet,
|
||||
submissionOptions = ExtrinsicService.SubmissionOptions(
|
||||
feePaymentCurrency = transfer.feePaymentCurrency
|
||||
)
|
||||
) {
|
||||
crossChainTransfer(configuration, transfer, crossChainFee)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun requiredRemainingAmountAfterTransfer(configuration: CrossChainTransferConfiguration): Balance {
|
||||
return when (configuration) {
|
||||
is CrossChainTransferConfiguration.Dynamic -> dynamic.requiredRemainingAmountAfterTransfer(configuration.config)
|
||||
is CrossChainTransferConfiguration.Legacy -> legacy.requiredRemainingAmountAfterTransfer(configuration.config)
|
||||
}
|
||||
}
|
||||
|
||||
context(ExtrinsicService)
|
||||
override suspend fun performAndTrackTransfer(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
): Result<Balance> {
|
||||
// Start balances updates eagerly to not to miss events in case tx has been included to block right after submission
|
||||
val balancesUpdates = observeTransferableBalance(transfer)
|
||||
.wrapInResult()
|
||||
.shareIn(CoroutineScope(coroutineContext), SharingStarted.Eagerly, replay = 100)
|
||||
|
||||
Log.d("CrossChain", "Starting cross-chain transfer")
|
||||
|
||||
return performTransferOfExactAmount(configuration, transfer)
|
||||
.requireOk()
|
||||
.flatMap {
|
||||
Log.d("CrossChain", "Cross chain transfer for successfully executed on origin, waiting for destination")
|
||||
|
||||
balancesUpdates.awaitCrossChainArrival(transfer)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun supportsXcmExecute(
|
||||
originChainId: ChainId,
|
||||
features: DynamicCrossChainTransferFeatures
|
||||
): Boolean {
|
||||
return dynamic.supportsXcmExecute(originChainId, features)
|
||||
}
|
||||
|
||||
override suspend fun estimateMaximumExecutionTime(configuration: CrossChainTransferConfiguration): Duration {
|
||||
val originChainId = configuration.originChainId
|
||||
val remoteReserveChainId = configuration.transferType.remoteReserveLocation()?.chainId
|
||||
val destinationChainId = configuration.destinationChainId
|
||||
|
||||
val relayId = chainRegistry.findRelayChainOrThrow(originChainId)
|
||||
|
||||
var totalDuration = ZERO
|
||||
|
||||
if (remoteReserveChainId != null) {
|
||||
totalDuration += maxTimeToTransmitMessage(originChainId, remoteReserveChainId, relayId)
|
||||
totalDuration += maxTimeToTransmitMessage(remoteReserveChainId, destinationChainId, relayId)
|
||||
} else {
|
||||
totalDuration += maxTimeToTransmitMessage(originChainId, destinationChainId, relayId)
|
||||
}
|
||||
|
||||
return totalDuration
|
||||
}
|
||||
|
||||
private suspend fun maxTimeToTransmitMessage(from: ChainId, to: ChainId, relay: ChainId): Duration {
|
||||
val toProduceBlockOnOrigin = chainStateRepository.expectedBlockTime(from)
|
||||
val toProduceBlockOnDestination = chainStateRepository.expectedBlockTime(to)
|
||||
val toProduceBlockOnRelay = if (from != relay && to != relay) chainStateRepository.expectedBlockTime(relay) else ZERO
|
||||
|
||||
return toProduceBlockOnOrigin + toProduceBlockOnRelay + toProduceBlockOnDestination
|
||||
}
|
||||
|
||||
private suspend fun Flow<Result<TransferableBalanceUpdatePoint>>.awaitCrossChainArrival(transfer: AssetTransferBase): Result<Balance> {
|
||||
return runCatching {
|
||||
withTimeout(60.seconds) {
|
||||
transformResult { balanceUpdate ->
|
||||
Log.d("CrossChain", "Destination balance update detected: $balanceUpdate")
|
||||
|
||||
val updatedAt = balanceUpdate.updatedAt
|
||||
|
||||
val blockEvents = eventsRepository.getBlockEvents(transfer.destinationChain.id, updatedAt)
|
||||
|
||||
val xcmArrivedDeposit = searchForXcmArrival(blockEvents.initialization, transfer)
|
||||
?: searchForXcmArrival(blockEvents.finalization, transfer)
|
||||
?: searchForXcmArrival(blockEvents.findSetValidationDataEvents(), transfer)
|
||||
|
||||
if (xcmArrivedDeposit != null) {
|
||||
Log.d("CrossChain", "Found destination xcm arrival event, amount is $xcmArrivedDeposit")
|
||||
|
||||
emit(xcmArrivedDeposit)
|
||||
} else {
|
||||
Log.d("CrossChain", "No destination xcm arrival event found for the received balance update")
|
||||
}
|
||||
}
|
||||
.first()
|
||||
.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BlockEvents.findSetValidationDataEvents(): List<GenericEvent.Instance> {
|
||||
val setValidationDataExtrinsic = applyExtrinsic.find { it.extrinsic.call.instanceOf(Modules.PARACHAIN_SYSTEM, "set_validation_data") }
|
||||
|
||||
return setValidationDataExtrinsic?.events.orEmpty()
|
||||
}
|
||||
|
||||
private suspend fun searchForXcmArrival(
|
||||
events: List<GenericEvent.Instance>,
|
||||
transfer: AssetTransferBase
|
||||
): Balance? {
|
||||
if (!events.hasXcmArrivalEvent()) return null
|
||||
|
||||
val eventDetector = assetSourceRegistry.getEventDetector(transfer.destinationChainAsset)
|
||||
|
||||
val depositEvent = events.mapNotNull { event -> eventDetector.tryDetectDeposit(event) }
|
||||
.find { it.destination == transfer.recipientAccountId }
|
||||
|
||||
return depositEvent?.amount
|
||||
}
|
||||
|
||||
private fun List<GenericEvent.Instance>.hasXcmArrivalEvent(): Boolean {
|
||||
return hasEvent("MessageQueue", "Processed") or hasEvent("XcmpQueue", "Success")
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicService.performTransferOfExactAmount(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
): Result<ExtrinsicExecutionResult> {
|
||||
return submitExtrinsicAndAwaitExecution(
|
||||
chain = transfer.originChain,
|
||||
origin = TransactionOrigin.SelectedWallet,
|
||||
submissionOptions = ExtrinsicService.SubmissionOptions(
|
||||
feePaymentCurrency = transfer.feePaymentCurrency
|
||||
)
|
||||
) {
|
||||
// We are transferring the exact amount, so we should add nothing on top of the transfer amount
|
||||
crossChainTransfer(configuration, transfer, crossChainFee = Balance.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun observeTransferableBalance(transfer: AssetTransferBase): Flow<TransferableBalanceUpdatePoint> {
|
||||
val destinationAssetBalances = assetSourceRegistry.sourceFor(transfer.destinationChainAsset)
|
||||
|
||||
return destinationAssetBalances.balance.subscribeAccountBalanceUpdatePoint(
|
||||
chain = transfer.destinationChain,
|
||||
chainAsset = transfer.destinationChainAsset,
|
||||
accountId = transfer.recipientAccountId.value,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicBuilder.crossChainTransfer(
|
||||
configuration: CrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
when (configuration) {
|
||||
is CrossChainTransferConfiguration.Dynamic -> dynamic.crossChainTransfer(configuration.config, transfer, crossChainFee)
|
||||
is CrossChainTransferConfiguration.Legacy -> legacy.crossChainTransfer(configuration.config, transfer, crossChainFee)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFeeModel
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainWeigher
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.CrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.DynamicCrossChainWeigher
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy.LegacyCrossChainWeigher
|
||||
|
||||
class RealCrossChainWeigher(
|
||||
private val dynamic: DynamicCrossChainWeigher,
|
||||
private val legacy: LegacyCrossChainWeigher
|
||||
) : CrossChainWeigher {
|
||||
|
||||
override suspend fun estimateFee(transfer: AssetTransferBase, config: CrossChainTransferConfiguration): CrossChainFeeModel {
|
||||
return when (config) {
|
||||
is CrossChainTransferConfiguration.Dynamic -> dynamic.estimateFee(config.config, transfer)
|
||||
is CrossChainTransferConfiguration.Legacy -> legacy.estimateFee(transfer.amountPlanks, config.config)
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.common
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.composeCall
|
||||
import io.novafoundation.nova.common.utils.metadata
|
||||
import io.novafoundation.nova.common.utils.xcmPalletName
|
||||
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.CrossChainTransferConfigurationBase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.assetLocationOnOrigin
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.destinationChainLocationOnOrigin
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.XcmTransferType
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainId
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAsset
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetFilter
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetId
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssets
|
||||
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
|
||||
import io.novafoundation.nova.feature_xcm_api.builder.buildXcmWithoutFeesMeasurement
|
||||
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.orDefault
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.versionedXcm
|
||||
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.withRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class TransferAssetUsingTypeTransactor @Inject constructor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val xcmBuilderFactory: XcmBuilder.Factory,
|
||||
private val xcmVersionDetector: XcmVersionDetector,
|
||||
) {
|
||||
|
||||
suspend fun composeCall(
|
||||
configuration: CrossChainTransferConfigurationBase,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance,
|
||||
forceXcmVersion: XcmVersion? = null
|
||||
): GenericCall.Instance {
|
||||
val totalTransferAmount = transfer.amountPlanks + crossChainFee
|
||||
val multiAsset = MultiAsset.from(configuration.assetLocationOnOrigin(), totalTransferAmount)
|
||||
val multiAssetId = MultiAssetId(configuration.assetLocationOnOrigin())
|
||||
|
||||
val multiLocationVersion = forceXcmVersion ?: xcmVersionDetector.lowestPresentMultiLocationVersion(transfer.originChain.id).orDefault()
|
||||
val multiAssetsVersion = forceXcmVersion ?: xcmVersionDetector.lowestPresentMultiAssetsVersion(transfer.originChain.id).orDefault()
|
||||
val multiAssetIdVersion = forceXcmVersion ?: xcmVersionDetector.lowestPresentMultiAssetIdVersion(transfer.originChain.id).orDefault()
|
||||
|
||||
val transferTypeParam = configuration.transferTypeParam(multiAssetsVersion)
|
||||
|
||||
// Debug logging for XCM transfer
|
||||
val destLocation = configuration.destinationChainLocationOnOrigin()
|
||||
Log.d("XCM_TRANSFER", "=== XCM TRANSFER DEBUG ===")
|
||||
Log.d("XCM_TRANSFER", "Origin chain: ${configuration.originChain.chain.name} (${configuration.originChain.chain.id})")
|
||||
Log.d("XCM_TRANSFER", "Origin parachainId: ${configuration.originChain.parachainId}")
|
||||
Log.d("XCM_TRANSFER", "Destination chain: ${configuration.destinationChain.chain.name} (${configuration.destinationChain.chain.id})")
|
||||
Log.d("XCM_TRANSFER", "Destination parachainId: ${configuration.destinationChain.parachainId}")
|
||||
Log.d("XCM_TRANSFER", "Destination location (relative): parents=${destLocation.parents}, interior=${destLocation.interior}")
|
||||
Log.d("XCM_TRANSFER", "Destination junctions: ${destLocation.interior}")
|
||||
Log.d("XCM_TRANSFER", "Transfer type: ${configuration.transferType}")
|
||||
Log.d("XCM_TRANSFER", "XCM Version: $multiLocationVersion")
|
||||
Log.d("XCM_TRANSFER", "==========================")
|
||||
|
||||
return chainRegistry.withRuntime(configuration.originChainId) {
|
||||
composeCall(
|
||||
moduleName = metadata.xcmPalletName(),
|
||||
callName = "transfer_assets_using_type_and_then",
|
||||
arguments = mapOf(
|
||||
"dest" to configuration.destinationChainLocationOnOrigin().versionedXcm(multiLocationVersion).toEncodableInstance(),
|
||||
"assets" to MultiAssets(multiAsset).versionedXcm(multiAssetsVersion).toEncodableInstance(),
|
||||
"assets_transfer_type" to transferTypeParam,
|
||||
"remote_fees_id" to multiAssetId.versionedXcm(multiAssetIdVersion).toEncodableInstance(),
|
||||
"fees_transfer_type" to transferTypeParam,
|
||||
"custom_xcm_on_dest" to constructCustomXcmOnDest(configuration, transfer, multiLocationVersion).toEncodableInstance(),
|
||||
"weight_limit" to WeightLimit.Unlimited.toEncodableInstance()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CrossChainTransferConfigurationBase.transferTypeParam(locationXcmVersion: XcmVersion): Any {
|
||||
return when (val type = transferType) {
|
||||
is XcmTransferType.Teleport -> DictEnum.Entry("Teleport", null)
|
||||
|
||||
is XcmTransferType.Reserve.Destination -> DictEnum.Entry("DestinationReserve", null)
|
||||
|
||||
is XcmTransferType.Reserve.Origin -> DictEnum.Entry("LocalReserve", null)
|
||||
|
||||
is XcmTransferType.Reserve.Remote -> {
|
||||
val reserveChainRelative = type.remoteReserveLocation.location.fromPointOfViewOf(originChainLocation.location)
|
||||
val remoteReserveEncodable = reserveChainRelative.versionedXcm(locationXcmVersion).toEncodableInstance()
|
||||
|
||||
DictEnum.Entry("RemoteReserve", remoteReserveEncodable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun constructCustomXcmOnDest(
|
||||
configuration: CrossChainTransferConfigurationBase,
|
||||
transfer: AssetTransferBase,
|
||||
minDetectedXcmVersion: XcmVersion
|
||||
): VersionedXcmMessage {
|
||||
return xcmBuilderFactory.buildXcmWithoutFeesMeasurement(
|
||||
initial = configuration.originChainLocation,
|
||||
// singleCounted is only available from V3
|
||||
xcmVersion = minDetectedXcmVersion.coerceAtLeast(XcmVersion.V3)
|
||||
) {
|
||||
depositAsset(MultiAssetFilter.singleCounted(), transfer.recipientAccountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.JunctionsRemote
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
class DynamicCrossChainTransfersConfigRemote(
|
||||
val assetsLocation: Map<String, DynamicReserveLocationRemote>?,
|
||||
// (ChainId, AssetId) -> ReserveId
|
||||
val reserveIdOverrides: Map<String, Map<Int, String>>,
|
||||
val chains: List<DynamicCrossChainOriginChainRemote>?,
|
||||
val customTeleports: List<CustomTeleportEntryRemote>?,
|
||||
)
|
||||
|
||||
class CustomTeleportEntryRemote(
|
||||
val originChain: String,
|
||||
val destChain: String,
|
||||
val originAsset: Int
|
||||
)
|
||||
|
||||
class DynamicReserveLocationRemote(
|
||||
val chainId: ChainId,
|
||||
val multiLocation: JunctionsRemote
|
||||
)
|
||||
|
||||
class DynamicCrossChainOriginChainRemote(
|
||||
val chainId: ChainId,
|
||||
val assets: List<DynamicCrossChainOriginAssetRemote>
|
||||
)
|
||||
|
||||
class DynamicCrossChainOriginAssetRemote(
|
||||
val assetId: Int,
|
||||
val xcmTransfers: List<DynamicXcmTransferRemote>,
|
||||
)
|
||||
|
||||
class DynamicXcmTransferRemote(
|
||||
// New format: nested destination object
|
||||
val destination: XcmTransferDestinationRemote?,
|
||||
// Legacy format: chainId and assetId at root level
|
||||
val chainId: ChainId?,
|
||||
val assetId: Int?,
|
||||
val type: String?,
|
||||
val hasDeliveryFee: Boolean?,
|
||||
val supportsXcmExecute: Boolean?,
|
||||
) {
|
||||
/**
|
||||
* Get the destination chainId, supporting both new and legacy formats.
|
||||
*/
|
||||
fun getDestinationChainId(): ChainId {
|
||||
return destination?.chainId ?: chainId
|
||||
?: throw IllegalStateException("XCM transfer has no destination chainId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the destination assetId, supporting both new and legacy formats.
|
||||
*/
|
||||
fun getDestinationAssetId(): Int {
|
||||
return destination?.assetId ?: assetId
|
||||
?: throw IllegalStateException("XCM transfer has no destination assetId")
|
||||
}
|
||||
}
|
||||
|
||||
class XcmTransferDestinationRemote(
|
||||
val chainId: ChainId,
|
||||
val assetId: Int,
|
||||
)
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.WeightV2
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
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.destinationChainLocation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferFeatures
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.XcmTransferType
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainId
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainLocation
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.common.TransferAssetUsingTypeTransactor
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetFilter
|
||||
import io.novafoundation.nova.feature_xcm_api.builder.XcmBuilder
|
||||
import io.novafoundation.nova.feature_xcm_api.builder.buyExecution
|
||||
import io.novafoundation.nova.feature_xcm_api.builder.createWithoutFeesMeasurement
|
||||
import io.novafoundation.nova.feature_xcm_api.builder.withdrawAsset
|
||||
import io.novafoundation.nova.feature_xcm_api.extrinsic.composeXcmExecute
|
||||
import io.novafoundation.nova.feature_xcm_api.message.VersionedXcmMessage
|
||||
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.runtimeApi.getInnerSuccessOrThrow
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.xcmPayment.XcmPaymentApi
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
|
||||
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.withRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
|
||||
private val USED_XCM_VERSION = XcmVersion.V4
|
||||
|
||||
@FeatureScope
|
||||
class DynamicCrossChainTransactor @Inject constructor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val xcmBuilderFactory: XcmBuilder.Factory,
|
||||
private val xcmPaymentApi: XcmPaymentApi,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val usingTypeTransactor: TransferAssetUsingTypeTransactor,
|
||||
) {
|
||||
|
||||
context(ExtrinsicBuilder)
|
||||
suspend fun crossChainTransfer(
|
||||
configuration: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
val call = composeCrossChainTransferCall(configuration, transfer, crossChainFee)
|
||||
call(call)
|
||||
}
|
||||
|
||||
suspend fun requiredRemainingAmountAfterTransfer(
|
||||
configuration: DynamicCrossChainTransferConfiguration
|
||||
): Balance {
|
||||
return if (supportsXcmExecute(configuration)) {
|
||||
BigInteger.ZERO
|
||||
} else {
|
||||
val chainAsset = configuration.originChainAsset
|
||||
assetSourceRegistry.sourceFor(chainAsset).balance.existentialDeposit(chainAsset)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun composeCrossChainTransferCall(
|
||||
configuration: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
): GenericCall.Instance {
|
||||
return if (supportsXcmExecute(configuration)) {
|
||||
composeXcmExecuteCall(configuration, transfer, crossChainFee)
|
||||
} else {
|
||||
usingTypeTransactor.composeCall(configuration, transfer, crossChainFee, forceXcmVersion = USED_XCM_VERSION)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun supportsXcmExecute(originChainId: ChainId, features: DynamicCrossChainTransferFeatures): Boolean {
|
||||
val supportsXcmExecute = features.supportsXcmExecute
|
||||
val hasXcmPaymentApi = xcmPaymentApi.isSupported(originChainId)
|
||||
|
||||
// For now, only enable xcm execute approach for the directions that will hugely benefit from it
|
||||
// In particular, xcm execute allows us to pay delivery fee from the holding register and not in JIT mode (from account)
|
||||
val hasDeliveryFee = features.hasDeliveryFee
|
||||
|
||||
return supportsXcmExecute && hasXcmPaymentApi && hasDeliveryFee
|
||||
}
|
||||
|
||||
private suspend fun supportsXcmExecute(configuration: DynamicCrossChainTransferConfiguration): Boolean {
|
||||
return supportsXcmExecute(configuration.originChainId, configuration.features)
|
||||
}
|
||||
|
||||
private suspend fun composeXcmExecuteCall(
|
||||
configuration: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
): GenericCall.Instance {
|
||||
val xcmProgram = buildXcmProgram(configuration, transfer, crossChainFee)
|
||||
val weight = xcmPaymentApi.queryXcmWeight(configuration.originChainId, xcmProgram)
|
||||
.getInnerSuccessOrThrow("DynamicCrossChainTransactor")
|
||||
|
||||
return chainRegistry.withRuntime(configuration.originChainId) {
|
||||
composeXcmExecute(xcmProgram, weight)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildXcmProgram(
|
||||
configuration: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
): VersionedXcmMessage {
|
||||
val builder = xcmBuilderFactory.createWithoutFeesMeasurement(
|
||||
initial = configuration.originChainLocation,
|
||||
xcmVersion = USED_XCM_VERSION
|
||||
)
|
||||
|
||||
builder.buildTransferProgram(configuration, transfer, crossChainFee)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun XcmBuilder.buildTransferProgram(
|
||||
configuration: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
val totalTransferAmount = transfer.amountPlanks + crossChainFee
|
||||
val assetAbsoluteMultiLocation = configuration.transferType.assetAbsoluteLocation
|
||||
|
||||
// Debug logging for Dynamic XCM transfer
|
||||
Log.d("XCM_DYNAMIC", "=== DYNAMIC XCM TRANSFER DEBUG ===")
|
||||
Log.d("XCM_DYNAMIC", "Origin chain: ${configuration.originChain.chain.name} (${configuration.originChain.chain.id})")
|
||||
Log.d("XCM_DYNAMIC", "Origin parachainId: ${configuration.originChain.parachainId}")
|
||||
Log.d("XCM_DYNAMIC", "Destination chain: ${configuration.destinationChain.chain.name} (${configuration.destinationChain.chain.id})")
|
||||
Log.d("XCM_DYNAMIC", "Destination parachainId: ${configuration.destinationChain.parachainId}")
|
||||
Log.d("XCM_DYNAMIC", "Destination location: ${configuration.destinationChainLocation}")
|
||||
Log.d("XCM_DYNAMIC", "Transfer type: ${configuration.transferType}")
|
||||
Log.d("XCM_DYNAMIC", "================================")
|
||||
|
||||
when (val transferType = configuration.transferType) {
|
||||
is XcmTransferType.Teleport -> buildTeleportProgram(
|
||||
assetLocation = assetAbsoluteMultiLocation,
|
||||
destinationChainLocation = configuration.destinationChainLocation,
|
||||
beneficiary = transfer.recipientAccountId,
|
||||
amount = totalTransferAmount
|
||||
)
|
||||
|
||||
is XcmTransferType.Reserve.Origin -> buildOriginReserveProgram(
|
||||
assetLocation = assetAbsoluteMultiLocation,
|
||||
destinationChainLocation = configuration.destinationChainLocation,
|
||||
beneficiary = transfer.recipientAccountId,
|
||||
amount = totalTransferAmount
|
||||
)
|
||||
|
||||
is XcmTransferType.Reserve.Destination -> buildDestinationReserveProgram(
|
||||
assetLocation = assetAbsoluteMultiLocation,
|
||||
destinationChainLocation = configuration.destinationChainLocation,
|
||||
beneficiary = transfer.recipientAccountId,
|
||||
amount = totalTransferAmount
|
||||
)
|
||||
|
||||
is XcmTransferType.Reserve.Remote -> buildRemoteReserveProgram(
|
||||
assetLocation = assetAbsoluteMultiLocation,
|
||||
remoteReserveLocation = transferType.remoteReserveLocation,
|
||||
destinationChainLocation = configuration.destinationChainLocation,
|
||||
beneficiary = transfer.recipientAccountId,
|
||||
amount = totalTransferAmount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun XcmBuilder.buildTeleportProgram(
|
||||
assetLocation: AbsoluteMultiLocation,
|
||||
destinationChainLocation: ChainLocation,
|
||||
beneficiary: AccountIdKey,
|
||||
amount: Balance,
|
||||
) {
|
||||
val feesAmount = deriveBuyExecutionUpperBoundAmount(amount)
|
||||
|
||||
// Origin
|
||||
withdrawAsset(assetLocation, amount)
|
||||
// Here and onward: we use buy execution for the very first segment to be able to pay delivery fees in sending asset
|
||||
// WeightLimit.one() is used since it doesn't matter anyways as the message on origin is already weighted
|
||||
// The only restriction is that it cannot be zero or Unlimited
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.one())
|
||||
initiateTeleport(MultiAssetFilter.singleCounted(), destinationChainLocation)
|
||||
|
||||
// Destination
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.Unlimited)
|
||||
depositAsset(MultiAssetFilter.singleCounted(), beneficiary)
|
||||
}
|
||||
|
||||
private fun XcmBuilder.buildOriginReserveProgram(
|
||||
assetLocation: AbsoluteMultiLocation,
|
||||
destinationChainLocation: ChainLocation,
|
||||
beneficiary: AccountIdKey,
|
||||
amount: Balance,
|
||||
) {
|
||||
val feesAmount = deriveBuyExecutionUpperBoundAmount(amount)
|
||||
|
||||
// Origin
|
||||
withdrawAsset(assetLocation, amount)
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.one())
|
||||
depositReserveAsset(MultiAssetFilter.singleCounted(), destinationChainLocation)
|
||||
|
||||
// Destination
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.Unlimited)
|
||||
depositAsset(MultiAssetFilter.singleCounted(), beneficiary)
|
||||
}
|
||||
|
||||
private fun XcmBuilder.buildDestinationReserveProgram(
|
||||
assetLocation: AbsoluteMultiLocation,
|
||||
destinationChainLocation: ChainLocation,
|
||||
beneficiary: AccountIdKey,
|
||||
amount: Balance,
|
||||
) {
|
||||
val feesAmount = deriveBuyExecutionUpperBoundAmount(amount)
|
||||
|
||||
// Origin
|
||||
withdrawAsset(assetLocation, amount)
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.one())
|
||||
initiateReserveWithdraw(MultiAssetFilter.singleCounted(), destinationChainLocation)
|
||||
|
||||
// Destination
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.Unlimited)
|
||||
depositAsset(MultiAssetFilter.singleCounted(), beneficiary)
|
||||
}
|
||||
|
||||
private fun XcmBuilder.buildRemoteReserveProgram(
|
||||
assetLocation: AbsoluteMultiLocation,
|
||||
remoteReserveLocation: ChainLocation,
|
||||
destinationChainLocation: ChainLocation,
|
||||
beneficiary: AccountIdKey,
|
||||
amount: Balance,
|
||||
) {
|
||||
val feesAmount = deriveBuyExecutionUpperBoundAmount(amount)
|
||||
|
||||
// Origin
|
||||
withdrawAsset(assetLocation, amount)
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.one())
|
||||
initiateReserveWithdraw(MultiAssetFilter.singleCounted(), remoteReserveLocation)
|
||||
|
||||
// Remote reserve
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.Unlimited)
|
||||
depositReserveAsset(MultiAssetFilter.singleCounted(), destinationChainLocation)
|
||||
|
||||
// Destination
|
||||
buyExecution(assetLocation, feesAmount, WeightLimit.Unlimited)
|
||||
depositAsset(MultiAssetFilter.singleCounted(), beneficiary)
|
||||
}
|
||||
|
||||
private fun deriveBuyExecutionUpperBoundAmount(transferringAmount: Balance): Balance {
|
||||
return transferringAmount / 2.toBigInteger()
|
||||
}
|
||||
|
||||
private fun WeightLimit.Companion.one(): WeightLimit.Limited {
|
||||
return WeightLimit.Limited(WeightV2(1.toBigInteger(), 1.toBigInteger()))
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
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.replaceAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFeeModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.XcmTransferDryRunOrigin
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.XcmTransferDryRunResult
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.XcmTransferDryRunner
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MINIMUM_SEND_AMOUNT = 100
|
||||
|
||||
@FeatureScope
|
||||
class DynamicCrossChainWeigher @Inject constructor(
|
||||
private val xcmTransferDryRunner: XcmTransferDryRunner,
|
||||
) {
|
||||
|
||||
suspend fun estimateFee(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase
|
||||
): CrossChainFeeModel {
|
||||
val safeTransfer = transfer.ensureSafeAmount()
|
||||
val result = xcmTransferDryRunner.dryRunXcmTransfer(config, safeTransfer, XcmTransferDryRunOrigin.Fake)
|
||||
|
||||
return result.getOrNull()?.let { dryRunResult ->
|
||||
CrossChainFeeModel.fromDryRunResult(
|
||||
initialAmount = safeTransfer.amountPlanks,
|
||||
transferDryRunResult = dryRunResult
|
||||
)
|
||||
} ?: run {
|
||||
// Dry run failed - use fallback fee estimation
|
||||
// For teleport transfers, dry run often doesn't produce forwarded XCMs
|
||||
Log.w(LOG_TAG, "Dry run failed for ${config.transferType}, using fallback fee estimation")
|
||||
estimateFallbackFee(config, transfer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback fee estimation when dry run fails.
|
||||
* Uses a conservative percentage of the transfer amount as fee buffer.
|
||||
*/
|
||||
private fun estimateFallbackFee(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase
|
||||
): CrossChainFeeModel {
|
||||
// Use 1% of transfer amount as conservative fee estimate for all transfer types
|
||||
// This covers execution fees on destination chain
|
||||
val estimatedFee = transfer.amountPlanks / 100.toBigInteger()
|
||||
return CrossChainFeeModel(paidFromHolding = estimatedFee.coerceAtLeast(Balance.ZERO))
|
||||
}
|
||||
|
||||
// Ensure we can calculate fee regardless of what user entered
|
||||
private fun AssetTransferBase.ensureSafeAmount(): AssetTransferBase {
|
||||
val minimumSendAmount = destinationChainAsset.planksFromAmount(MINIMUM_SEND_AMOUNT.toBigDecimal())
|
||||
val safeAmount = amountPlanks.coerceAtLeast(minimumSendAmount)
|
||||
return replaceAmount(newAmount = safeAmount)
|
||||
}
|
||||
|
||||
private fun CrossChainFeeModel.Companion.fromDryRunResult(
|
||||
initialAmount: Balance,
|
||||
transferDryRunResult: XcmTransferDryRunResult
|
||||
): CrossChainFeeModel {
|
||||
return with(transferDryRunResult) {
|
||||
// We do not add `remoteReserve.deliveryFee` since it is paid from holding and not by account
|
||||
val paidByAccount = origin.deliveryFee
|
||||
|
||||
val trapped = origin.trapped + remoteReserve?.trapped.orZero()
|
||||
val totalFee = initialAmount - destination.depositedAmount - trapped
|
||||
|
||||
// We do not subtract `origin.deliveryFee` since it is paid directly from the origin account and thus do not contribute towards execution fee
|
||||
// We do not subtract `remoteReserve.deliveryFee` since it is paid from holding and thus is already accounted in totalFee
|
||||
val executionFee = totalFee
|
||||
|
||||
CrossChainFeeModel(paidByAccount = paidByAccount, paidFromHolding = executionFee)
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
|
||||
class XcmTransferDryRunResult(
|
||||
val origin: IntermediateSegment,
|
||||
val remoteReserve: IntermediateSegment?,
|
||||
val destination: FinalSegment,
|
||||
) {
|
||||
|
||||
class IntermediateSegment(
|
||||
val deliveryFee: Balance,
|
||||
val trapped: Balance,
|
||||
)
|
||||
|
||||
class FinalSegment(
|
||||
val depositedAmount: Balance
|
||||
)
|
||||
}
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.xcmPalletNameOrNull
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
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.amount
|
||||
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.planksFromAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.destinationChainLocation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.isRemoteReserve
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.reserve.remoteReserveLocation
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainId
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainLocation
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.calls.composeBatchAll
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.calls.composeDispatchAs
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.DynamicCrossChainTransactor
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.XcmTransferDryRunResult.FinalSegment
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.XcmTransferDryRunResult.IntermediateSegment
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.issuing.AssetIssuerRegistry
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssets
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.requireFungible
|
||||
import io.novafoundation.nova.feature_xcm_api.message.VersionedRawXcmMessage
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.DryRunApi
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.DryRunEffects
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.OriginCaller
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.getByLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.senderXcmVersion
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.getInnerSuccessOrThrow
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.XcmVersion
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.versionedXcm
|
||||
import io.novafoundation.nova.runtime.ext.emptyAccountIdKey
|
||||
import io.novafoundation.nova.runtime.ext.isUtilityAsset
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import javax.inject.Inject
|
||||
|
||||
interface XcmTransferDryRunner {
|
||||
|
||||
suspend fun dryRunXcmTransfer(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
origin: XcmTransferDryRunOrigin
|
||||
): Result<XcmTransferDryRunResult>
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
class RealXcmTransferDryRunner @Inject constructor(
|
||||
private val dryRunApi: DryRunApi,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetIssuerRegistry: AssetIssuerRegistry,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val dynamicCrossChainTransactor: DynamicCrossChainTransactor,
|
||||
) : XcmTransferDryRunner {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MINIMUM_FUND_AMOUNT = 100
|
||||
|
||||
private const val FEES_PAID_FEES_ARGUMENT_INDEX = 1
|
||||
private const val ASSETS_TRAPPED_ARGUMENT_INDEX = 2
|
||||
}
|
||||
|
||||
override suspend fun dryRunXcmTransfer(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
origin: XcmTransferDryRunOrigin
|
||||
): Result<XcmTransferDryRunResult> {
|
||||
return runCatching {
|
||||
val originResult = dryRunOnOrigin(config, transfer, origin)
|
||||
val remoteReserveResult = dryRunOnRemoteReserve(config, originResult.forwardedXcm)
|
||||
val destinationResult = dryRunOnDestination(config, transfer, remoteReserveResult.forwardedXcm)
|
||||
|
||||
XcmTransferDryRunResult(
|
||||
origin = originResult.toPublicResult(),
|
||||
remoteReserve = remoteReserveResult.takeIfRemoteReserve(config)?.toPublicResult(),
|
||||
destination = destinationResult.toPublicResult()
|
||||
)
|
||||
}
|
||||
.onFailure { Log.w(LOG_TAG, "Dry run failed", it) }
|
||||
}
|
||||
|
||||
private suspend fun dryRunOnOrigin(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
origin: XcmTransferDryRunOrigin,
|
||||
): IntermediateDryRunResult {
|
||||
val runtime = chainRegistry.getRuntime(config.originChainId)
|
||||
val xcmResultsVersion = XcmVersion.V4
|
||||
|
||||
val (dryRunCall, dryRunOrigin) = constructDryRunCallParams(config, transfer, origin, runtime)
|
||||
val dryRunResult = dryRunApi.dryRunCall(dryRunOrigin, dryRunCall, xcmResultsVersion, config.originChainId)
|
||||
.getInnerSuccessOrThrow(LOG_TAG)
|
||||
|
||||
val nextHopLocation = (config.transferType.remoteReserveLocation() ?: config.destinationChainLocation).location
|
||||
|
||||
val forwardedXcm = searchForwardedXcm(
|
||||
dryRunEffects = dryRunResult,
|
||||
destination = nextHopLocation.fromPointOfViewOf(config.originChainLocation.location),
|
||||
)
|
||||
val deliveryFee = searchDeliveryFee(dryRunResult, runtime)
|
||||
val trappedAssets = searchTrappedAssets(dryRunResult, runtime)
|
||||
|
||||
return IntermediateDryRunResult(forwardedXcm, deliveryFee, trappedAssets)
|
||||
}
|
||||
|
||||
private suspend fun constructDryRunCallParams(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
origin: XcmTransferDryRunOrigin,
|
||||
runtime: RuntimeSnapshot
|
||||
): OriginCallParams {
|
||||
return when (origin) {
|
||||
XcmTransferDryRunOrigin.Fake -> constructDryRunCallFromFakeOrigin(transfer, config, runtime)
|
||||
|
||||
is XcmTransferDryRunOrigin.Signed -> constructDryRunCallFromRealOrigin(transfer, config, origin)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun constructDryRunCallFromRealOrigin(
|
||||
transfer: AssetTransferBase,
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
origin: XcmTransferDryRunOrigin.Signed,
|
||||
): OriginCallParams {
|
||||
val callOnOrigin = dynamicCrossChainTransactor.composeCrossChainTransferCall(config, transfer, crossChainFee = origin.crossChainFee)
|
||||
|
||||
return OriginCallParams(
|
||||
call = callOnOrigin,
|
||||
origin = OriginCaller.System.Signed(origin.accountId)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun constructDryRunCallFromFakeOrigin(
|
||||
transfer: AssetTransferBase,
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
runtime: RuntimeSnapshot,
|
||||
): OriginCallParams {
|
||||
val callOnOrigin = dynamicCrossChainTransactor.composeCrossChainTransferCall(config, transfer, crossChainFee = Balance.ZERO)
|
||||
|
||||
val dryRunAccount = transfer.originChain.emptyAccountIdKey()
|
||||
val transferOrigin = OriginCaller.System.Signed(dryRunAccount)
|
||||
|
||||
val calls = buildList {
|
||||
addFundCalls(transfer, dryRunAccount)
|
||||
|
||||
val transferCallFromOrigin = runtime.composeDispatchAs(callOnOrigin, transferOrigin)
|
||||
add(transferCallFromOrigin)
|
||||
}
|
||||
|
||||
val finalOriginCall = runtime.composeBatchAll(calls)
|
||||
return OriginCallParams(finalOriginCall, OriginCaller.System.Root)
|
||||
}
|
||||
|
||||
private suspend fun MutableList<GenericCall.Instance>.addFundCalls(transfer: AssetTransferBase, dryRunAccount: AccountIdKey) {
|
||||
val fundAmount = determineFundAmount(transfer)
|
||||
|
||||
// Fund native asset first so we can later fund potentially non-sufficient assets
|
||||
if (!transfer.originChainAsset.isUtilityAsset) {
|
||||
// Additionally fund native asset to pay delivery fees
|
||||
val nativeAsset = transfer.originChain.utilityAsset
|
||||
val planks = nativeAsset.planksFromAmount(MINIMUM_FUND_AMOUNT.toBigDecimal())
|
||||
val fundNativeAssetCall = assetIssuerRegistry.create(nativeAsset).composeIssueCall(planks, dryRunAccount)
|
||||
add(fundNativeAssetCall)
|
||||
}
|
||||
|
||||
val fundSendingAssetCall = assetIssuerRegistry.create(transfer.originChainAsset).composeIssueCall(fundAmount, dryRunAccount)
|
||||
add(fundSendingAssetCall)
|
||||
}
|
||||
|
||||
private suspend fun dryRunOnRemoteReserve(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
forwardedFromOrigin: VersionedRawXcmMessage,
|
||||
): IntermediateDryRunResult {
|
||||
// No remote reserve - nothing to dry run, return unchanged value
|
||||
val remoteReserveLocation = config.transferType.remoteReserveLocation()
|
||||
?: return IntermediateDryRunResult(forwardedFromOrigin, Balance.ZERO, Balance.ZERO)
|
||||
|
||||
val runtime = chainRegistry.getRuntime(remoteReserveLocation.chainId)
|
||||
|
||||
val originLocation = config.originChainLocation.location
|
||||
val destinationLocation = config.destinationChainLocation.location
|
||||
|
||||
val usedXcmVersion = forwardedFromOrigin.version
|
||||
|
||||
val dryRunOrigin = originLocation.fromPointOfViewOf(remoteReserveLocation.location).versionedXcm(usedXcmVersion)
|
||||
val dryRunResult = dryRunApi.dryRunXcm(dryRunOrigin, forwardedFromOrigin, remoteReserveLocation.chainId)
|
||||
.getInnerSuccessOrThrow(LOG_TAG)
|
||||
|
||||
val destinationOnRemoteReserve = destinationLocation.fromPointOfViewOf(remoteReserveLocation.location)
|
||||
|
||||
val forwardedXcm = searchForwardedXcm(
|
||||
dryRunEffects = dryRunResult,
|
||||
destination = destinationOnRemoteReserve,
|
||||
)
|
||||
val deliveryFee = searchDeliveryFee(dryRunResult, runtime)
|
||||
val trappedAssets = searchTrappedAssets(dryRunResult, runtime)
|
||||
|
||||
return IntermediateDryRunResult(forwardedXcm, deliveryFee, trappedAssets)
|
||||
}
|
||||
|
||||
private suspend fun dryRunOnDestination(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
forwardedFromPrevious: VersionedRawXcmMessage,
|
||||
): FinalDryRunResult {
|
||||
val previousLocation = (config.transferType.remoteReserveLocation() ?: config.originChainLocation).location
|
||||
val destinationLocation = config.destinationChainLocation
|
||||
|
||||
val usedXcmVersion = forwardedFromPrevious.version
|
||||
|
||||
val dryRunOrigin = previousLocation.fromPointOfViewOf(destinationLocation.location).versionedXcm(usedXcmVersion)
|
||||
val dryRunResult = dryRunApi.dryRunXcm(dryRunOrigin, forwardedFromPrevious, destinationLocation.chainId)
|
||||
.getInnerSuccessOrThrow(LOG_TAG)
|
||||
|
||||
val depositedAmount = searchDepositAmount(dryRunResult, transfer.destinationChainAsset, transfer.recipientAccountId)
|
||||
|
||||
return FinalDryRunResult(depositedAmount)
|
||||
}
|
||||
|
||||
private fun searchForwardedXcm(
|
||||
dryRunEffects: DryRunEffects,
|
||||
destination: RelativeMultiLocation,
|
||||
): VersionedRawXcmMessage {
|
||||
return searchForwardedXcmInQueues(dryRunEffects, destination)
|
||||
}
|
||||
|
||||
private suspend fun searchDepositAmount(
|
||||
dryRunEffects: DryRunEffects,
|
||||
chainAsset: Chain.Asset,
|
||||
recipientAccountId: AccountIdKey,
|
||||
): Balance {
|
||||
val depositDetector = assetSourceRegistry.getEventDetector(chainAsset)
|
||||
|
||||
val deposits = dryRunEffects.emittedEvents.mapNotNull { depositDetector.detectDeposit(it) }
|
||||
.filter { it.destination == recipientAccountId }
|
||||
|
||||
if (deposits.isEmpty()) error("No deposits detected")
|
||||
|
||||
return deposits.sumOf { it.amount }
|
||||
}
|
||||
|
||||
private fun searchDeliveryFee(
|
||||
dryRunEffects: DryRunEffects,
|
||||
runtimeSnapshot: RuntimeSnapshot,
|
||||
): Balance {
|
||||
val xcmPalletName = runtimeSnapshot.metadata.xcmPalletNameOrNull() ?: return Balance.ZERO
|
||||
val event = dryRunEffects.emittedEvents.findEvent(xcmPalletName, "FeesPaid") ?: return Balance.ZERO
|
||||
|
||||
val usedXcmVersion = dryRunEffects.senderXcmVersion()
|
||||
|
||||
val feesDecoded = event.arguments[FEES_PAID_FEES_ARGUMENT_INDEX]
|
||||
val multiAssets = MultiAssets.bind(feesDecoded, usedXcmVersion)
|
||||
|
||||
return multiAssets.extractFirstAmount()
|
||||
}
|
||||
|
||||
private fun searchTrappedAssets(
|
||||
dryRunEffects: DryRunEffects,
|
||||
runtimeSnapshot: RuntimeSnapshot,
|
||||
): Balance {
|
||||
val xcmPalletName = runtimeSnapshot.metadata.xcmPalletNameOrNull() ?: return Balance.ZERO
|
||||
val event = dryRunEffects.emittedEvents.findEvent(xcmPalletName, "AssetsTrapped") ?: return Balance.ZERO
|
||||
|
||||
val feesDecoded = event.arguments[ASSETS_TRAPPED_ARGUMENT_INDEX]
|
||||
val multiAssets = MultiAssets.bindVersioned(feesDecoded).xcm
|
||||
|
||||
return multiAssets.extractFirstAmount()
|
||||
}
|
||||
|
||||
private fun MultiAssets.extractFirstAmount(): Balance {
|
||||
return if (value.isNotEmpty()) {
|
||||
value.first().requireFungible().amount
|
||||
} else {
|
||||
Balance.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchForwardedXcmInQueues(
|
||||
dryRunEffects: DryRunEffects,
|
||||
destination: RelativeMultiLocation
|
||||
): VersionedRawXcmMessage {
|
||||
val forwardedXcms = dryRunEffects.forwardedXcms
|
||||
|
||||
// For teleport transfers, forwarded XCMs might be empty or structured differently
|
||||
if (forwardedXcms.isEmpty()) {
|
||||
error("Dry run did not produce any forwarded XCMs. This transfer type may not support dry run fee estimation.")
|
||||
}
|
||||
|
||||
val usedXcmVersion = dryRunEffects.senderXcmVersion()
|
||||
val versionedDestination = destination.versionedXcm(usedXcmVersion)
|
||||
|
||||
val forwardedXcmsToDestination = forwardedXcms.getByLocation(versionedDestination)
|
||||
|
||||
// If destination location not found, try first available forwarded XCM
|
||||
if (forwardedXcmsToDestination.isEmpty()) {
|
||||
Log.w(LOG_TAG, "No forwarded XCM found for destination $destination, using first available")
|
||||
val firstAvailable = forwardedXcms.firstOrNull()?.second?.firstOrNull()
|
||||
return firstAvailable ?: error("No forwarded XCMs available for dry run")
|
||||
}
|
||||
|
||||
// There should only be one forwarded message during dry run
|
||||
return forwardedXcmsToDestination.first()
|
||||
}
|
||||
|
||||
private fun determineFundAmount(transfer: AssetTransferBase): Balance {
|
||||
val amount = (transfer.amount() * 2.toBigDecimal()).coerceAtLeast(MINIMUM_FUND_AMOUNT.toBigDecimal())
|
||||
return transfer.originChainAsset.planksFromAmount(amount)
|
||||
}
|
||||
|
||||
private fun IntermediateDryRunResult.toPublicResult(): IntermediateSegment {
|
||||
return IntermediateSegment(
|
||||
deliveryFee = deliveryFee,
|
||||
trapped = trapped
|
||||
)
|
||||
}
|
||||
|
||||
private fun FinalDryRunResult.toPublicResult(): FinalSegment {
|
||||
return FinalSegment(depositedAmount = depositedAmount)
|
||||
}
|
||||
|
||||
private fun IntermediateDryRunResult.takeIfRemoteReserve(config: DynamicCrossChainTransferConfiguration): IntermediateDryRunResult? {
|
||||
return takeIf { config.transferType.isRemoteReserve() }
|
||||
}
|
||||
|
||||
private class IntermediateDryRunResult(
|
||||
val forwardedXcm: VersionedRawXcmMessage,
|
||||
val deliveryFee: Balance,
|
||||
val trapped: Balance,
|
||||
)
|
||||
|
||||
private class FinalDryRunResult(
|
||||
val depositedAmount: Balance
|
||||
)
|
||||
|
||||
private data class OriginCallParams(val call: GenericCall.Instance, val origin: OriginCaller)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.issuing
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.OriginCaller
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
|
||||
interface AssetIssuer {
|
||||
|
||||
/**
|
||||
* Compose a call to issue [amount] of tokens to [destination]
|
||||
* Implementation can assume execution happens under [OriginCaller.System.Root]
|
||||
*/
|
||||
suspend fun composeIssueCall(amount: Balance, destination: AccountIdKey): GenericCall.Instance
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.issuing
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.StatemineAssetsRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.Type
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import javax.inject.Inject
|
||||
|
||||
interface AssetIssuerRegistry {
|
||||
|
||||
suspend fun create(chainAsset: Chain.Asset): AssetIssuer
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
class RealAssetIssuerRegistry @Inject constructor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val statemineAssetsRepository: StatemineAssetsRepository,
|
||||
) : AssetIssuerRegistry {
|
||||
|
||||
override suspend fun create(chainAsset: Chain.Asset): AssetIssuer {
|
||||
val runtime = chainRegistry.getRuntime(chainAsset.chainId)
|
||||
|
||||
return when (val type = chainAsset.type) {
|
||||
is Type.Native -> NativeAssetIssuer(runtime)
|
||||
is Type.Statemine -> StatemineAssetIssuer(chainAsset.chainId, type, runtime, statemineAssetsRepository)
|
||||
is Type.Orml -> OrmlAssetIssuer(type, runtime)
|
||||
else -> error("Unsupported asset type: $type for ${chainAsset.symbol} on ${chainAsset.chainId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.issuing
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.composeCall
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novafoundation.nova.common.utils.PezkuwiAddressConstructor
|
||||
|
||||
class NativeAssetIssuer(
|
||||
private val runtimeSnapshot: RuntimeSnapshot
|
||||
) : AssetIssuer {
|
||||
|
||||
override suspend fun composeIssueCall(amount: Balance, destination: AccountIdKey): GenericCall.Instance {
|
||||
return runtimeSnapshot.composeCall(
|
||||
moduleName = Modules.BALANCES,
|
||||
callName = "force_set_balance",
|
||||
arguments = mapOf(
|
||||
"who" to PezkuwiAddressConstructor.constructInstance(runtimeSnapshot.typeRegistry, destination.value),
|
||||
"new_free" to amount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.issuing
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.utils.Modules
|
||||
import io.novafoundation.nova.common.utils.composeCall
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.runtime.ext.currencyId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novafoundation.nova.common.utils.PezkuwiAddressConstructor
|
||||
|
||||
class OrmlAssetIssuer(
|
||||
private val ormlType: Chain.Asset.Type.Orml,
|
||||
private val runtimeSnapshot: RuntimeSnapshot
|
||||
) : AssetIssuer {
|
||||
|
||||
override suspend fun composeIssueCall(amount: Balance, destination: AccountIdKey): GenericCall.Instance {
|
||||
return runtimeSnapshot.composeCall(
|
||||
moduleName = Modules.TOKENS,
|
||||
callName = "set_balance",
|
||||
arguments = mapOf(
|
||||
"who" to PezkuwiAddressConstructor.constructInstance(runtimeSnapshot.typeRegistry, destination.value),
|
||||
"currency_id" to ormlType.currencyId(runtimeSnapshot),
|
||||
"new_free" to amount,
|
||||
"new_reserved" to Balance.ZERO
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic.dryRun.issuing
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.utils.composeCall
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.StatemineAssetsRepository
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.calls.composeDispatchAs
|
||||
import io.novafoundation.nova.feature_xcm_api.runtimeApi.dryRun.model.OriginCaller
|
||||
import io.novafoundation.nova.runtime.ext.palletNameOrDefault
|
||||
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.prepareIdForEncoding
|
||||
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import io.novafoundation.nova.common.utils.PezkuwiAddressConstructor
|
||||
|
||||
class StatemineAssetIssuer(
|
||||
private val chainId: ChainId,
|
||||
private val assetType: Chain.Asset.Type.Statemine,
|
||||
private val runtimeSnapshot: RuntimeSnapshot,
|
||||
private val statemineAssetsRepository: StatemineAssetsRepository,
|
||||
) : AssetIssuer {
|
||||
|
||||
override suspend fun composeIssueCall(amount: Balance, destination: AccountIdKey): GenericCall.Instance {
|
||||
val assetDetails = statemineAssetsRepository.getAssetDetails(chainId, assetType)
|
||||
val issuer = assetDetails.issuer
|
||||
|
||||
// We're dispatching as issuer since only issuer is allowed to mint tokens
|
||||
return runtimeSnapshot.composeDispatchAs(
|
||||
call = composeMint(amount, destination),
|
||||
origin = OriginCaller.System.Signed(issuer)
|
||||
)
|
||||
}
|
||||
|
||||
private fun composeMint(amount: Balance, destination: AccountIdKey): GenericCall.Instance {
|
||||
return runtimeSnapshot.composeCall(
|
||||
moduleName = assetType.palletNameOrDefault(),
|
||||
callName = "mint",
|
||||
arguments = mapOf(
|
||||
"id" to assetType.prepareIdForEncoding(runtimeSnapshot),
|
||||
"beneficiary" to PezkuwiAddressConstructor.constructInstance(runtimeSnapshot.typeRegistry, destination.value),
|
||||
"amount" to amount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.JunctionsRemote
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import java.math.BigInteger
|
||||
|
||||
class LegacyCrossChainTransfersConfigRemote(
|
||||
val assetsLocation: Map<String, LegacyReserveLocationRemote>?,
|
||||
val instructions: Map<String, List<String>>?,
|
||||
val networkDeliveryFee: Map<String, LegacyNetworkDeliveryFeeRemote>?,
|
||||
val networkBaseWeight: Map<String, BigInteger>?,
|
||||
val chains: List<LegacyCrossChainOriginChainRemote>?
|
||||
)
|
||||
|
||||
class LegacyReserveLocationRemote(
|
||||
val chainId: ChainId,
|
||||
val reserveFee: LegacyXcmFeeRemote?,
|
||||
val multiLocation: JunctionsRemote
|
||||
)
|
||||
|
||||
class LegacyNetworkDeliveryFeeRemote(
|
||||
val toParent: LegacyDeliveryFeeConfigRemote?,
|
||||
val toParachain: LegacyDeliveryFeeConfigRemote?
|
||||
)
|
||||
|
||||
class LegacyDeliveryFeeConfigRemote(
|
||||
val type: String,
|
||||
val factorPallet: String,
|
||||
val sizeBase: BigInteger,
|
||||
val sizeFactor: BigInteger,
|
||||
val alwaysHoldingPays: Boolean?
|
||||
)
|
||||
|
||||
class LegacyCrossChainOriginChainRemote(
|
||||
val chainId: ChainId,
|
||||
val assets: List<LegacyCrossChainOriginAssetRemote>
|
||||
)
|
||||
|
||||
class LegacyCrossChainOriginAssetRemote(
|
||||
val assetId: Int,
|
||||
val assetLocation: String,
|
||||
val assetLocationPath: LegacyAssetLocationPathRemote,
|
||||
val xcmTransfers: List<LegacyXcmTransferRemote>,
|
||||
)
|
||||
|
||||
class LegacyXcmTransferRemote(
|
||||
val destination: LegacyXcmDestinationRemote,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
class LegacyXcmDestinationRemote(
|
||||
val chainId: ChainId,
|
||||
val assetId: Int,
|
||||
val fee: LegacyXcmFeeRemote
|
||||
)
|
||||
|
||||
class LegacyXcmFeeRemote(
|
||||
val mode: Mode,
|
||||
val instructions: String
|
||||
) {
|
||||
|
||||
class Mode(
|
||||
val type: String,
|
||||
val value: String?
|
||||
)
|
||||
}
|
||||
|
||||
class LegacyAssetLocationPathRemote(
|
||||
val type: String,
|
||||
val path: JunctionsRemote?
|
||||
)
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy
|
||||
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Weight
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.xTokensName
|
||||
import io.novafoundation.nova.common.utils.xcmPalletName
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
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.legacy.LegacyCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyXcmTransferMethod
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.common.TransferAssetUsingTypeTransactor
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAsset
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssets
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.plus
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.toMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.orDefault
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.versionedXcm
|
||||
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.builder.ExtrinsicBuilder
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.call
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class LegacyCrossChainTransactor @Inject constructor(
|
||||
private val weigher: LegacyCrossChainWeigher,
|
||||
private val xcmVersionDetector: XcmVersionDetector,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val usingTypeTransactor: TransferAssetUsingTypeTransactor,
|
||||
) {
|
||||
|
||||
context(ExtrinsicBuilder)
|
||||
suspend fun crossChainTransfer(
|
||||
configuration: LegacyCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
when (configuration.transferMethod) {
|
||||
LegacyXcmTransferMethod.X_TOKENS -> xTokensTransfer(configuration, transfer, crossChainFee)
|
||||
LegacyXcmTransferMethod.XCM_PALLET_RESERVE -> xcmPalletReserveTransfer(configuration, transfer, crossChainFee)
|
||||
LegacyXcmTransferMethod.XCM_PALLET_TELEPORT -> xcmPalletTeleport(configuration, transfer, crossChainFee)
|
||||
LegacyXcmTransferMethod.XCM_PALLET_TRANSFER_ASSETS -> xcmPalletTransferAssets(configuration, transfer, crossChainFee)
|
||||
LegacyXcmTransferMethod.UNKNOWN -> throw IllegalArgumentException("Unknown transfer type")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requiredRemainingAmountAfterTransfer(
|
||||
configuration: LegacyCrossChainTransferConfiguration
|
||||
): Balance {
|
||||
val chainAsset = configuration.originChainAsset
|
||||
return assetSourceRegistry.sourceFor(chainAsset).balance.existentialDeposit(chainAsset)
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicBuilder.xTokensTransfer(
|
||||
configuration: LegacyCrossChainTransferConfiguration,
|
||||
assetTransfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
val multiAsset = configuration.multiAssetFor(assetTransfer, crossChainFee)
|
||||
val fullDestinationLocation = configuration.destinationChainLocation + assetTransfer.beneficiaryLocation()
|
||||
val requiredDestWeight = weigher.estimateRequiredDestWeight(configuration)
|
||||
|
||||
val lowestMultiLocationVersion = xcmVersionDetector.lowestPresentMultiLocationVersion(assetTransfer.originChain.id).orDefault()
|
||||
val lowestMultiAssetVersion = xcmVersionDetector.lowestPresentMultiAssetVersion(assetTransfer.originChain.id).orDefault()
|
||||
|
||||
call(
|
||||
moduleName = runtime.metadata.xTokensName(),
|
||||
callName = "transfer_multiasset",
|
||||
arguments = mapOf(
|
||||
"asset" to multiAsset.versionedXcm(lowestMultiAssetVersion).toEncodableInstance(),
|
||||
"dest" to fullDestinationLocation.versionedXcm(lowestMultiLocationVersion).toEncodableInstance(),
|
||||
|
||||
// depending on the version of the pallet, only one of weights arguments going to be encoded
|
||||
"dest_weight" to destWeightEncodable(requiredDestWeight),
|
||||
"dest_weight_limit" to WeightLimit.Unlimited.toEncodableInstance()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun destWeightEncodable(weight: Weight): Any = weight
|
||||
|
||||
private suspend fun ExtrinsicBuilder.xcmPalletTransferAssets(
|
||||
configuration: LegacyCrossChainTransferConfiguration,
|
||||
assetTransfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
val call = usingTypeTransactor.composeCall(configuration, assetTransfer, crossChainFee)
|
||||
call(call)
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicBuilder.xcmPalletReserveTransfer(
|
||||
configuration: LegacyCrossChainTransferConfiguration,
|
||||
assetTransfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
xcmPalletTransfer(
|
||||
configuration = configuration,
|
||||
assetTransfer = assetTransfer,
|
||||
crossChainFee = crossChainFee,
|
||||
callName = "limited_reserve_transfer_assets"
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicBuilder.xcmPalletTeleport(
|
||||
configuration: LegacyCrossChainTransferConfiguration,
|
||||
assetTransfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
) {
|
||||
xcmPalletTransfer(
|
||||
configuration = configuration,
|
||||
assetTransfer = assetTransfer,
|
||||
crossChainFee = crossChainFee,
|
||||
callName = "limited_teleport_assets"
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun ExtrinsicBuilder.xcmPalletTransfer(
|
||||
configuration: LegacyCrossChainTransferConfiguration,
|
||||
assetTransfer: AssetTransferBase,
|
||||
crossChainFee: Balance,
|
||||
callName: String
|
||||
) {
|
||||
val lowestMultiLocationVersion = xcmVersionDetector.lowestPresentMultiLocationVersion(assetTransfer.originChain.id).orDefault()
|
||||
val lowestMultiAssetsVersion = xcmVersionDetector.lowestPresentMultiAssetsVersion(assetTransfer.originChain.id).orDefault()
|
||||
|
||||
val multiAsset = configuration.multiAssetFor(assetTransfer, crossChainFee)
|
||||
|
||||
call(
|
||||
moduleName = runtime.metadata.xcmPalletName(),
|
||||
callName = callName,
|
||||
arguments = mapOf(
|
||||
"dest" to configuration.destinationChainLocation.versionedXcm(lowestMultiLocationVersion).toEncodableInstance(),
|
||||
"beneficiary" to assetTransfer.beneficiaryLocation().versionedXcm(lowestMultiLocationVersion).toEncodableInstance(),
|
||||
"assets" to MultiAssets(multiAsset).versionedXcm(lowestMultiAssetsVersion).toEncodableInstance(),
|
||||
"fee_asset_item" to BigInteger.ZERO,
|
||||
"weight_limit" to WeightLimit.Unlimited.toEncodableInstance()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.multiAssetFor(
|
||||
transfer: AssetTransferBase,
|
||||
crossChainFee: Balance
|
||||
): MultiAsset {
|
||||
// we add cross chain fee top of entered amount so received amount will be no less than entered one
|
||||
val planks = transfer.amountPlanks + crossChainFee
|
||||
return MultiAsset.from(assetLocation, planks)
|
||||
}
|
||||
|
||||
private fun AssetTransferBase.beneficiaryLocation(): RelativeMultiLocation {
|
||||
val accountId = destinationChain.accountIdOrDefault(recipient).intoKey()
|
||||
return accountId.toMultiLocation()
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.legacy
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.ParaId
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.Weight
|
||||
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.BigRational
|
||||
import io.novafoundation.nova.common.utils.argument
|
||||
import io.novafoundation.nova.common.utils.fixedU128
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.utils.requireActualType
|
||||
import io.novafoundation.nova.common.utils.xcmPalletName
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFeeModel
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.orZero
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.plus
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.zero
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.CrossChainFeeConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.DeliveryFeeConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.LegacyCrossChainTransfersConfiguration.XcmFee.Mode
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.XCMInstructionType
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.legacy.weightToFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.originChainId
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.xcmExecute
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAsset
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssetFilter
|
||||
import io.novafoundation.nova.feature_xcm_api.asset.MultiAssets
|
||||
import io.novafoundation.nova.feature_xcm_api.message.XcmInstruction
|
||||
import io.novafoundation.nova.feature_xcm_api.message.XcmMessage
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.RelativeMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.isHere
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.paraIdOrNull
|
||||
import io.novafoundation.nova.feature_xcm_api.multiLocation.toMultiLocation
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.VersionedXcm
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.detector.XcmVersionDetector
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.orDefault
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.toEncodableInstance
|
||||
import io.novafoundation.nova.feature_xcm_api.versions.versionedXcm
|
||||
import io.novafoundation.nova.feature_xcm_api.weight.WeightLimit
|
||||
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
|
||||
import io.novafoundation.nova.runtime.ext.emptyAccountIdKey
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.bytes
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.call
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.module
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storage
|
||||
import java.math.BigInteger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
// TODO: Currently message doesn't contain setTopic command in the end. It will come with XCMv3 support
|
||||
private const val SET_TOPIC_SIZE = 33
|
||||
|
||||
@FeatureScope
|
||||
class LegacyCrossChainWeigher @Inject constructor(
|
||||
@Named(REMOTE_STORAGE_SOURCE)
|
||||
private val storageDataSource: StorageDataSource,
|
||||
private val extrinsicService: ExtrinsicService,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val xcmVersionDetector: XcmVersionDetector
|
||||
) {
|
||||
|
||||
fun estimateRequiredDestWeight(transferConfiguration: LegacyCrossChainTransferConfiguration): Weight {
|
||||
val destinationWeight = transferConfiguration.destinationFee.estimatedWeight()
|
||||
val reserveWeight = transferConfiguration.reserveFee?.estimatedWeight().orZero()
|
||||
|
||||
return destinationWeight.max(reserveWeight)
|
||||
}
|
||||
|
||||
suspend fun estimateFee(amount: Balance, config: LegacyCrossChainTransferConfiguration): CrossChainFeeModel = with(config) {
|
||||
// Reserve fee may be zero if xcm transfer doesn't reserve tokens
|
||||
val reserveFeeAmount = calculateFee(amount, reserveFee, reserveChainLocation)
|
||||
val destinationFeeAmount = calculateFee(amount, destinationFee, destinationChainLocation)
|
||||
|
||||
return reserveFeeAmount + destinationFeeAmount
|
||||
}
|
||||
|
||||
private suspend fun LegacyCrossChainTransferConfiguration.calculateFee(
|
||||
amount: Balance,
|
||||
feeConfig: CrossChainFeeConfiguration?,
|
||||
chainLocation: RelativeMultiLocation
|
||||
): CrossChainFeeModel {
|
||||
return when (feeConfig) {
|
||||
null -> CrossChainFeeModel.zero()
|
||||
else -> {
|
||||
val isSendingFromOrigin = originChainId == feeConfig.from.chainId
|
||||
val feeAmount = feeFor(amount, feeConfig)
|
||||
val deliveryFee = deliveryFeeFor(amount, feeConfig, chainLocation, isSendingFromOrigin = isSendingFromOrigin)
|
||||
feeAmount.orZero() + deliveryFee.orZero()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun LegacyCrossChainTransferConfiguration.feeFor(amount: Balance, feeConfig: CrossChainFeeConfiguration): CrossChainFeeModel {
|
||||
val chain = chainRegistry.getChain(feeConfig.to.chainId)
|
||||
val maxWeight = feeConfig.estimatedWeight()
|
||||
|
||||
return when (val mode = feeConfig.to.xcmFeeType.mode) {
|
||||
is Mode.Proportional -> CrossChainFeeModel(paidFromHolding = mode.weightToFee(maxWeight))
|
||||
|
||||
Mode.Standard -> {
|
||||
val xcmMessage = xcmMessage(feeConfig.to.xcmFeeType.instructions, chain, amount)
|
||||
|
||||
val paymentInfo = extrinsicService.paymentInfo(
|
||||
chain,
|
||||
TransactionOrigin.SelectedWallet
|
||||
) {
|
||||
xcmExecute(xcmMessage, maxWeight)
|
||||
}
|
||||
|
||||
CrossChainFeeModel(paidFromHolding = paymentInfo.partialFee)
|
||||
}
|
||||
|
||||
Mode.Unknown -> CrossChainFeeModel.zero()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun LegacyCrossChainTransferConfiguration.deliveryFeeFor(
|
||||
amount: Balance,
|
||||
config: CrossChainFeeConfiguration,
|
||||
destinationChainLocation: RelativeMultiLocation,
|
||||
isSendingFromOrigin: Boolean
|
||||
): CrossChainFeeModel {
|
||||
val deliveryFeeConfiguration = config.from.deliveryFeeConfiguration ?: return CrossChainFeeModel.zero()
|
||||
|
||||
val deliveryConfig = deliveryFeeConfiguration.getDeliveryConfig(destinationChainLocation)
|
||||
|
||||
val deliveryFeeFactor: BigInteger = queryDeliveryFeeFactor(config.from.chainId, deliveryConfig.factorPallet, destinationChainLocation)
|
||||
|
||||
val xcmMessageSize = getXcmMessageSize(amount, config)
|
||||
val xcmMessageSizeWithTopic = xcmMessageSize + SET_TOPIC_SIZE.toBigInteger()
|
||||
|
||||
val feeSize = (deliveryConfig.sizeBase + xcmMessageSizeWithTopic * deliveryConfig.sizeFactor)
|
||||
val deliveryFee = BigRational.fixedU128(deliveryFeeFactor * feeSize).integralQuotient
|
||||
|
||||
val isSenderPaysOriginDelivery = !deliveryConfig.alwaysHoldingPays
|
||||
return if (isSenderPaysOriginDelivery && isSendingFromOrigin) {
|
||||
CrossChainFeeModel(paidByAccount = deliveryFee)
|
||||
} else {
|
||||
CrossChainFeeModel(paidFromHolding = deliveryFee)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun LegacyCrossChainTransferConfiguration.getXcmMessageSize(amount: Balance, config: CrossChainFeeConfiguration): BigInteger {
|
||||
val chain = chainRegistry.getChain(config.to.chainId)
|
||||
val runtime = chainRegistry.getRuntime(config.to.chainId)
|
||||
val xcmMessage = xcmMessage(config.to.xcmFeeType.instructions, chain, amount)
|
||||
.toEncodableInstance()
|
||||
|
||||
return runtime.metadata
|
||||
.module(runtime.metadata.xcmPalletName())
|
||||
.call("execute")
|
||||
.argument("message")
|
||||
.requireActualType()
|
||||
.bytes(runtime, xcmMessage)
|
||||
.size.toBigInteger()
|
||||
}
|
||||
|
||||
private fun DeliveryFeeConfiguration.getDeliveryConfig(destinationChainLocation: RelativeMultiLocation): DeliveryFeeConfiguration.Type.Exponential {
|
||||
val isParent = destinationChainLocation.interior.isHere()
|
||||
|
||||
val configType = when {
|
||||
isParent -> toParent
|
||||
else -> toParachain
|
||||
}
|
||||
|
||||
return configType.asExponentialOrThrow()
|
||||
}
|
||||
|
||||
private fun DeliveryFeeConfiguration.Type?.asExponentialOrThrow(): DeliveryFeeConfiguration.Type.Exponential {
|
||||
return this as? DeliveryFeeConfiguration.Type.Exponential ?: throw IllegalStateException("Unknown delivery fee type")
|
||||
}
|
||||
|
||||
private suspend fun queryDeliveryFeeFactor(
|
||||
chainId: ChainId,
|
||||
pallet: String,
|
||||
destinationMultiLocation: RelativeMultiLocation,
|
||||
): BigInteger {
|
||||
return when {
|
||||
destinationMultiLocation.interior.isHere() -> xcmParentDeliveryFeeFactor(chainId, pallet)
|
||||
else -> {
|
||||
val paraId = destinationMultiLocation.paraIdOrNull() ?: throw IllegalStateException("ParaId must be not null")
|
||||
xcmParachainDeliveryFeeFactor(chainId, pallet, paraId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CrossChainFeeConfiguration.estimatedWeight(): Weight {
|
||||
val instructionTypes = to.xcmFeeType.instructions
|
||||
|
||||
return to.instructionWeight * instructionTypes.size.toBigInteger()
|
||||
}
|
||||
|
||||
private suspend fun LegacyCrossChainTransferConfiguration.xcmMessage(
|
||||
instructionTypes: List<XCMInstructionType>,
|
||||
chain: Chain,
|
||||
amount: Balance
|
||||
): VersionedXcm<XcmMessage> {
|
||||
val instructions = instructionTypes.mapNotNull { instructionType -> xcmInstruction(instructionType, chain, amount) }
|
||||
val message = XcmMessage(instructions)
|
||||
val xcmVersion = xcmVersionDetector.lowestPresentMultiLocationVersion(chain.id).orDefault()
|
||||
|
||||
return message.versionedXcm(xcmVersion)
|
||||
}
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.xcmInstruction(
|
||||
instructionType: XCMInstructionType,
|
||||
chain: Chain,
|
||||
amount: Balance
|
||||
): XcmInstruction? {
|
||||
return when (instructionType) {
|
||||
XCMInstructionType.ReserveAssetDeposited -> reserveAssetDeposited(amount)
|
||||
XCMInstructionType.ClearOrigin -> clearOrigin()
|
||||
XCMInstructionType.BuyExecution -> buyExecution(amount)
|
||||
XCMInstructionType.DepositAsset -> depositAsset(chain)
|
||||
XCMInstructionType.WithdrawAsset -> withdrawAsset(amount)
|
||||
XCMInstructionType.DepositReserveAsset -> depositReserveAsset()
|
||||
XCMInstructionType.ReceiveTeleportedAsset -> receiveTeleportedAsset(amount)
|
||||
XCMInstructionType.UNKNOWN -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.reserveAssetDeposited(amount: Balance) =
|
||||
XcmInstruction.ReserveAssetDeposited(
|
||||
assets = MultiAssets(
|
||||
sendingAssetAmountOf(amount)
|
||||
)
|
||||
)
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.receiveTeleportedAsset(amount: Balance) =
|
||||
XcmInstruction.ReceiveTeleportedAsset(
|
||||
assets = MultiAssets(
|
||||
sendingAssetAmountOf(amount)
|
||||
)
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
private fun LegacyCrossChainTransferConfiguration.clearOrigin() = XcmInstruction.ClearOrigin
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.buyExecution(amount: Balance): XcmInstruction.BuyExecution {
|
||||
return XcmInstruction.BuyExecution(
|
||||
fees = sendingAssetAmountOf(amount),
|
||||
weightLimit = WeightLimit.Unlimited
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun LegacyCrossChainTransferConfiguration.depositAsset(chain: Chain): XcmInstruction.DepositAsset {
|
||||
return XcmInstruction.DepositAsset(
|
||||
assets = MultiAssetFilter.Wild.All,
|
||||
beneficiary = chain.emptyBeneficiaryMultiLocation()
|
||||
)
|
||||
}
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.withdrawAsset(amount: Balance): XcmInstruction.WithdrawAsset {
|
||||
return XcmInstruction.WithdrawAsset(
|
||||
assets = MultiAssets(
|
||||
sendingAssetAmountOf(amount)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.depositReserveAsset(): XcmInstruction {
|
||||
return XcmInstruction.DepositReserveAsset(
|
||||
assets = MultiAssetFilter.Wild.All,
|
||||
dest = destinationChainLocation,
|
||||
xcm = XcmMessage(emptyList())
|
||||
)
|
||||
}
|
||||
|
||||
private fun LegacyCrossChainTransferConfiguration.sendingAssetAmountOf(planks: Balance): MultiAsset {
|
||||
return MultiAsset.from(
|
||||
amount = planks,
|
||||
multiLocation = assetLocation,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Chain.emptyBeneficiaryMultiLocation(): RelativeMultiLocation = emptyAccountIdKey().toMultiLocation()
|
||||
|
||||
private suspend fun xcmParachainDeliveryFeeFactor(chainId: ChainId, moduleName: String, paraId: ParaId): BigInteger {
|
||||
return storageDataSource.query(chainId, applyStorageDefault = true) {
|
||||
runtime.metadata.module(moduleName).storage("DeliveryFeeFactor")
|
||||
.query(
|
||||
paraId,
|
||||
binding = ::bindNumber
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun xcmParentDeliveryFeeFactor(chainId: ChainId, moduleName: String): BigInteger {
|
||||
return storageDataSource.query(chainId) {
|
||||
runtime.metadata.module(moduleName).storage("UpwardDeliveryFeeFactor")
|
||||
.query(binding = ::bindNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations
|
||||
|
||||
import io.novafoundation.nova.common.utils.atLeastZero
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.valid
|
||||
import io.novafoundation.nova.common.validation.validationError
|
||||
import io.novafoundation.nova.feature_account_api.data.model.amountByExecutingAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure.NotEnoughFunds.ToStayAboveEdBeforePayingDeliveryFees
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidation
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.isSendingCommissionAsset
|
||||
import io.novasama.substrate_sdk_android.hash.isPositive
|
||||
|
||||
class CannotDropBelowEdWhenPayingDeliveryFeeValidation(
|
||||
private val assetSourceRegistry: AssetSourceRegistry
|
||||
) : AssetTransfersValidation {
|
||||
|
||||
override suspend fun validate(value: AssetTransferPayload): ValidationStatus<AssetTransferValidationFailure> {
|
||||
if (!value.isSendingCommissionAsset) return valid()
|
||||
|
||||
val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(value.transfer.originChainAsset)
|
||||
|
||||
val deliveryFeePart = value.originFee.deliveryFee?.amount.orZero()
|
||||
val paysDeliveryFee = deliveryFeePart.isPositive()
|
||||
|
||||
val networkFeePlanks = value.originFee.submissionFee.amountByExecutingAccount
|
||||
val crossChainFeePlanks = value.crossChainFee?.amount.orZero()
|
||||
|
||||
val sendingAmount = value.transfer.amountInPlanks + crossChainFeePlanks
|
||||
val requiredAmountWhenPayingDeliveryFee = sendingAmount + networkFeePlanks + deliveryFeePart + existentialDeposit
|
||||
|
||||
val balanceCountedTowardsEd = value.originUsedAsset.balanceCountedTowardsEDInPlanks
|
||||
|
||||
return when {
|
||||
!paysDeliveryFee -> valid()
|
||||
|
||||
requiredAmountWhenPayingDeliveryFee <= balanceCountedTowardsEd -> valid()
|
||||
|
||||
else -> {
|
||||
val availableBalance = (balanceCountedTowardsEd - networkFeePlanks - deliveryFeePart - crossChainFeePlanks - existentialDeposit).atLeastZero()
|
||||
|
||||
validationError(
|
||||
ToStayAboveEdBeforePayingDeliveryFees(
|
||||
maxPossibleTransferAmount = availableBalance,
|
||||
chainAsset = value.transfer.originChainAsset
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.cannotDropBelowEdBeforePayingDeliveryFee(
|
||||
assetSourceRegistry: AssetSourceRegistry
|
||||
) = validate(CannotDropBelowEdWhenPayingDeliveryFeeValidation(assetSourceRegistry))
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations
|
||||
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.isTrueOrError
|
||||
import io.novafoundation.nova.feature_account_api.data.model.decimalAmount
|
||||
import io.novafoundation.nova.feature_account_api.data.model.decimalAmountByExecutingAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidation
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeListInUsedAsset
|
||||
|
||||
class CrossChainFeeValidation : AssetTransfersValidation {
|
||||
|
||||
override suspend fun validate(value: AssetTransferPayload): ValidationStatus<AssetTransferValidationFailure> {
|
||||
val originFeeSum = value.originFeeListInUsedAsset.sumOf { it.decimalAmountByExecutingAccount }
|
||||
|
||||
val remainingBalanceAfterTransfer = value.originUsedAsset.transferable - value.transfer.amount - originFeeSum
|
||||
|
||||
val crossChainFee = value.crossChainFee?.decimalAmount.orZero()
|
||||
val remainsEnoughToPayCrossChainFees = remainingBalanceAfterTransfer >= crossChainFee
|
||||
|
||||
return remainsEnoughToPayCrossChainFees isTrueOrError {
|
||||
AssetTransferValidationFailure.NotEnoughFunds.ToPayCrossChainFee(
|
||||
usedAsset = value.transfer.originChainAsset,
|
||||
fee = crossChainFee,
|
||||
remainingBalanceAfterTransfer = remainingBalanceAfterTransfer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AssetTransfersValidationSystemBuilder.canPayCrossChainFee() = validate(CrossChainFeeValidation())
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.model.decimalAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainValidationSystemProvider
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.EnoughTotalToStayAboveEDValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDepositInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInCommissionAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notPhishingRecipient
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientCommissionBalanceToStayAboveED
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress
|
||||
import javax.inject.Inject
|
||||
|
||||
@FeatureScope
|
||||
class RealCrossChainValidationSystemProvider @Inject constructor(
|
||||
private val phishingValidationFactory: PhishingValidationFactory,
|
||||
private val enoughTotalToStayAboveEDValidationFactory: EnoughTotalToStayAboveEDValidationFactory,
|
||||
private val dryRunSucceedsValidationFactory: DryRunSucceedsValidationFactory,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : CrossChainValidationSystemProvider {
|
||||
|
||||
override fun createValidationSystem(): AssetTransfersValidationSystem = ValidationSystem {
|
||||
positiveAmount()
|
||||
recipientIsNotSystemAccount()
|
||||
|
||||
validAddress()
|
||||
notPhishingRecipient(phishingValidationFactory)
|
||||
|
||||
notDeadRecipientInCommissionAsset(assetSourceRegistry)
|
||||
notDeadRecipientInUsedAsset(assetSourceRegistry)
|
||||
|
||||
sufficientCommissionBalanceToStayAboveED(enoughTotalToStayAboveEDValidationFactory)
|
||||
|
||||
sufficientTransferableBalanceToPayOriginFee()
|
||||
canPayCrossChainFee()
|
||||
|
||||
cannotDropBelowEdBeforePayingDeliveryFee(assetSourceRegistry)
|
||||
|
||||
doNotCrossExistentialDepositInUsedAsset(
|
||||
assetSourceRegistry = assetSourceRegistry,
|
||||
extraAmount = { it.transfer.amount + it.crossChainFee?.decimalAmount.orZero() }
|
||||
)
|
||||
|
||||
dryRunSucceedsValidationFactory.dryRunSucceeds()
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.validations
|
||||
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.isTrueOrError
|
||||
import io.novafoundation.nova.common.validation.valid
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidation
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.senderAccountId
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.XcmTransferDryRunOrigin
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@FeatureScope
|
||||
class DryRunSucceedsValidationFactory @Inject constructor(
|
||||
private val crossChainTransfersUseCase: CrossChainTransfersUseCase,
|
||||
) {
|
||||
|
||||
context(AssetTransfersValidationSystemBuilder)
|
||||
fun dryRunSucceeds() {
|
||||
validate(DryRunSucceedsValidation(crossChainTransfersUseCase))
|
||||
}
|
||||
}
|
||||
|
||||
private class DryRunSucceedsValidation(
|
||||
private val crossChainTransfersUseCase: CrossChainTransfersUseCase,
|
||||
) : AssetTransfersValidation {
|
||||
|
||||
override suspend fun validate(value: AssetTransferPayload): ValidationStatus<AssetTransferValidationFailure> {
|
||||
// Skip validation if it is not a cross chain transfer
|
||||
val crossChainFee = value.crossChainFee ?: return valid()
|
||||
|
||||
val dryRunResult = crossChainTransfersUseCase.dryRunTransferIfPossible(
|
||||
transfer = value.transfer,
|
||||
origin = XcmTransferDryRunOrigin.Signed(value.transfer.senderAccountId, crossChainFee.amount),
|
||||
computationalScope = CoroutineScope(coroutineContext)
|
||||
)
|
||||
|
||||
return dryRunResult.isSuccess isTrueOrError {
|
||||
AssetTransferValidationFailure.DryRunFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.etherscan
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_impl.BuildConfig
|
||||
import io.novafoundation.nova.runtime.ext.Ids
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
class EtherscanApiKeys(private val keys: Map<ChainId, String>) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun default(): EtherscanApiKeys {
|
||||
return EtherscanApiKeys(
|
||||
mapOf(
|
||||
Chain.Ids.MOONBEAM to BuildConfig.EHTERSCAN_API_KEY_MOONBEAM,
|
||||
Chain.Ids.MOONRIVER to BuildConfig.EHTERSCAN_API_KEY_MOONRIVER,
|
||||
Chain.Ids.ETHEREUM to BuildConfig.EHTERSCAN_API_KEY_ETHEREUM
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun keyFor(chainId: ChainId): String? = keys[chainId]
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.etherscan
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanAccountTransfer
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanNormalTxResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanResponse
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
interface EtherscanTransactionsApi {
|
||||
|
||||
suspend fun getErc20Transfers(
|
||||
chainId: ChainId,
|
||||
baseUrl: String,
|
||||
contractAddress: String,
|
||||
accountAddress: String,
|
||||
pageNumber: Int,
|
||||
pageSize: Int
|
||||
): EtherscanResponse<List<EtherscanAccountTransfer>>
|
||||
|
||||
suspend fun getNormalTxsHistory(
|
||||
chainId: ChainId,
|
||||
baseUrl: String,
|
||||
accountAddress: String,
|
||||
pageNumber: Int,
|
||||
pageSize: Int
|
||||
): EtherscanResponse<List<EtherscanNormalTxResponse>>
|
||||
}
|
||||
|
||||
class RealEtherscanTransactionsApi(
|
||||
private val retrofitApi: RetrofitEtherscanTransactionsApi,
|
||||
private val apiKeys: EtherscanApiKeys
|
||||
) : EtherscanTransactionsApi {
|
||||
|
||||
override suspend fun getErc20Transfers(
|
||||
chainId: ChainId,
|
||||
baseUrl: String,
|
||||
contractAddress: String,
|
||||
accountAddress: String,
|
||||
pageNumber: Int,
|
||||
pageSize: Int
|
||||
): EtherscanResponse<List<EtherscanAccountTransfer>> {
|
||||
val apiKey = apiKeys.keyFor(chainId)
|
||||
|
||||
return retrofitApi.getErc20Transfers(
|
||||
baseUrl = baseUrl,
|
||||
contractAddress = contractAddress,
|
||||
accountAddress = accountAddress,
|
||||
pageNumber = pageNumber,
|
||||
pageSize = pageSize,
|
||||
apiKey = apiKey
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getNormalTxsHistory(
|
||||
chainId: ChainId,
|
||||
baseUrl: String,
|
||||
accountAddress: String,
|
||||
pageNumber: Int,
|
||||
pageSize: Int
|
||||
): EtherscanResponse<List<EtherscanNormalTxResponse>> {
|
||||
val apiKey = apiKeys.keyFor(chainId)
|
||||
|
||||
return retrofitApi.getNormalTxsHistory(
|
||||
baseUrl = baseUrl,
|
||||
accountAddress = accountAddress,
|
||||
pageNumber = pageNumber,
|
||||
pageSize = pageSize,
|
||||
apiKey = apiKey
|
||||
)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.etherscan
|
||||
|
||||
import io.novafoundation.nova.common.data.network.UserAgent
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanAccountTransfer
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanNormalTxResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model.EtherscanResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface RetrofitEtherscanTransactionsApi {
|
||||
|
||||
@GET
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun getErc20Transfers(
|
||||
@Url baseUrl: String,
|
||||
@Query("contractaddress") contractAddress: String,
|
||||
@Query("address") accountAddress: String,
|
||||
@Query("page") pageNumber: Int,
|
||||
@Query("offset") pageSize: Int,
|
||||
@Query("apikey") apiKey: String?,
|
||||
@Query("module") module: String = "account",
|
||||
@Query("action") action: String = "tokentx",
|
||||
@Query("sort") sort: String = "desc"
|
||||
): EtherscanResponse<List<EtherscanAccountTransfer>>
|
||||
|
||||
@GET
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun getNormalTxsHistory(
|
||||
@Url baseUrl: String,
|
||||
@Query("address") accountAddress: String,
|
||||
@Query("page") pageNumber: Int,
|
||||
@Query("offset") pageSize: Int,
|
||||
@Query("apikey") apiKey: String?,
|
||||
@Query("module") module: String = "account",
|
||||
@Query("action") action: String = "txlist",
|
||||
@Query("sort") sort: String = "desc"
|
||||
): EtherscanResponse<List<EtherscanNormalTxResponse>>
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
class EtherscanAccountTransfer(
|
||||
val timeStamp: Long,
|
||||
val hash: String,
|
||||
val from: String,
|
||||
val to: String,
|
||||
val value: BigInteger,
|
||||
override val gasPrice: BigInteger,
|
||||
override val gasUsed: BigInteger,
|
||||
) : WithEvmFee
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import java.math.BigInteger
|
||||
|
||||
class EtherscanNormalTxResponse(
|
||||
val timeStamp: Long,
|
||||
val hash: String,
|
||||
val from: String,
|
||||
val to: String,
|
||||
val value: BigInteger,
|
||||
val input: String,
|
||||
val functionName: String,
|
||||
@SerializedName("txreceipt_status")val txReceiptStatus: BigInteger,
|
||||
override val gasPrice: BigInteger,
|
||||
override val gasUsed: BigInteger,
|
||||
) : WithEvmFee
|
||||
|
||||
val EtherscanNormalTxResponse.isTransfer
|
||||
get() = input.removeHexPrefix().isEmpty()
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model
|
||||
|
||||
class EtherscanResponse<T>(
|
||||
val status: String,
|
||||
val message: String,
|
||||
val result: T
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.etherscan.model
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
interface WithEvmFee {
|
||||
|
||||
val gasPrice: BigInteger
|
||||
|
||||
val gasUsed: BigInteger
|
||||
}
|
||||
|
||||
val WithEvmFee.feeUsed
|
||||
get() = gasUsed * gasPrice
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.model
|
||||
|
||||
import io.novafoundation.nova.runtime.ext.onChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
fun Chain.Asset.Type.subQueryAssetId(): String {
|
||||
return when (this) {
|
||||
is Chain.Asset.Type.Equilibrium -> id.toString()
|
||||
Chain.Asset.Type.Native -> "native"
|
||||
is Chain.Asset.Type.Orml -> currencyIdScale
|
||||
is Chain.Asset.Type.Statemine -> id.onChainAssetId()
|
||||
else -> error("Unsupported assetId type for SubQuery request: ${this::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.Asset.Type.isSupportedBySubQuery(): Boolean {
|
||||
return when (this) {
|
||||
is Chain.Asset.Type.Equilibrium,
|
||||
Chain.Asset.Type.Native,
|
||||
is Chain.Asset.Type.Orml,
|
||||
is Chain.Asset.Type.Statemine -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
typealias AssetsBySubQueryId = Map<String?, Chain.Asset>
|
||||
|
||||
fun Chain.assetsBySubQueryId(): AssetsBySubQueryId {
|
||||
return assets
|
||||
.filter { it.type.isSupportedBySubQuery() }
|
||||
.associateBy { it.type.subQueryAssetId() }
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.model.request
|
||||
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubQueryFilters
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubqueryExpressions.and
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubqueryExpressions.anyOf
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubqueryExpressions.not
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubqueryExpressions.or
|
||||
import io.novafoundation.nova.common.utils.nullIfEmpty
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.model.subQueryAssetId
|
||||
import io.novafoundation.nova.runtime.ext.StakingTypeGroup
|
||||
import io.novafoundation.nova.runtime.ext.group
|
||||
import io.novafoundation.nova.runtime.ext.isSwapSupported
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset
|
||||
|
||||
private class ModuleRestriction(
|
||||
val moduleName: String,
|
||||
val restrictedCalls: List<String>
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
private fun ModuleRestriction.Companion.ignoreSpecialOperationTypesExtrinsics() = listOf(
|
||||
ModuleRestriction(
|
||||
moduleName = "balances",
|
||||
restrictedCalls = listOf(
|
||||
"transfer",
|
||||
"transferKeepAlive",
|
||||
"transferAllowDeath",
|
||||
"forceTransfer",
|
||||
"transferAll"
|
||||
)
|
||||
),
|
||||
ModuleRestriction(
|
||||
moduleName = "assetConversion",
|
||||
restrictedCalls = listOf(
|
||||
"swapExactTokensForTokens",
|
||||
"swapTokensForExactTokens",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
class SubqueryHistoryRequest(
|
||||
accountAddress: String,
|
||||
legacyAccountAddress: String?,
|
||||
pageSize: Int = 1,
|
||||
cursor: String? = null,
|
||||
filters: Set<TransactionFilter>,
|
||||
asset: Asset,
|
||||
chain: Chain,
|
||||
) : SubQueryFilters {
|
||||
val query = """
|
||||
{
|
||||
query {
|
||||
historyElements(
|
||||
after: ${if (cursor == null) null else "\"$cursor\""},
|
||||
first: $pageSize,
|
||||
orderBy: TIMESTAMP_DESC,
|
||||
filter: {
|
||||
${addressFilter(accountAddress, legacyAccountAddress)}
|
||||
${filters.toQueryFilter(asset, chain)}
|
||||
}
|
||||
) {
|
||||
pageInfo {
|
||||
startCursor,
|
||||
endCursor
|
||||
},
|
||||
nodes {
|
||||
id
|
||||
timestamp
|
||||
extrinsicHash
|
||||
blockNumber
|
||||
address
|
||||
${rewardsResponseSections(asset)}
|
||||
extrinsic
|
||||
${transferResponseSection(asset.type)}
|
||||
${swapResponseSection(chain)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
private fun addressFilter(accountAddress: String, legacyAccountAddress: String?): String {
|
||||
return if (legacyAccountAddress != null) {
|
||||
"""address: { in: ["$accountAddress", "$legacyAccountAddress"] }"""
|
||||
} else {
|
||||
"""address: { equalTo: "$accountAddress" }"""
|
||||
}
|
||||
}
|
||||
|
||||
private fun Set<TransactionFilter>.toQueryFilter(asset: Asset, chain: Chain): String {
|
||||
val additionalFilters = not(isIgnoredExtrinsic())
|
||||
|
||||
val filtersExpressions = mapNotNull { it.filterExpression(asset, chain) }
|
||||
val userFilters = anyOf(filtersExpressions)
|
||||
|
||||
return userFilters and additionalFilters
|
||||
}
|
||||
|
||||
private fun TransactionFilter.filterExpression(asset: Asset, chain: Chain): String? {
|
||||
return when (this) {
|
||||
TransactionFilter.TRANSFER -> transfersFilter(asset.type)
|
||||
TransactionFilter.REWARD -> rewardsFilter(asset)
|
||||
TransactionFilter.EXTRINSIC -> hasExtrinsic()
|
||||
TransactionFilter.SWAP -> swapFilter(chain, asset)
|
||||
}.nullIfEmpty()
|
||||
}
|
||||
|
||||
private fun transferResponseSection(assetType: Asset.Type): String {
|
||||
return when (assetType) {
|
||||
Asset.Type.Native -> "transfer"
|
||||
else -> "assetTransfer"
|
||||
}
|
||||
}
|
||||
|
||||
private fun swapResponseSection(chain: Chain): String {
|
||||
return if (chain.isSwapSupported()) {
|
||||
"swap"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun rewardsResponseSections(asset: Asset): String {
|
||||
return rewardsSections(asset).joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
private fun rewardsSections(asset: Asset): List<String> {
|
||||
return asset.staking.mapNotNull { it.rewardSection() }
|
||||
}
|
||||
|
||||
private fun Asset.StakingType.rewardSection(): String? {
|
||||
return when (group()) {
|
||||
StakingTypeGroup.RELAYCHAIN,
|
||||
StakingTypeGroup.PARACHAIN,
|
||||
StakingTypeGroup.MYTHOS -> "reward"
|
||||
|
||||
StakingTypeGroup.NOMINATION_POOL -> "poolReward"
|
||||
StakingTypeGroup.UNSUPPORTED -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun rewardsFilter(asset: Asset): String {
|
||||
return anyOf(rewardsSections(asset).map { hasType(it) })
|
||||
}
|
||||
|
||||
private fun transfersFilter(assetType: Asset.Type): String {
|
||||
return if (assetType == Asset.Type.Native) {
|
||||
hasType("transfer")
|
||||
} else {
|
||||
transferAssetHasId(assetType.subQueryAssetId())
|
||||
}
|
||||
}
|
||||
|
||||
private fun swapFilter(chain: Chain, asset: Asset): String? {
|
||||
if (!chain.isSwapSupported()) return null
|
||||
|
||||
val subQueryAssetId = asset.type.subQueryAssetId()
|
||||
return or(
|
||||
"swap".containsFilter("assetIdIn", subQueryAssetId),
|
||||
"swap".containsFilter("assetIdOut", subQueryAssetId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasExtrinsic() = hasType("extrinsic")
|
||||
|
||||
private fun isIgnoredExtrinsic(): String {
|
||||
val exists = hasExtrinsic()
|
||||
|
||||
val restrictedModulesList = ModuleRestriction.ignoreSpecialOperationTypesExtrinsics().map {
|
||||
val restrictedCallsExpressions = it.restrictedCalls.map(::callNamed)
|
||||
|
||||
and(
|
||||
moduleNamed(it.moduleName),
|
||||
anyOf(restrictedCallsExpressions)
|
||||
)
|
||||
}
|
||||
|
||||
val hasRestrictedModules = anyOf(restrictedModulesList)
|
||||
|
||||
return and(
|
||||
exists,
|
||||
hasRestrictedModules
|
||||
)
|
||||
}
|
||||
|
||||
private fun callNamed(callName: String) = "extrinsic: {contains: {call: \"$callName\"}}"
|
||||
private fun moduleNamed(moduleName: String) = "extrinsic: {contains: {module: \"$moduleName\"}}"
|
||||
private fun hasType(typeName: String) = "$typeName: {isNull: false}"
|
||||
|
||||
private fun transferAssetHasId(assetId: String?): String {
|
||||
return "assetTransfer".containsFilter("assetId", assetId)
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.model.response
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigInteger
|
||||
|
||||
class SubqueryHistoryElementResponse(val query: Query) {
|
||||
class Query(val historyElements: HistoryElements) {
|
||||
|
||||
class HistoryElements(val nodes: Array<Node>, val pageInfo: PageInfo) {
|
||||
class PageInfo(
|
||||
val startCursor: String,
|
||||
val endCursor: String?
|
||||
)
|
||||
|
||||
class Node(
|
||||
val id: String,
|
||||
val timestamp: Long,
|
||||
val extrinsicHash: String?,
|
||||
val address: String,
|
||||
val reward: Reward?,
|
||||
val blockNumber: Long,
|
||||
val poolReward: PoolReward?,
|
||||
val transfer: Transfer?,
|
||||
val extrinsic: Extrinsic?,
|
||||
val assetTransfer: AssetTransfer?,
|
||||
val swap: Swap?
|
||||
) {
|
||||
class Reward(
|
||||
val era: Int?,
|
||||
val amount: String?,
|
||||
val eventIdx: Int,
|
||||
val isReward: Boolean,
|
||||
val validator: String?,
|
||||
)
|
||||
|
||||
class PoolReward(
|
||||
val amount: BigInteger,
|
||||
val eventIdx: Int,
|
||||
val poolId: Int,
|
||||
val isReward: Boolean,
|
||||
)
|
||||
|
||||
class Transfer(
|
||||
val amount: BigInteger,
|
||||
val to: String,
|
||||
val from: String,
|
||||
val fee: BigInteger,
|
||||
val success: Boolean
|
||||
)
|
||||
|
||||
class Extrinsic(
|
||||
val module: String,
|
||||
val call: String,
|
||||
val fee: BigInteger,
|
||||
val success: Boolean
|
||||
)
|
||||
|
||||
class AssetTransfer(
|
||||
val assetId: String,
|
||||
val amount: BigInteger,
|
||||
val to: String,
|
||||
val from: String,
|
||||
val fee: BigInteger,
|
||||
val success: Boolean
|
||||
)
|
||||
|
||||
class Swap(
|
||||
val assetIdIn: String?,
|
||||
val amountIn: Balance,
|
||||
val assetIdOut: String?,
|
||||
val amountOut: Balance,
|
||||
val sender: String,
|
||||
val receiver: String,
|
||||
val fee: Balance,
|
||||
val assetIdFee: String?,
|
||||
val success: Boolean?
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.phishing
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface PhishingApi {
|
||||
|
||||
@GET("https://polkadot.js.org/phishing/address.json")
|
||||
suspend fun getPhishingAddresses(): Map<String, List<String>>
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.subquery
|
||||
|
||||
import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.model.request.SubqueryHistoryRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.model.response.SubqueryHistoryElementResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface SubQueryOperationsApi {
|
||||
|
||||
@POST
|
||||
suspend fun getOperationsHistory(
|
||||
@Url url: String,
|
||||
@Body body: SubqueryHistoryRequest
|
||||
): SubQueryResponse<SubqueryHistoryElementResponse>
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.utils.binarySearchFloor
|
||||
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.data.source.CoinPriceRemoteDataSource
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.CoinPriceRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.PricePeriod
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.HistoricalCoinRate
|
||||
import kotlin.time.Duration
|
||||
|
||||
class CoinPriceRepositoryImpl(
|
||||
private val cacheCoinPriceDataSource: CoinPriceLocalDataSource,
|
||||
private val remoteCoinPriceDataSource: CoinPriceRemoteDataSource
|
||||
) : CoinPriceRepository {
|
||||
|
||||
override suspend fun getCoinPriceAtTime(priceId: String, currency: Currency, timestamp: Duration): HistoricalCoinRate? {
|
||||
val timestampInSeconds = timestamp.inWholeSeconds
|
||||
var coinRate = cacheCoinPriceDataSource.getFloorCoinPriceAtTime(priceId, currency, timestampInSeconds)
|
||||
val hasCeilingItem = cacheCoinPriceDataSource.hasCeilingCoinPriceAtTime(priceId, currency, timestampInSeconds)
|
||||
if (coinRate == null && !hasCeilingItem) {
|
||||
val coinRateForAllTime = getLastHistoryForPeriod(priceId, currency, PricePeriod.MAX)
|
||||
val index = coinRateForAllTime.binarySearchFloor { it.timestamp.compareTo(timestampInSeconds) }
|
||||
coinRate = coinRateForAllTime.getOrNull(index)
|
||||
|
||||
// If nearest coin rate timestamp is bigger than target timestamp it means that coingecko doesn't have data before coin rate timestamp
|
||||
// so in this case we should return null
|
||||
if (coinRate != null && coinRate.timestamp > timestampInSeconds) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return coinRate
|
||||
}
|
||||
|
||||
override suspend fun getLastHistoryForPeriod(priceId: String, currency: Currency, range: PricePeriod): List<HistoricalCoinRate> {
|
||||
return remoteCoinPriceDataSource.getLastCoinPriceRange(priceId, currency, range)
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.repository
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.balances
|
||||
import io.novafoundation.nova.common.utils.getOrNull
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.core_db.dao.HoldsDao
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceHoldsRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceHold
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.mapBalanceHoldFromLocal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chainsById
|
||||
import io.novafoundation.nova.runtime.multiNetwork.getRuntime
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Struct
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.Vec
|
||||
import io.novasama.substrate_sdk_android.runtime.metadata.storageOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RealBalanceHoldsRepository(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val holdsDao: HoldsDao,
|
||||
) : BalanceHoldsRepository {
|
||||
|
||||
override suspend fun chainHasHoldId(chainId: ChainId, holdId: BalanceHold.HoldId): Boolean {
|
||||
return runCatching {
|
||||
val holdReasonType = getHoldReasonType(chainId) ?: return false
|
||||
holdReasonType.hasHoldId(holdId).also {
|
||||
Log.d(LOG_TAG, "chainHasHoldId for $chainId: $it")
|
||||
}
|
||||
}
|
||||
.onFailure { Log.w(LOG_TAG, "Failed to get hold reason type", it) }
|
||||
.getOrDefault(false)
|
||||
}
|
||||
|
||||
override suspend fun observeBalanceHolds(metaInt: Long, chainAsset: Chain.Asset): Flow<List<BalanceHold>> {
|
||||
return holdsDao.observeBalanceHolds(metaInt, chainAsset.chainId, chainAsset.id).mapList { hold ->
|
||||
mapBalanceHoldFromLocal(chainAsset, hold)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeHoldsForMetaAccount(metaInt: Long): Flow<List<BalanceHold>> {
|
||||
return holdsDao.observeHoldsForMetaAccount(metaInt).map { holds ->
|
||||
val chainsById = chainRegistry.chainsById()
|
||||
holds.mapNotNull { holdLocal ->
|
||||
val asset = chainsById[holdLocal.chainId]?.assetsById?.get(holdLocal.assetId) ?: return@mapNotNull null
|
||||
|
||||
mapBalanceHoldFromLocal(asset, holdLocal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DictEnum.hasHoldId(holdId: BalanceHold.HoldId): Boolean {
|
||||
val moduleReasons = getOrNull(holdId.module) as? DictEnum ?: return false
|
||||
return moduleReasons[holdId.reason] != null
|
||||
}
|
||||
|
||||
private suspend fun getHoldReasonType(chainId: ChainId): DictEnum? {
|
||||
val runtime = chainRegistry.getRuntime(chainId)
|
||||
|
||||
val storage = runtime.metadata.balances().storageOrNull("Holds") ?: return null
|
||||
val storageReturnType = storage.type.value as Vec
|
||||
|
||||
return storageReturnType
|
||||
.innerType<Struct>()!!
|
||||
.get<DictEnum>("id")
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.core_db.dao.LockDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLock
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.BalanceLockId
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.mapBalanceLockFromLocal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RealBalanceLocksRepository(
|
||||
// TODO refactoring - repository should not depend on other repository. MetaId should be passed to repository arguments
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val lockDao: LockDao
|
||||
) : BalanceLocksRepository {
|
||||
|
||||
override fun observeBalanceLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow<List<BalanceLock>> {
|
||||
return lockDao.observeBalanceLocks(metaId, chain.id, chainAsset.id)
|
||||
.mapList { lock -> mapBalanceLockFromLocal(chainAsset, lock) }
|
||||
}
|
||||
|
||||
override suspend fun getBalanceLocks(metaId: Long, chainAsset: Chain.Asset): List<BalanceLock> {
|
||||
return lockDao.getBalanceLocks(metaId, chainAsset.chainId, chainAsset.id)
|
||||
.map { lock -> mapBalanceLockFromLocal(chainAsset, lock) }
|
||||
}
|
||||
|
||||
override suspend fun getBiggestLock(chain: Chain, chainAsset: Chain.Asset): BalanceLock? {
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
|
||||
return lockDao.getBiggestBalanceLock(metaAccount.id, chain.id, chainAsset.id)?.let {
|
||||
mapBalanceLockFromLocal(chainAsset, it)
|
||||
}
|
||||
}
|
||||
override suspend fun observeBalanceLock(chainAsset: Chain.Asset, lockId: BalanceLockId): Flow<BalanceLock?> {
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
|
||||
return lockDao.observeBalanceLock(metaAccount.id, chainAsset.chainId, chainAsset.id, lockId.value).map { lockLocal ->
|
||||
lockLocal?.let { mapBalanceLockFromLocal(chainAsset, it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeLocksForMetaAccount(metaAccount: MetaAccount): Flow<List<BalanceLock>> {
|
||||
return combine(lockDao.observeLocksForMetaAccount(metaAccount.id), chainRegistry.chainsById) { locks, chains ->
|
||||
locks.map {
|
||||
val asset = chains.getValue(it.chainId)
|
||||
.assetsById.getValue(it.assetId)
|
||||
mapBalanceLockFromLocal(asset, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.repository
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.core_db.dao.ChainAssetDao
|
||||
import io.novafoundation.nova.core_db.dao.SetAssetEnabledParams
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainAssetLocalToAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.mapChainAssetToLocal
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
|
||||
class RealChainAssetRepository(
|
||||
private val chainAssetDao: ChainAssetDao,
|
||||
private val gson: Gson
|
||||
) : ChainAssetRepository {
|
||||
|
||||
override suspend fun setAssetsEnabled(enabled: Boolean, assetIds: List<FullChainAssetId>) {
|
||||
val updateParams = assetIds.map { SetAssetEnabledParams(enabled, it.chainId, it.assetId) }
|
||||
|
||||
chainAssetDao.setAssetsEnabled(updateParams)
|
||||
}
|
||||
|
||||
override suspend fun insertCustomAsset(chainAsset: Chain.Asset) {
|
||||
val localAsset = mapChainAssetToLocal(chainAsset, gson)
|
||||
chainAssetDao.insertAsset(localAsset)
|
||||
}
|
||||
|
||||
override suspend fun getEnabledAssets(): List<Chain.Asset> {
|
||||
return chainAssetDao.getEnabledAssets().map { mapChainAssetLocalToAsset(it, gson) }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user