diff --git a/.github/workflows/manual_firebase_distribution.yml b/.github/workflows/manual_firebase_distribution.yml index e53734f4b9..0da2f31778 100644 --- a/.github/workflows/manual_firebase_distribution.yml +++ b/.github/workflows/manual_firebase_distribution.yml @@ -36,7 +36,7 @@ jobs: uses: ./.github/workflows/upload-to-firebase with: appId: ${{ secrets.ANDROID_DEVELOP_FIREBASE_APP_ID }} - firebase-token: ${{ secrets.ANDROID_DEV_FIREBASE_TOKEN }} + firebase-token: ${{ secrets.CREDENTIAL_FILE_CONTENT }} releaseNotes: ${{ github.event.head_commit.message }} test-groups: ${{ github.event.inputs.firebase_group }} upload-file: app/develop/app-develop.apk diff --git a/.github/workflows/push_develop.yml b/.github/workflows/push_develop.yml index dd6bdaf41d..dc1e55f3ed 100644 --- a/.github/workflows/push_develop.yml +++ b/.github/workflows/push_develop.yml @@ -29,7 +29,7 @@ jobs: uses: ./.github/workflows/upload-to-firebase with: appId: ${{ secrets.ANDROID_DEVELOP_FIREBASE_APP_ID }} - firebase-token: ${{ secrets.ANDROID_DEV_FIREBASE_TOKEN }} + firebase-token: ${{ secrets.CREDENTIAL_FILE_CONTENT }} releaseNotes: ${{ github.event.head_commit.message }} test-groups: dev-team upload-file: app/develop/app-develop.apk diff --git a/.github/workflows/upload-to-firebase/action.yml b/.github/workflows/upload-to-firebase/action.yml index f8fd0b2ead..ef654256a7 100644 --- a/.github/workflows/upload-to-firebase/action.yml +++ b/.github/workflows/upload-to-firebase/action.yml @@ -24,10 +24,10 @@ runs: - name: Upload artifact to Firebase App Distribution id: upload continue-on-error: true - uses: wzieba/Firebase-Distribution-Github-Action@v1 + uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 with: appId: ${{ inputs.appId }} - token: ${{ inputs.firebase-token }} + serviceCredentialsFileContent: ${{ inputs.firebase-token }} releaseNotes: ${{ inputs.releaseNotes }} groups: ${{ inputs.test-groups }} file: ${{ inputs.upload-file }} @@ -40,10 +40,10 @@ runs: - name: Retry upload artifacts if: steps.upload.outcome=='failure' - uses: wzieba/Firebase-Distribution-Github-Action@v1 + uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 with: appId: ${{ inputs.appId }} - token: ${{ inputs.firebase-token }} + serviceCredentialsFileContent: ${{ inputs.firebase-token }} releaseNotes: ${{ inputs.releaseNotes }} groups: ${{ inputs.test-groups }} file: ${{ inputs.upload-file }} diff --git a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt index 978370d7b8..df3c39ad02 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/SwapServiceIntegrationTest.kt @@ -4,6 +4,7 @@ import android.util.Log import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.feature_swap_api.di.SwapFeatureApi +import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit @@ -88,7 +89,8 @@ class SwapServiceIntegrationTest : BaseIntegrationTest() { expectedAmountOut = Balance.ZERO ), customFeeAsset = null, - nativeAsset = arbitraryAssetUseCase.assetFlow(westmint.commissionAsset).first() + nativeAsset = arbitraryAssetUseCase.assetFlow(westmint.commissionAsset).first(), + path = QuotePath(emptyList()) ) val fee = swapService.estimateFee(swapArgs) @@ -111,7 +113,8 @@ class SwapServiceIntegrationTest : BaseIntegrationTest() { expectedAmountOut = Balance.ZERO ), customFeeAsset = siri, - nativeAsset = arbitraryAssetUseCase.assetFlow(westmint.commissionAsset).first() + nativeAsset = arbitraryAssetUseCase.assetFlow(westmint.commissionAsset).first(), + path = QuotePath(emptyList()) ) val fee = swapService.estimateFee(swapArgs) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 71de0f9b2c..639c1427de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + @@ -76,6 +77,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt index ba2c21d6a6..2f2827d2f1 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/RootViewModel.kt @@ -15,7 +15,6 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.sequrity.SafeModeService import io.novafoundation.nova.common.utils.coroutines.RootScope import io.novafoundation.nova.common.utils.inBackground -import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.common.utils.sequrity.BackgroundAccessObserver import io.novafoundation.nova.core.updater.Updater import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsInteractor @@ -44,7 +43,6 @@ class RootViewModel( private val walletConnectService: WalletConnectService, private val walletConnectSessionsUseCase: WalletConnectSessionsUseCase, private val deepLinkHandler: DeepLinkHandler, - private val automaticInteractionGate: AutomaticInteractionGate, private val rootScope: RootScope, private val compoundRequestBusHandler: CompoundRequestBusHandler ) : BaseViewModel(), NetworkStateUi by networkStateMixin { diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/RootDeepLinkHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/RootDeepLinkHandler.kt index cfac9aad8e..cf7afb27e8 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/RootDeepLinkHandler.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/RootDeepLinkHandler.kt @@ -17,7 +17,11 @@ class RootDeepLinkHandler( } override suspend fun handleDeepLink(data: Uri) { - nestedHandlers.find { it.matches(data) } - ?.handleDeepLink(data) + runCatching { + nestedHandlers.find { + runCatching { it.matches(data) }.getOrDefault(false) + } + ?.handleDeepLink(data) + } } } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ReferendumDeepLinkHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ReferendumDeepLinkHandler.kt index 869b02e206..c8664115e9 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ReferendumDeepLinkHandler.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/ReferendumDeepLinkHandler.kt @@ -6,7 +6,6 @@ import io.novafoundation.nova.app.root.presentation.deepLinks.DeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.common.DeepLinkHandlingException.ReferendumHandlingException import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed -import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_governance_api.data.MutableGovernanceState import io.novafoundation.nova.feature_governance_impl.presentation.GovernanceRouter import io.novafoundation.nova.feature_governance_impl.presentation.referenda.details.ReferendumDetailsPayload @@ -14,8 +13,8 @@ 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.getChainOrNull -import java.math.BigInteger import kotlinx.coroutines.flow.MutableSharedFlow +import java.math.BigInteger private const val GOV_DEEP_LINK_PREFIX = "/open/gov" @@ -23,7 +22,6 @@ class ReferendumDeepLinkHandler( private val governanceRouter: GovernanceRouter, private val chainRegistry: ChainRegistry, private val mutableGovernanceState: MutableGovernanceState, - private val accountRepository: AccountRepository, private val automaticInteractionGate: AutomaticInteractionGate ) : DeepLinkHandler { @@ -57,7 +55,7 @@ class ReferendumDeepLinkHandler( ?.toBigIntegerOrNull() } - private suspend fun Uri.getGovernanceType(chain: Chain): Chain.Governance { + private fun Uri.getGovernanceType(chain: Chain): Chain.Governance { val supportedGov = chain.governance val govType = getQueryParameter("type") ?.toIntOrNull() @@ -74,8 +72,4 @@ class ReferendumDeepLinkHandler( else -> throw ReferendumHandlingException.GovernanceTypeIsNotSupported } } - - private fun Chain.Governance.takeIfContainedIn(list: List): Chain.Governance? { - return list.firstOrNull { it == this } - } } diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/walletConnect/WalletConnectPairDeeplinkHandler.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/walletConnect/WalletConnectPairDeeplinkHandler.kt new file mode 100644 index 0000000000..9c88b89550 --- /dev/null +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/deepLinks/handlers/walletConnect/WalletConnectPairDeeplinkHandler.kt @@ -0,0 +1,31 @@ +package io.novafoundation.nova.app.root.presentation.deepLinks.handlers.walletConnect + +import android.net.Uri +import io.novafoundation.nova.app.root.presentation.deepLinks.CallbackEvent +import io.novafoundation.nova.app.root.presentation.deepLinks.DeepLinkHandler +import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate +import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed +import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +class WalletConnectPairDeeplinkHandler( + private val walletConnectService: WalletConnectService, + private val automaticInteractionGate: AutomaticInteractionGate +) : DeepLinkHandler { + + override val callbackFlow: Flow = emptyFlow() + + override suspend fun matches(data: Uri): Boolean { + val newLinkMatch = data.scheme == "novawallet" && data.host == "wc" + // Older version of wc send both pair and sign requests through `wc:` deeplink so we additionaly check for `symKey` which is only present in pairing url + val oldLinkMatch = data.scheme == "wc" && "symKey" in data.toString() + + return newLinkMatch || oldLinkMatch + } + + override suspend fun handleDeepLink(data: Uri) { + automaticInteractionGate.awaitInteractionAllowed() + walletConnectService.pair(data.toString()) + } +} diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt index 00158d5005..d8fd8bb5a3 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/DeepLinkModule.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.DAppDeepL import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.ImportMnemonicDeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.ReferendumDeepLinkHandler import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.StakingDashboardDeepLinkHandler +import io.novafoundation.nova.app.root.presentation.deepLinks.handlers.walletConnect.WalletConnectPairDeeplinkHandler import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults @@ -21,6 +22,7 @@ import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepos import io.novafoundation.nova.feature_dapp_impl.DAppRouter import io.novafoundation.nova.feature_governance_api.data.MutableGovernanceState import io.novafoundation.nova.feature_governance_impl.presentation.GovernanceRouter +import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @Module @@ -71,14 +73,12 @@ class DeepLinkModule { governanceRouter: GovernanceRouter, chainRegistry: ChainRegistry, mutableGovernanceState: MutableGovernanceState, - accountRepository: AccountRepository, automaticInteractionGate: AutomaticInteractionGate ): DeepLinkHandler { return ReferendumDeepLinkHandler( governanceRouter, chainRegistry, mutableGovernanceState, - accountRepository, automaticInteractionGate ) } @@ -92,6 +92,15 @@ class DeepLinkModule { return BuyCallbackDeepLinkHandler(interactor, resourceManager) } + @Provides + @IntoSet + fun provideWalletConnectPairDeepLinkHandler( + walletConnectService: WalletConnectService, + automaticInteractionGate: AutomaticInteractionGate + ): DeepLinkHandler { + return WalletConnectPairDeeplinkHandler(walletConnectService, automaticInteractionGate) + } + @Provides fun provideRootDeepLinkHandler( deepLinkHandlers: Set<@JvmSuppressWildcards DeepLinkHandler> diff --git a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt index a86dc36655..15f78f2c64 100644 --- a/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt +++ b/app/src/main/java/io/novafoundation/nova/app/root/presentation/di/RootActivityModule.kt @@ -16,7 +16,6 @@ import io.novafoundation.nova.common.di.viewmodel.ViewModelModule import io.novafoundation.nova.common.mixin.api.NetworkStateMixin import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.sequrity.SafeModeService -import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate import io.novafoundation.nova.common.utils.coroutines.RootScope import io.novafoundation.nova.common.utils.sequrity.BackgroundAccessObserver import io.novafoundation.nova.feature_crowdloan_api.domain.contributions.ContributionsInteractor @@ -52,7 +51,6 @@ class RootActivityModule { walletConnectService: WalletConnectService, walletConnectSessionsUseCase: WalletConnectSessionsUseCase, deepLinkHandler: RootDeepLinkHandler, - automaticInteractionGate: AutomaticInteractionGate, rootScope: RootScope, compoundRequestBusHandler: CompoundRequestBusHandler ): ViewModel { @@ -70,7 +68,6 @@ class RootActivityModule { walletConnectService, walletConnectSessionsUseCase, deepLinkHandler, - automaticInteractionGate, rootScope, compoundRequestBusHandler ) diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 41cab41e5b..140943c881 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -867,6 +867,7 @@ android:label="SelectWalletFragment" /> diff --git a/build.gradle b/build.gradle index 01b7b74e35..a4f1278c1e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { // App version - versionName = '7.8.0' - versionCode = 115 + versionName = '7.9.0' + versionCode = 117 applicationId = "io.novafoundation.nova" releaseApplicationSuffix = "market" @@ -51,7 +51,7 @@ buildscript { web3jVersion = '4.9.5' - fearlessLibVersion = '1.11.1' + fearlessLibVersion = '1.11.2' gifVersion = '1.2.19' @@ -83,8 +83,8 @@ buildscript { markwonVersion = '4.6.2' - walletConnectCoreVersion = "1.13.0" - walletConnectWalletVersion = "1.6.0" + walletConnectCoreVersion = "1.27.2" + walletConnectWalletVersion = "1.20.2" withoutTransitiveAndroidX = { exclude group: "androidx.appcompat", module: "appcompat" @@ -208,6 +208,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath 'com.google.gms:google-services:4.3.14' + classpath 'org.mozilla.rust-android-gradle:plugin:0.9.3' classpath "com.google.firebase:firebase-appdistribution-gradle:$firebaseAppDistrVersion" classpath "com.github.triplet.gradle:play-publisher:$playPublisherVersion" } diff --git a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/AccountInfo.kt b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/AccountInfo.kt index cb5b0d9665..d1f610e775 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/AccountInfo.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/AccountInfo.kt @@ -26,6 +26,8 @@ open class AccountBalance( } } +fun AccountBalance?.orEmpty(): AccountBalance = this ?: AccountBalance.empty() + class AccountData( free: BigInteger, reserved: BigInteger, @@ -113,7 +115,13 @@ fun bindNonce(dynamicInstance: Any?): BigInteger { fun bindAccountInfo(scale: String, runtime: RuntimeSnapshot): AccountInfo { val type = runtime.metadata.system().storage("Account").returnType() - val dynamicInstance = type.fromHexOrNull(runtime, scale).cast() + val dynamicInstance = type.fromHexOrNull(runtime, scale) + + return bindAccountInfo(dynamicInstance) +} + +fun bindAccountInfo(decoded: Any?): AccountInfo { + val dynamicInstance = decoded.cast() return AccountInfo( consumers = dynamicInstance.getTyped("consumers").orZero(), @@ -122,3 +130,17 @@ fun bindAccountInfo(scale: String, runtime: RuntimeSnapshot): AccountInfo { data = bindAccountData(dynamicInstance.getTyped("data")) ) } + +fun bindOrmlAccountBalanceOrEmpty(decoded: Any?): AccountBalance { + return decoded?.let { bindOrmlAccountData(decoded) } ?: AccountBalance.empty() +} + +fun bindOrmlAccountData(decoded: Any?): AccountBalance { + val dynamicInstance = decoded.cast() + + return AccountBalance( + free = bindNumber(dynamicInstance["free"]), + reserved = bindNumber(dynamicInstance["reserved"]), + frozen = bindNumber(dynamicInstance["frozen"]), + ) +} diff --git a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Floats.kt b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Floats.kt index bc08b05c3a..3f8107fe68 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Floats.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/Floats.kt @@ -7,15 +7,16 @@ import io.novafoundation.nova.common.utils.Perbill as PerbillTyped typealias Perbill = BigDecimal typealias FixedI64 = BigDecimal -private const val FLOAT_MANTISSA_SIZE = 9 +const val PERBILL_MANTISSA_SIZE = 9 +const val PERMILL_MANTISSA_SIZE = 6 @HelperBinding -fun bindPerbillNumber(value: BigInteger): Perbill { - return value.toBigDecimal(scale = FLOAT_MANTISSA_SIZE) +fun bindPerbillNumber(value: BigInteger, mantissa: Int = PERBILL_MANTISSA_SIZE): Perbill { + return value.toBigDecimal(scale = mantissa) } -fun bindPerbill(dynamic: Any?): Perbill { - return bindPerbillNumber(dynamic.cast()) +fun bindPerbill(dynamic: Any?, mantissa: Int = PERBILL_MANTISSA_SIZE): Perbill { + return bindPerbillNumber(dynamic.cast(), mantissa) } fun bindFixedI64Number(value: BigInteger): FixedI64 { @@ -26,6 +27,10 @@ fun bindFixedI64(dynamic: Any?): FixedI64 { return bindPerbill(dynamic) } -fun bindPerbillTyped(dynamic: Any?): PerbillTyped { - return PerbillTyped(bindPerbill(dynamic).toDouble()) +fun bindPerbillTyped(dynamic: Any?, mantissa: Int = PERBILL_MANTISSA_SIZE): PerbillTyped { + return PerbillTyped(bindPerbill(dynamic, mantissa).toDouble()) +} + +fun bindPermill(dynamic: Any?): PerbillTyped { + return bindPerbillTyped(dynamic, mantissa = PERMILL_MANTISSA_SIZE) } diff --git a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/GenericCall.kt b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/GenericCall.kt index 146a0fe1a6..5c43b8cd03 100644 --- a/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/GenericCall.kt +++ b/common/src/main/java/io/novafoundation/nova/common/data/network/runtime/binding/GenericCall.kt @@ -5,3 +5,7 @@ import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Generic fun bindGenericCall(decoded: Any?): GenericCall.Instance { return decoded.cast() } + +fun bindGenericCallList(decoded: Any?): List { + return bindList(decoded, ::bindGenericCall) +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/CollectionDiffer.kt b/common/src/main/java/io/novafoundation/nova/common/utils/CollectionDiffer.kt index 1bc9957f5b..b1cdf00203 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/CollectionDiffer.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/CollectionDiffer.kt @@ -5,8 +5,10 @@ interface Identifiable { val identifier: String } -fun List.findById(other: Identifiable?): T? = find { it.identifier == other?.identifier } -fun List.findById(id: String): T? = find { it.identifier == id } +fun Iterable.findById(other: Identifiable?): T? = find { it.identifier == other?.identifier } +fun Iterable.findById(id: String): T? = find { it.identifier == id } + +fun Iterable.firstById(id: String): T = first { it.identifier == id } fun CollectionDiffer.Diff<*>.hasDifference() = newOrUpdated.isNotEmpty() || removed.isNotEmpty() diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt index 3b5563dc96..11b25b347b 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/FearlessLibExt.kt @@ -23,6 +23,7 @@ import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.RuntimeType import jp.co.soramitsu.fearless_utils.runtime.definitions.types.bytesOrNull import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromByteArrayOrNull import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.DefaultSignedExtensions import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation @@ -48,6 +49,7 @@ import jp.co.soramitsu.fearless_utils.runtime.metadata.storageOrNull import jp.co.soramitsu.fearless_utils.scale.EncodableStruct import jp.co.soramitsu.fearless_utils.scale.Schema import jp.co.soramitsu.fearless_utils.scale.dataType.DataType +import jp.co.soramitsu.fearless_utils.scale.utils.toUnsignedBytes import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.addressPrefix import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId @@ -95,6 +97,10 @@ val Short.bigEndianBytes fun ByteArray.toBigEndianShort(): Short = ByteBuffer.wrap(this).order(ByteOrder.BIG_ENDIAN).short fun ByteArray.toBigEndianU16(): UShort = toBigEndianShort().toUShort() +fun BigInteger.toUnsignedLittleEndian(): ByteArray { + return toUnsignedBytes().reversedArray() +} + fun ByteArray.toBigEndianU32(): UInt = ByteBuffer.wrap(this).order(ByteOrder.BIG_ENDIAN).int.toUInt() fun DataType.fromHex(hex: String): T { @@ -166,6 +172,10 @@ fun Module.optionalNumberConstant(name: String, runtimeSnapshot: RuntimeSnapshot fun Constant.asNumber(runtimeSnapshot: RuntimeSnapshot) = bindNumberConstant(this, runtimeSnapshot) +fun Constant.decoded(runtimeSnapshot: RuntimeSnapshot): Any? { + return type?.fromByteArrayOrNull(runtimeSnapshot, value) +} + fun Module.constantOrNull(name: String) = constants[name] fun RuntimeMetadata.staking() = module(Modules.STAKING) @@ -181,6 +191,8 @@ fun RuntimeMetadata.eqBalances() = module(Modules.EQ_BALANCES) fun RuntimeMetadata.tokens() = module(Modules.TOKENS) +fun RuntimeMetadata.assetRegistry() = module(Modules.ASSET_REGISTRY) + fun RuntimeMetadata.currencies() = module(Modules.CURRENCIES) fun RuntimeMetadata.currenciesOrNull() = moduleOrNull(Modules.CURRENCIES) fun RuntimeMetadata.crowdloan() = module(Modules.CROWDLOAN) @@ -233,12 +245,30 @@ fun RuntimeMetadata.nominationPoolsOrNull() = moduleOrNull(Modules.NOMINATION_PO fun RuntimeMetadata.assetConversionOrNull() = moduleOrNull(Modules.ASSET_CONVERSION) +fun RuntimeMetadata.omnipoolOrNull() = moduleOrNull(Modules.OMNIPOOL) + +fun RuntimeMetadata.omnipool() = module(Modules.OMNIPOOL) + +fun RuntimeMetadata.stableSwapOrNull() = moduleOrNull(Modules.STABLE_SWAP) + +fun RuntimeMetadata.stableSwap() = module(Modules.STABLE_SWAP) + +fun RuntimeMetadata.dynamicFeesOrNull() = moduleOrNull(Modules.DYNAMIC_FEES) + +fun RuntimeMetadata.dynamicFees() = module(Modules.DYNAMIC_FEES) + +fun RuntimeMetadata.multiTransactionPayment() = module(Modules.MULTI_TRANSACTION_PAYMENT) + +fun RuntimeMetadata.referralsOrNull() = moduleOrNull(Modules.REFERRALS) + fun RuntimeMetadata.assetConversion() = module(Modules.ASSET_CONVERSION) fun RuntimeMetadata.proxyOrNull() = moduleOrNull(Modules.PROXY) fun RuntimeMetadata.proxy() = module(Modules.PROXY) +fun RuntimeMetadata.utility() = module(Modules.UTILITY) + fun RuntimeMetadata.firstExistingModuleName(vararg options: String): String { return options.first(::hasModule) } @@ -396,4 +426,18 @@ object Modules { const val MULTISIG = "Multisig" const val REGISTRAR = "Registrar" const val FAST_UNSTAKE = "FastUnstake" + + const val OMNIPOOL = "Omnipool" + + const val DYNAMIC_FEES = "DynamicFees" + + const val MULTI_TRANSACTION_PAYMENT = "MultiTransactionPayment" + + const val REFERRALS = "Referrals" + + const val ROUTER = "Router" + + const val STABLE_SWAP = "Stableswap" + + const val ASSET_REGISTRY = "AssetRegistry" } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt index bbb69e4d84..0202c1244b 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/KotlinExt.kt @@ -5,7 +5,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.runningFold import org.web3j.utils.Numeric import java.io.ByteArrayOutputStream import java.io.InputStream @@ -40,6 +45,14 @@ inline fun Result.flatMap(transform: (T) -> Result): Result { ) } +fun List>>.toMultiSubscription(expectedSize: Int): Flow> { + return mergeIfMultiple() + .runningFold(emptyMap()) { accumulator, tokenIdWithBalance -> + accumulator + tokenIdWithBalance + } + .filter { it.size == expectedSize } +} + inline fun > enumValueOfOrNull(raw: String): E? = runCatching { enumValueOf(raw) }.getOrNull() inline fun List.associateByMultiple(keysExtractor: (V) -> Iterable): Map { @@ -56,6 +69,12 @@ inline fun List.associateByMultiple(keysExtractor: (V) -> Iterable) return destination } +suspend fun Iterable.mapAsync(operation: suspend (T) -> R): List { + return coroutineScope { + map { async { operation(it) } } + }.awaitAll() +} + fun ByteArray.startsWith(prefix: ByteArray): Boolean { if (prefix.size > size) return false diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/MultiMap.kt b/common/src/main/java/io/novafoundation/nova/common/utils/MultiMap.kt index a2ab60c82c..323a40c597 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/MultiMap.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/MultiMap.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.common.utils typealias MutableMultiMap = MutableMap> typealias MultiMap = Map> +typealias MultiMapList = Map> fun mutableMultiMapOf(): MutableMultiMap = mutableMapOf() diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt index eec323a9a7..5ea66904ae 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/Percent.kt @@ -11,6 +11,11 @@ import java.math.BigDecimal @JvmInline value class Perbill(val value: Double) : Comparable { + companion object { + + fun zero() = Perbill(0.0) + } + override fun compareTo(other: Perbill): Int { return value.compareTo(other.value) } diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt index 38a7367efd..6860d72f30 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/formatting/NumberFormatters.kt @@ -227,6 +227,23 @@ fun currencyFormatter() = CompoundNumberFormatter( ) ) +fun simpleCurrencyFormatter() = CompoundNumberFormatter( + abbreviations = listOf( + NumberAbbreviation( + threshold = BigDecimal.ZERO, + divisor = BigDecimal.ONE, + suffix = "", + formatter = DynamicPrecisionFormatter(minScale = ABBREVIATED_SCALE, minPrecision = PRICE_MIN_PRECISION) + ), + NumberAbbreviation( + threshold = BigDecimal.ONE, + divisor = BigDecimal.ONE, + suffix = "", + formatter = defaultAbbreviationFormatter + ) + ) +) + fun baseDurationFormatter( context: Context, dayDurationFormatter: BoundedDurationFormatter = DayAndHourDurationFormatter( diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt new file mode 100644 index 0000000000..3ffdf28764 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/Graph.kt @@ -0,0 +1,156 @@ +package io.novafoundation.nova.common.utils.graph + +import io.novafoundation.nova.common.utils.MultiMap +import io.novafoundation.nova.common.utils.MultiMapList +import java.util.PriorityQueue + +interface Edge { + + val from: N + + val to: N +} + +class Graph>( + val adjacencyList: Map> +) { + + companion object; +} + +fun > Graph.Companion.create(vararg multiMaps: MultiMapList): Graph { + return create(multiMaps.toList()) +} + +fun > Graph.Companion.create(vararg adjacencyPairs: Pair>): Graph { + return create(adjacencyPairs.toMap()) +} + +fun > Graph.Companion.create(multiMaps: List>): Graph { + return GraphBuilder().apply { + multiMaps.forEach(::addEdges) + }.build() +} + +typealias ConnectedComponent = List +typealias Path = List + +/** + * Find all connected components of the graph. + * Time Complexity is O(V+E) + * Space Complexity is O(V) + */ +fun > Graph.findConnectedComponents(): List> { + val visited = mutableSetOf() + val result = mutableListOf>() + + for (vertex in adjacencyList.keys) { + if (vertex in visited) continue + + val nextConnectedComponent = connectedComponentsDfs(vertex, adjacencyList, visited) + result.add(nextConnectedComponent) + } + + return result +} + +fun > Graph.findAllPossibleDirections(): MultiMap { + val connectedComponents = findConnectedComponents() + return connectedComponents.findAllPossibleDirections() +} + +fun > Graph.findAllPossibleDirectionsToList(): MultiMapList { + val connectedComponents = findConnectedComponents() + return connectedComponents.findAllPossibleDirectionsToList() +} + +fun List>.findAllPossibleDirections(): MultiMap { + val result = mutableMapOf>() + + forEach { connectedComponent -> + val asSet = connectedComponent.toSet() + + asSet.forEach { node -> + // in the connected component every node is connected to every other except itself + result[node] = asSet - node + } + } + + return result +} + +fun List>.findAllPossibleDirectionsToList(): MultiMapList { + val result = mutableMapOf>() + + forEach { connectedComponent -> + connectedComponent.forEach { node -> + // in the connected component every node is connected to every other except itself + result[node] = connectedComponent - node + } + } + + return result +} + +fun > Graph.findDijkstraPathsBetween(from: N, to: N, limit: Int): List> { + data class QueueElement(val currentPath: Path, val nodeList: List, val score: Int) : Comparable { + override fun compareTo(other: QueueElement): Int { + return score - other.score + } + } + + val paths = mutableListOf>() + + val count = mutableMapOf() + adjacencyList.keys.forEach { count[it] = 0 } + + val heap = PriorityQueue() + heap.add(QueueElement(currentPath = emptyList(), nodeList = listOf(from), score = 0)) + + while (heap.isNotEmpty() && paths.size < limit) { + val minimumQueueElement = heap.poll()!! + val lastNode = minimumQueueElement.nodeList.last() + + val newCount = count.getValue(lastNode) + 1 + count[lastNode] = newCount + + if (lastNode == to) { + paths.add(minimumQueueElement.currentPath) + continue + } + + if (newCount <= limit) { + adjacencyList.getValue(lastNode).forEach { edge -> + if (edge.to in minimumQueueElement.nodeList) return@forEach + + val newElement = QueueElement( + currentPath = minimumQueueElement.currentPath + edge, + nodeList = minimumQueueElement.nodeList + edge.to, + score = minimumQueueElement.score + 1 + ) + + heap.add(newElement) + } + } + } + + return paths +} + +private fun > connectedComponentsDfs( + node: N, + adjacencyList: Map>, + visited: MutableSet, + connectedComponentState: MutableList = mutableListOf() +): ConnectedComponent { + visited.add(node) + connectedComponentState.add(node) + + for (edge in adjacencyList.getValue(node)) { + if (edge.to !in visited) { + connectedComponentsDfs(edge.to, adjacencyList, visited, connectedComponentState) + } + } + + return connectedComponentState +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt new file mode 100644 index 0000000000..9c7d68bf8a --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/GraphBuilder.kt @@ -0,0 +1,28 @@ +package io.novafoundation.nova.common.utils.graph + +import io.novafoundation.nova.common.utils.MultiMapList + +class GraphBuilder> { + + private val adjacencyList: MutableMap> = mutableMapOf() + + fun addEdge(from: N, to: E) { + val fromEdges = adjacencyList.getOrPut(from) { mutableListOf() } + fromEdges.add(to) + } + + fun addEdges(from: N, to: List) { + val fromEdges = adjacencyList.getOrPut(from) { mutableListOf() } + fromEdges.addAll(to) + } + + fun build(): Graph { + return Graph(adjacencyList) + } +} + +fun > GraphBuilder.addEdges(map: MultiMapList) { + map.forEach { (fromNode, toNodes) -> + addEdges(fromNode, toNodes) + } +} diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/graph/SimpleGraph.kt b/common/src/main/java/io/novafoundation/nova/common/utils/graph/SimpleGraph.kt new file mode 100644 index 0000000000..a3960da08b --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/graph/SimpleGraph.kt @@ -0,0 +1,31 @@ +package io.novafoundation.nova.common.utils.graph + +import io.novafoundation.nova.common.utils.MultiMapList + +data class SimpleEdge(override val from: N, override val to: N) : Edge + +typealias SimpleGraph = Graph> + +fun Graph.Companion.createSimple(vararg multiMaps: MultiMapList): SimpleGraph { + return createSimple(multiMaps.toList()) +} + +fun Graph.Companion.createSimple(vararg adjacencyPairs: Pair>): SimpleGraph { + return createSimple(adjacencyPairs.toMap()) +} + +fun Graph.Companion.createSimple(multiMaps: List>): SimpleGraph { + return GraphBuilder>().apply { + multiMaps.forEach(::addSimpleEdges) + }.build() +} + +fun GraphBuilder>.addSimpleEdges(map: MultiMapList) { + map.forEach { (fromNode, toNodes) -> + addSimpleEdges(fromNode, toNodes) + } +} + +fun GraphBuilder>.addSimpleEdges(from: N, to: List) { + addEdges(from, to.map { SimpleEdge(from, it) }) +} diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 1f3611c872..d52410cb36 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,12 +1,15 @@ + %s units of %s + + %s for %s Use Proxies to delegate Staking operations to another account Select stash account to setup proxy Please switch your wallet to %s to setup a proxy - %s not supported + %s is not supported Revoke access type Revoke for diff --git a/common/src/test/java/io/novafoundation/nova/common/utils/graph/GraphKtTest.kt b/common/src/test/java/io/novafoundation/nova/common/utils/graph/GraphKtTest.kt new file mode 100644 index 0000000000..6da292b72f --- /dev/null +++ b/common/src/test/java/io/novafoundation/nova/common/utils/graph/GraphKtTest.kt @@ -0,0 +1,101 @@ +package io.novafoundation.nova.common.utils.graph + +import io.novafoundation.nova.common.utils.mapToSet +import io.novafoundation.nova.test_shared.assertListEquals +import io.novafoundation.nova.test_shared.assertMapEquals +import io.novafoundation.nova.test_shared.assertSetEquals +import org.junit.Test + +internal class GraphKtTest { + + @Test + fun shouldFindConnectedComponents() { + // 3 and 2 are connected through 1 + testConnectedComponents( + 1 to listOf(2, 3), + 2 to listOf(1), + 3 to listOf(1), + expectedComponents = listOf(listOf(1, 2, 3)) + ) + + testConnectedComponents( + 1 to listOf(2), + 2 to listOf(1), + 3 to emptyList(), + expectedComponents = listOf(listOf(1, 2), listOf(3)) + ) + + testConnectedComponents( + 1 to listOf(2, 3), + 2 to listOf(1), + 3 to listOf(1), + 4 to listOf(5), + 5 to listOf(4), + 6 to emptyList(), + expectedComponents = listOf(listOf(1, 2, 3), listOf(4, 5), listOf(6)) + ) + } + + @Test + fun shouldFindAllPossibleDirections() { + val graph = Graph.createSimple( + 1 to listOf(2, 3), + 2 to listOf(1), + 3 to listOf(1), + 4 to listOf(5), + 5 to listOf(4), + 6 to emptyList(), + ) + val actual = graph.findAllPossibleDirections() + val expected = mapOf( + 1 to setOf(2, 3), + 2 to setOf(1, 3), + 3 to setOf(1, 2), + 4 to setOf(5), + 5 to setOf(4), + 6 to emptySet() + ) + + assertMapEquals(expected, actual) + } + + @Test + fun shouldFindPaths() { + val graph = Graph.createSimple( + 1 to listOf(2, 3, 4), + 2 to listOf(1, 4, 3), + 3 to listOf(1, 2), + 4 to listOf(1, 2) + ) + + var actual = graph.findDijkstraPathsBetween(2, 3, limit = 3) + var expected = listOf( + listOf(SimpleEdge(2, 3)), + listOf(SimpleEdge(2, 1), SimpleEdge(1, 3)), + listOf(SimpleEdge(2, 4), SimpleEdge(4, 1), SimpleEdge(1, 3)), + ) + assertListEquals(expected, actual) + + actual = graph.findDijkstraPathsBetween(2, 3, limit = 1) + expected = listOf( + listOf(SimpleEdge(2, 3)), + ) + + assertListEquals(expected, actual) + } + + private fun testConnectedComponents( + vararg adjacencyPairs: Pair>, + expectedComponents: List> + ) { + val graph = Graph.createSimple(*adjacencyPairs) + val actualComponents = graph.findConnectedComponents().unordered() + val expectedUnordered = expectedComponents.unordered() + + assertSetEquals(expectedUnordered, actualComponents) + } + + private fun Iterable>.unordered(): Set> { + return mapToSet { it.toSet() } + } +} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt index ee400583e8..db68c59fdf 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/AppDatabase.kt @@ -14,7 +14,7 @@ import io.novafoundation.nova.core_db.converters.ExternalBalanceTypeConverters import io.novafoundation.nova.core_db.converters.LongMathConverters import io.novafoundation.nova.core_db.converters.MetaAccountTypeConverters import io.novafoundation.nova.core_db.converters.NetworkTypeConverters -import io.novafoundation.nova.core_db.converters.NftTypeConverters +import io.novafoundation.nova.core_db.converters.NftConverters import io.novafoundation.nova.core_db.converters.OperationConverters import io.novafoundation.nova.core_db.converters.ProxyAccountConverters import io.novafoundation.nova.core_db.dao.AccountDao @@ -57,6 +57,7 @@ import io.novafoundation.nova.core_db.migrations.AddEventIdToOperation_47_48 import io.novafoundation.nova.core_db.migrations.AddExternalBalances_45_46 import io.novafoundation.nova.core_db.migrations.AddExtrinsicContentField_37_38 import io.novafoundation.nova.core_db.migrations.AddFavouriteDApps_9_10 +import io.novafoundation.nova.core_db.migrations.AddFungibleNfts_55_56 import io.novafoundation.nova.core_db.migrations.AddGovernanceDapps_25_26 import io.novafoundation.nova.core_db.migrations.AddGovernanceExternalApiToChain_27_28 import io.novafoundation.nova.core_db.migrations.AddGovernanceFlagToChains_24_25 @@ -136,7 +137,7 @@ import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal @Database( - version = 55, + version = 56, entities = [ AccountLocal::class, NodeLocal::class, @@ -182,7 +183,7 @@ import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal NetworkTypeConverters::class, OperationConverters::class, CryptoTypeConverters::class, - NftTypeConverters::class, + NftConverters::class, MetaAccountTypeConverters::class, CurrencyConverters::class, AssetConverters::class, @@ -227,6 +228,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(AddPoolIdToOperations_46_47, AddEventIdToOperation_47_48, AddSwapOption_48_49) .addMigrations(RefactorOperations_49_50, AddTransactionVersionToRuntime_50_51, AddBalanceModesToAssets_51_52) .addMigrations(ChangeSessionTopicToParing_52_53, AddConnectionStateToChains_53_54, AddProxyAccount_54_55) + .addMigrations(AddFungibleNfts_55_56) .build() } return instance!! diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/NftConverters.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/NftConverters.kt new file mode 100644 index 0000000000..0b2bcbc01a --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/NftConverters.kt @@ -0,0 +1,27 @@ +package io.novafoundation.nova.core_db.converters + +import androidx.room.TypeConverter +import io.novafoundation.nova.core_db.model.NftLocal + +class NftConverters { + + @TypeConverter + fun fromNftType(type: NftLocal.Type): String { + return type.name + } + + @TypeConverter + fun toNftType(name: String): NftLocal.Type { + return enumValueOf(name) + } + + @TypeConverter + fun fromNftIssuanceType(type: NftLocal.IssuanceType): String { + return type.name + } + + @TypeConverter + fun toNftIssuanceType(name: String): NftLocal.IssuanceType { + return enumValueOf(name) + } +} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/NftTypeConverters.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/converters/NftTypeConverters.kt deleted file mode 100644 index 8bc436679b..0000000000 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/converters/NftTypeConverters.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.novafoundation.nova.core_db.converters - -import androidx.room.TypeConverter -import io.novafoundation.nova.core_db.model.NftLocal - -class NftTypeConverters { - - @TypeConverter - fun from(type: NftLocal.Type): String { - return type.name - } - - @TypeConverter - fun to(name: String): NftLocal.Type { - return enumValueOf(name) - } -} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/54_55_AddFungibleNfts.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/54_55_AddFungibleNfts.kt new file mode 100644 index 0000000000..a4cfca146a --- /dev/null +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/migrations/54_55_AddFungibleNfts.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.core_db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val AddFungibleNfts_55_56 = object : Migration(55, 56) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP INDEX IF EXISTS `index_nfts_metaId`") + database.execSQL("DROP TABLE nfts") + + /* ktlint-disable max-line-length */ + database.execSQL( + "CREATE TABLE IF NOT EXISTS `nfts` (`identifier` TEXT NOT NULL, `metaId` INTEGER NOT NULL, `chainId` TEXT NOT NULL, `collectionId` TEXT NOT NULL, `instanceId` TEXT, `metadata` BLOB, `type` TEXT NOT NULL, `wholeDetailsLoaded` INTEGER NOT NULL, `name` TEXT, `label` TEXT, `media` TEXT, `issuanceType` TEXT NOT NULL, `issuanceTotal` TEXT, `issuanceMyEdition` TEXT, `issuanceMyAmount` TEXT, `price` TEXT, `pricedUnits` TEXT, PRIMARY KEY(`identifier`))" + ) + database.execSQL("CREATE INDEX IF NOT EXISTS `index_nfts_metaId` ON `nfts` (`metaId`)") + } +} diff --git a/core-db/src/main/java/io/novafoundation/nova/core_db/model/NftLocal.kt b/core-db/src/main/java/io/novafoundation/nova/core_db/model/NftLocal.kt index 28a021bcc4..34a1c19433 100644 --- a/core-db/src/main/java/io/novafoundation/nova/core_db/model/NftLocal.kt +++ b/core-db/src/main/java/io/novafoundation/nova/core_db/model/NftLocal.kt @@ -27,14 +27,30 @@ data class NftLocal( val media: String? = null, // --- !metadata fields --- - val issuanceTotal: Int? = null, + val issuanceType: IssuanceType, + val issuanceTotal: BigInteger? = null, val issuanceMyEdition: String? = null, + val issuanceMyAmount: BigInteger? = null, val price: BigInteger? = null, + // use null to indicate non-fungible price + val pricedUnits: BigInteger? = null ) : Identifiable { enum class Type { - UNIQUES, RMRK1, RMRK2 + UNIQUES, RMRK1, RMRK2, PDC20 + } + + enum class IssuanceType { + + // issuanceMyEdition: optional + UNLIMITED, + + // issuanceMyEdition + issuanceTotal + LIMITED, + + // issuanceTotal + issuanceFungible + FUNGIBLE } override fun equals(other: Any?): Boolean { diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt index c8597e5d99..9d462a9eba 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/extrinsic/ExtrinsicService.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.extrinsic.signer.FeeSigner import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.BatchMode import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder import jp.co.soramitsu.fearless_utils.runtime.extrinsic.signer.Signer import kotlinx.coroutines.flow.Flow @@ -39,35 +40,42 @@ class SubmissionOrigin( class ExtrinsicSubmission(val hash: String, val submissionOrigin: SubmissionOrigin) +private val DEFAULT_BATCH_MODE = BatchMode.BATCH_ALL + interface ExtrinsicService { suspend fun submitExtrinsic( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode = DEFAULT_BATCH_MODE, formExtrinsic: FormExtrinsicWithOrigin, ): Result suspend fun submitAndWatchExtrinsic( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode = DEFAULT_BATCH_MODE, formExtrinsic: FormExtrinsicWithOrigin, ): Result> suspend fun submitMultiExtrinsicAwaitingInclusion( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode = DEFAULT_BATCH_MODE, formExtrinsic: FormMultiExtrinsicWithOrigin, ): RetriableMultiResult suspend fun paymentInfo( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode = DEFAULT_BATCH_MODE, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, ): FeeResponse suspend fun estimateFee( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode = DEFAULT_BATCH_MODE, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, ): Fee @@ -76,6 +84,7 @@ interface ExtrinsicService { suspend fun estimateMultiFee( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode = DEFAULT_BATCH_MODE, formExtrinsic: FormMultiExtrinsic, ): Fee diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt index ea8db504a4..11759d83f5 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/extrinsic/RealExtrinsicService.kt @@ -32,6 +32,7 @@ import io.novafoundation.nova.runtime.multiNetwork.getRuntime import io.novafoundation.nova.runtime.network.rpc.RpcCalls import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHex import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.BatchMode import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -51,10 +52,11 @@ class RealExtrinsicService( override suspend fun submitExtrinsic( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode, formExtrinsic: FormExtrinsicWithOrigin ): Result = runCatching { val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id) - val (extrinsic, submissionOrigin) = buildExtrinsic(chain, metaAccount, formExtrinsic) + val (extrinsic, submissionOrigin) = buildExtrinsic(chain, metaAccount, batchMode, formExtrinsic) val hash = rpcCalls.submitExtrinsic(chain.id, extrinsic) ExtrinsicSubmission(hash, submissionOrigin) @@ -63,10 +65,11 @@ class RealExtrinsicService( override suspend fun submitMultiExtrinsicAwaitingInclusion( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode, formExtrinsic: FormMultiExtrinsicWithOrigin ): RetriableMultiResult { return runMultiCatching( - intermediateListLoading = { constructSplitExtrinsicsForSubmission(chain, origin, formExtrinsic) }, + intermediateListLoading = { constructSplitExtrinsicsForSubmission(chain, origin, batchMode, formExtrinsic) }, listProcessing = { extrinsic -> rpcCalls.submitAndWatchExtrinsic(chain.id, extrinsic) .filterIsInstance() @@ -80,10 +83,11 @@ class RealExtrinsicService( override suspend fun submitAndWatchExtrinsic( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode, formExtrinsic: FormExtrinsicWithOrigin ): Result> = runCatching { val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id) - val (extrinsic) = buildExtrinsic(chain, metaAccount, formExtrinsic) + val (extrinsic) = buildExtrinsic(chain, metaAccount, batchMode, formExtrinsic) rpcCalls.submitAndWatchExtrinsic(chain.id, extrinsic) .takeWhileInclusive { !it.terminal } @@ -92,11 +96,12 @@ class RealExtrinsicService( override suspend fun paymentInfo( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, ): FeeResponse { val extrinsic = extrinsicBuilderFactory.createForFee(getFeeSigner(chain, origin), chain) .also { it.formExtrinsic() } - .build() + .build(batchMode) return rpcCalls.getExtrinsicFee(chain, extrinsic) } @@ -104,12 +109,13 @@ class RealExtrinsicService( override suspend fun estimateFee( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit ): Fee { val signer = getFeeSigner(chain, origin) val extrinsicBuilder = extrinsicBuilderFactory.createForFee(signer, chain) extrinsicBuilder.formExtrinsic() - val extrinsic = extrinsicBuilder.build() + val extrinsic = extrinsicBuilder.build(batchMode) return estimateFee(chain, extrinsic, signer) } @@ -139,12 +145,20 @@ class RealExtrinsicService( override suspend fun estimateMultiFee( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode, formExtrinsic: FormMultiExtrinsic ): Fee { val feeSigner = getFeeSigner(chain, origin) val feeExtrinsicBuilderSequence = extrinsicBuilderFactory.createMultiForFee(feeSigner, chain) - val extrinsics = constructSplitExtrinsics(chain, origin, formExtrinsic, feeExtrinsicBuilderSequence, alreadyComputedFeeSigner = feeSigner) + val extrinsics = constructSplitExtrinsics( + chain = chain, + origin = origin, + formExtrinsic = formExtrinsic, + extrinsicBuilderSequence = feeExtrinsicBuilderSequence, + alreadyComputedFeeSigner = feeSigner, + batchMode = batchMode + ) if (extrinsics.isEmpty()) return zeroFee(chain, feeSigner) @@ -158,6 +172,7 @@ class RealExtrinsicService( private suspend fun constructSplitExtrinsicsForSubmission( chain: Chain, origin: TransactionOrigin, + batchMode: BatchMode, formExtrinsic: FormMultiExtrinsicWithOrigin, ): List { val metaAccount = accountRepository.requireMetaAccountFor(origin, chain.id) @@ -171,7 +186,7 @@ class RealExtrinsicService( val formExtrinsicWithOrigin: FormMultiExtrinsic = { formExtrinsic(submissionOrigin) } - return constructSplitExtrinsics(chain, origin, formExtrinsicWithOrigin, extrinsicBuilderSequence) + return constructSplitExtrinsics(chain, origin, formExtrinsicWithOrigin, extrinsicBuilderSequence, batchMode) } private suspend fun constructSplitExtrinsics( @@ -179,6 +194,7 @@ class RealExtrinsicService( origin: TransactionOrigin, formExtrinsic: FormMultiExtrinsic, extrinsicBuilderSequence: Sequence, + batchMode: BatchMode, alreadyComputedFeeSigner: FeeSigner? = null, ): List = coroutineScope { val feeSigner = alreadyComputedFeeSigner ?: getFeeSigner(chain, origin) @@ -194,7 +210,7 @@ class RealExtrinsicService( batch.forEach(extrinsicBuilder::call) - extrinsicBuilder.build() + extrinsicBuilder.build(batchMode) } extrinsicsToSubmit @@ -203,6 +219,7 @@ class RealExtrinsicService( private suspend fun buildExtrinsic( chain: Chain, metaAccount: MetaAccount, + batchMode: BatchMode, formExtrinsic: FormExtrinsicWithOrigin, ): SubmissionRaw { val signer = signerProvider.rootSignerFor(metaAccount) @@ -216,7 +233,7 @@ class RealExtrinsicService( extrinsicBuilder.formExtrinsic(submissionOrigin) - val extrinsic = extrinsicBuilder.build(useBatchAll = true) + val extrinsic = extrinsicBuilder.build(batchMode) return SubmissionRaw(extrinsic, submissionOrigin) } diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt index f105ec0aea..4b994d91dd 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/BalanceListViewModel.kt @@ -35,6 +35,7 @@ import io.novafoundation.nova.feature_assets.presentation.model.AssetModel import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.model.Currency import io.novafoundation.nova.feature_currency_api.presentation.formatters.formatAsCurrency +import io.novafoundation.nova.feature_currency_api.presentation.formatters.simpleFormatAsCurrency import io.novafoundation.nova.feature_nft_api.data.model.Nft import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor import io.novafoundation.nova.feature_wallet_api.presentation.formatters.mapBalanceIdToUi @@ -137,7 +138,7 @@ class BalanceListViewModel( val currency = selectedCurrency.first() TotalBalanceModel( isBreakdownAbailable = breakdown.breakdown.isNotEmpty(), - totalBalanceFiat = breakdown.total.formatAsCurrency(currency).formatAsTotalBalance(), + totalBalanceFiat = breakdown.total.simpleFormatAsCurrency(currency).formatAsTotalBalance(), lockedBalanceFiat = breakdown.locksTotal.amount.formatAsCurrency(currency), enableSwap = swapSupported ) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsTotalBalanceView.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsTotalBalanceView.kt index 5bd6675ecf..085eee2f53 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsTotalBalanceView.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/balance/list/view/AssetsTotalBalanceView.kt @@ -40,8 +40,9 @@ class AssetsTotalBalanceView @JvmOverloads constructor( fun showTotalBalance(totalBalance: TotalBalanceModel) { viewAssetsTotalBalanceShimmer.setShimmerVisible(false) - viewAssetsTotalBalanceTotal.setVisible(true, falseState = View.INVISIBLE) + viewAssetsTotalBalanceTotal.setVisible(true) viewAssetsTotalBalanceTotal.text = totalBalance.totalBalanceFiat + viewAssetsTotalBalanceTotal.requestLayout() // to fix the issue when elipsing the text is working incorrectly during fast text update viewAssetsTotalBalanceLockedContainer.setVisible(totalBalance.isBreakdownAbailable) diff --git a/feature-assets/src/main/res/layout/view_total_balance.xml b/feature-assets/src/main/res/layout/view_total_balance.xml index cda7d37864..6c01a42cd4 100644 --- a/feature-assets/src/main/res/layout/view_total_balance.xml +++ b/feature-assets/src/main/res/layout/view_total_balance.xml @@ -35,6 +35,8 @@ style="@style/TextAppearance.NovaFoundation.Bold.HugeTitle" android:layout_width="match_parent" android:layout_height="wrap_content" + android:ellipsize="end" + android:lines="1" android:textColor="@color/text_primary" android:visibility="gone" tools:text="$214.66" diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt index 3b0d232328..b09b147df1 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/domain/contribute/CrowdloanContributeInteractor.kt @@ -88,7 +88,7 @@ class CrowdloanContributeInteractor( customizationPayload = customizationPayload, toCalculateFee = true ) { submission, chain, _ -> - extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, submission) + extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, formExtrinsic = submission) } suspend fun contribute( diff --git a/feature-currency-api/src/main/java/io/novafoundation/nova/feature_currency_api/presentation/formatters/CurrencyFormatters.kt b/feature-currency-api/src/main/java/io/novafoundation/nova/feature_currency_api/presentation/formatters/CurrencyFormatters.kt index 479d53de09..f046c25bd3 100644 --- a/feature-currency-api/src/main/java/io/novafoundation/nova/feature_currency_api/presentation/formatters/CurrencyFormatters.kt +++ b/feature-currency-api/src/main/java/io/novafoundation/nova/feature_currency_api/presentation/formatters/CurrencyFormatters.kt @@ -1,17 +1,30 @@ package io.novafoundation.nova.feature_currency_api.presentation.formatters import io.novafoundation.nova.common.utils.formatting.currencyFormatter +import io.novafoundation.nova.common.utils.formatting.simpleCurrencyFormatter import io.novafoundation.nova.feature_currency_api.domain.model.Currency import java.math.BigDecimal import java.math.RoundingMode private val currencyFormatter = currencyFormatter() +private val simpleCurrencyFormatter = simpleCurrencyFormatter() fun BigDecimal.formatAsCurrency(currency: Currency, roundingMode: RoundingMode = RoundingMode.FLOOR): String { return formatAsCurrency(currency.symbol, currency.code, roundingMode) } +fun BigDecimal.simpleFormatAsCurrency(currency: Currency, roundingMode: RoundingMode = RoundingMode.FLOOR): String { + return simpleFormatAsCurrency(currency.symbol, currency.code, roundingMode) +} + fun BigDecimal.formatAsCurrency(symbol: String?, code: String, roundingMode: RoundingMode = RoundingMode.FLOOR): String { - val currencySymbol = symbol ?: "$code " - return currencySymbol + currencyFormatter.format(this, roundingMode) + return formatCurrencySymbol(symbol, code) + currencyFormatter.format(this, roundingMode) +} + +fun BigDecimal.simpleFormatAsCurrency(symbol: String?, code: String, roundingMode: RoundingMode = RoundingMode.FLOOR): String { + return formatCurrencySymbol(symbol, code) + simpleCurrencyFormatter.format(this, roundingMode) +} + +private fun formatCurrencySymbol(symbol: String?, code: String): String { + return symbol ?: "$code " } diff --git a/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/Nft.kt b/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/Nft.kt index 3a87010c8c..7a2c856fe5 100644 --- a/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/Nft.kt +++ b/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/Nft.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_nft_api.data.model +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId import java.math.BigInteger @@ -18,7 +19,7 @@ class Nft( sealed class Details { class Loaded( - val price: BigInteger?, + val price: Price?, val issuance: Issuance, val name: String?, val label: String?, @@ -28,23 +29,35 @@ class Nft( object Loadable : Details() } + sealed class Price { + + class NonFungible(val nftPrice: BigInteger) : Price() + + class Fungible(val units: BigInteger, val totalPrice: Balance) : Price() + } + sealed class Issuance { - class Unlimited(val edition: String) : Issuance() + + object Unlimited : Issuance() class Limited(val max: Int, val edition: Int) : Issuance() + + class Fungible(val myAmount: BigInteger, val totalSupply: BigInteger) : Issuance() } sealed class Type(val key: Key) { enum class Key { - UNIQUES, RMRKV1, RMRKV2 + UNIQUES, RMRKV1, RMRKV2, PDC20 } - class Uniques(val instanceId: BigInteger, val collectionId: BigInteger) : Type(Key.UNIQUES) + object Uniques : Type(Key.UNIQUES) + + object Rmrk1 : Type(Key.RMRKV1) - class Rmrk1(val instanceId: String, val collectionId: String) : Type(Key.RMRKV1) + object Rmrk2 : Type(Key.RMRKV2) - class Rmrk2(val collectionId: String) : Type(Key.RMRKV2) + object Pdc20 : Type(Key.PDC20) } } diff --git a/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/NftDetails.kt b/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/NftDetails.kt index a9309d549e..371de24bab 100644 --- a/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/NftDetails.kt +++ b/feature-nft-api/src/main/java/io/novafoundation/nova/feature_nft_api/data/model/NftDetails.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_nft_api.data.model import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId -import java.math.BigInteger class NftDetails( val identifier: String, @@ -13,7 +12,7 @@ class NftDetails( val name: String, val description: String?, val issuance: Nft.Issuance, - val price: BigInteger?, + val price: Nft.Price?, val collection: Collection? ) { diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/mappers/Nft.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/mappers/Nft.kt index a0b9b48e9b..74736de1fc 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/mappers/Nft.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/mappers/Nft.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_nft_impl.data.mappers +import io.novafoundation.nova.common.utils.isZero import io.novafoundation.nova.core_db.model.NftLocal import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn @@ -14,26 +15,48 @@ fun mapNftTypeLocalToTypeKey( NftLocal.Type.UNIQUES -> Nft.Type.Key.UNIQUES NftLocal.Type.RMRK1 -> Nft.Type.Key.RMRKV1 NftLocal.Type.RMRK2 -> Nft.Type.Key.RMRKV2 + NftLocal.Type.PDC20 -> Nft.Type.Key.PDC20 +} + +fun nftIssuance( + typeLocal: NftLocal.IssuanceType, + issuanceTotal: BigInteger?, + issuanceMyEdition: String?, + issuanceMyAmount: BigInteger? +): Nft.Issuance { + return when (typeLocal) { + NftLocal.IssuanceType.UNLIMITED -> Nft.Issuance.Unlimited + + NftLocal.IssuanceType.LIMITED -> { + val myEditionInt = issuanceMyEdition?.toIntOrNull() + + if (issuanceTotal != null && issuanceTotal.isZero && myEditionInt != null) { + Nft.Issuance.Limited(max = issuanceTotal.toInt(), edition = myEditionInt) + } else { + Nft.Issuance.Unlimited + } + } + NftLocal.IssuanceType.FUNGIBLE -> if (issuanceTotal != null && issuanceMyAmount != null) { + Nft.Issuance.Fungible(myAmount = issuanceMyAmount, totalSupply = issuanceTotal) + } else { + Nft.Issuance.Unlimited + } + } } fun nftIssuance(nftLocal: NftLocal): Nft.Issuance { require(nftLocal.wholeDetailsLoaded) - return if (nftLocal.issuanceTotal != null && nftLocal.issuanceTotal!! > 0) { - Nft.Issuance.Limited( - max = nftLocal.issuanceTotal!!, - edition = nftLocal.issuanceMyEdition!!.toInt() - ) - } else { - Nft.Issuance.Unlimited(nftLocal.issuanceMyEdition!!) - } + return nftIssuance(nftLocal.issuanceType, nftLocal.issuanceTotal, nftLocal.issuanceMyEdition, nftLocal.issuanceMyAmount) } -fun nftPrice(nftLocal: NftLocal): BigInteger? { - return if (nftLocal.price == BigInteger.ZERO) { - null - } else { - nftLocal.price +fun nftPrice(nftLocal: NftLocal): Nft.Price? { + val price = nftLocal.price + if (price == null || price == BigInteger.ZERO) return null + + return when (val units = nftLocal.pricedUnits) { + null -> Nft.Price.NonFungible(price) + else -> Nft.Price.Fungible(units = units, totalPrice = price) } } @@ -45,17 +68,10 @@ fun mapNftLocalToNft( val chain = chainsById[nftLocal.chainId] ?: return null val type = when (nftLocal.type) { - NftLocal.Type.UNIQUES -> Nft.Type.Uniques( - instanceId = nftLocal.instanceId!!.toBigInteger(), - collectionId = nftLocal.collectionId.toBigInteger(), - ) - NftLocal.Type.RMRK1 -> Nft.Type.Rmrk1( - instanceId = nftLocal.instanceId!!, - collectionId = nftLocal.collectionId - ) - NftLocal.Type.RMRK2 -> Nft.Type.Rmrk2( - collectionId = nftLocal.collectionId - ) + NftLocal.Type.UNIQUES -> Nft.Type.Uniques + NftLocal.Type.RMRK1 -> Nft.Type.Rmrk1 + NftLocal.Type.RMRK2 -> Nft.Type.Rmrk2 + NftLocal.Type.PDC20 -> Nft.Type.Pdc20 } val details = if (nftLocal.wholeDetailsLoaded) { diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/NftProvidersRegistry.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/NftProvidersRegistry.kt index f27d1f3388..ce97b35be2 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/NftProvidersRegistry.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/NftProvidersRegistry.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_nft_impl.data.source import io.novafoundation.nova.feature_nft_api.data.model.Nft +import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.Pdc20Provider import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.RmrkV1NftProvider import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.RmrkV2NftProvider import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.UniquesNftProvider @@ -11,24 +12,24 @@ class NftProvidersRegistry( private val uniquesNftProvider: UniquesNftProvider, private val rmrkV1NftProvider: RmrkV1NftProvider, private val rmrkV2NftProvider: RmrkV2NftProvider, + private val pdc20Provider: Pdc20Provider, ) { private val statemineProviders = listOf(uniquesNftProvider) private val kusamaProviders = listOf(rmrkV1NftProvider, rmrkV2NftProvider) + private val polkadotProviders = listOf(pdc20Provider) fun get(chain: Chain): List { return when (chain.id) { Chain.Geneses.STATEMINE -> statemineProviders Chain.Geneses.KUSAMA -> kusamaProviders + Chain.Geneses.POLKADOT -> polkadotProviders else -> emptyList() } } fun nftSupported(chain: Chain): Boolean { - return when (chain.id) { - Chain.Geneses.STATEMINE, Chain.Geneses.KUSAMA -> true - else -> false - } + return get(chain).isNotEmpty() } fun get(nftTypeKey: Nft.Type.Key): NftProvider { @@ -36,6 +37,7 @@ class NftProvidersRegistry( Nft.Type.Key.RMRKV1 -> rmrkV1NftProvider Nft.Type.Key.RMRKV2 -> rmrkV2NftProvider Nft.Type.Key.UNIQUES -> uniquesNftProvider + Nft.Type.Key.PDC20 -> pdc20Provider } } } diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/Pdc20Provider.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/Pdc20Provider.kt new file mode 100644 index 0000000000..9e3f019212 --- /dev/null +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/Pdc20Provider.kt @@ -0,0 +1,111 @@ +package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20 + +import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.core_db.dao.NftDao +import io.novafoundation.nova.core_db.model.NftLocal +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_account_api.domain.model.addressIn +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn +import io.novafoundation.nova.feature_nft_api.data.model.Nft +import io.novafoundation.nova.feature_nft_api.data.model.NftDetails +import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance +import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice +import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider +import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Api +import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Listing +import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Request +import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount +import io.novafoundation.nova.runtime.ext.utilityAsset +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.Flow + +class Pdc20Provider( + private val api: Pdc20Api, + private val nftDao: NftDao, + private val accountRepository: AccountRepository, + private val chainRegistry: ChainRegistry, +) : NftProvider { + + override val requireFullChainSync: Boolean = false + + override suspend fun initialNftsSync(chain: Chain, metaAccount: MetaAccount, forceOverwrite: Boolean) { + val address = metaAccount.addressIn(chain) ?: return + + val request = Pdc20Request(address, network = Pdc20Api.NETWORK_POLKADOT) + val nfts = api.getNfts(request) + + val aggregatedListingsByToken = nfts.data.listings.groupBy { it.token.id } + .mapValues { (_, listings) -> + listings.reduce(Pdc20Listing::plus) + } + + val toSave = nfts.data.userTokenBalances.map { nftRemote -> + val listing = aggregatedListingsByToken[nftRemote.token.id] + + NftLocal( + identifier = nftRemote.token.id, + metaId = metaAccount.id, + chainId = chain.id, + collectionId = nftRemote.token.id, + instanceId = nftRemote.token.id, + metadata = null, + type = NftLocal.Type.PDC20, + wholeDetailsLoaded = true, + name = nftRemote.token.ticker, + label = null, + media = nftRemote.token.logo, + issuanceType = NftLocal.IssuanceType.FUNGIBLE, + // We dont know if supply or holding amount can be fractional or not so we are behaving safe + issuanceTotal = nftRemote.token.totalSupply?.toBigIntegerOrNull(), + issuanceMyAmount = nftRemote.balance.toBigIntegerOrNull(), + price = listing?.value?.let { chain.utilityAsset.planksFromAmount(it) }, + pricedUnits = listing?.amount + ) + } + + nftDao.insertNftsDiff(NftLocal.Type.PDC20, metaAccount.id, toSave, forceOverwrite) + } + + override suspend fun nftFullSync(nft: Nft) { + // do nothing + } + + override fun nftDetailsFlow(nftIdentifier: String): Flow { + return flowOf { + val nftLocal = nftDao.getNft(nftIdentifier) + require(nftLocal.wholeDetailsLoaded) { + "Cannot load details of non fully-synced NFT" + } + + val chain = chainRegistry.getChain(nftLocal.chainId) + val metaAccount = accountRepository.getMetaAccount(nftLocal.metaId) + + NftDetails( + identifier = nftLocal.identifier, + chain = chain, + owner = metaAccount.requireAccountIdIn(chain), + creator = null, + media = nftLocal.media, + name = nftLocal.name ?: nftLocal.instanceId!!, + description = null, + issuance = nftIssuance(nftLocal), + price = nftPrice(nftLocal), + collection = null // pdc20 token is the same as collection + ) + } + } +} + +private operator fun Pdc20Listing.plus(other: Pdc20Listing): Pdc20Listing { + require(this.from.address == other.from.address) + require(this.token.id == other.token.id) + + return Pdc20Listing( + from = from, + token = token, + amount = amount + other.amount, + value = value + other.value + ) +} diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20Api.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20Api.kt new file mode 100644 index 0000000000..5cb73ce95d --- /dev/null +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20Api.kt @@ -0,0 +1,15 @@ +package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network + +import io.novafoundation.nova.common.data.network.subquery.SubQueryResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface Pdc20Api { + + companion object { + const val NETWORK_POLKADOT = "polkadot" + } + + @POST("https://squid.subsquid.io/dot-ordinals/graphql") + suspend fun getNfts(@Body request: Pdc20Request): SubQueryResponse +} diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20NftResponse.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20NftResponse.kt new file mode 100644 index 0000000000..7b1ff88223 --- /dev/null +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20NftResponse.kt @@ -0,0 +1,43 @@ +package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network + +import java.math.BigDecimal +import java.math.BigInteger + +class Pdc20NftResponse( + val userTokenBalances: List, + val listings: List +) + +class Pdc20NftRemote( + val balance: String, + val address: PdcAddress, + val token: Token +) { + + class Token( + val id: String, + val logo: String?, + val ticker: String?, + val totalSupply: String?, + val network: String + ) +} + +class Pdc20Listing( + val from: PdcAddress, + val token: Token, + val amount: BigInteger, + val value: BigDecimal +) { + + class Token( + val id: String + ) +} + +class PdcAddress(val address: String) + +class RmrkV1NftMetadataRemote( + val image: String, + val description: String +) diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20Request.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20Request.kt new file mode 100644 index 0000000000..b2a3974099 --- /dev/null +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/pdc20/network/Pdc20Request.kt @@ -0,0 +1,49 @@ +package io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network + +class Pdc20Request(userAddress: String, network: String) { + + val query = """ + query { + userTokenBalances( + where: { + address: { + address_eq: "$userAddress" + } + standard_eq: "pdc-20" + token: { network_eq: "$network" } + } + ) { + balance + address { + address + } + token { + id + logo + ticker + totalSupply + network + } + } + + listings( + where: { + from: { address_eq: "$userAddress" } + standard_eq: "pdc-20" + token: { network_eq: "$network" } + } + ) { + from { + address + } + + token { + id + } + + amount + value + } + } + """.trimIndent() +} diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV1/RmrkV1NftProvider.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV1/RmrkV1NftProvider.kt index e2c2e7dafb..cd36077a36 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV1/RmrkV1NftProvider.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV1/RmrkV1NftProvider.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1 import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.core_db.dao.NftDao +import io.novafoundation.nova.core_db.model.NftLocal import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn @@ -49,7 +50,7 @@ class RmrkV1NftProvider( media = nftLocal.media, name = nftLocal.name!!, description = nftLocal.label, - issuance = nftIssuance(nftLocal), + issuance = nftIssuance(NftLocal.IssuanceType.LIMITED, nftLocal.issuanceTotal, nftLocal.issuanceMyEdition, nftLocal.issuanceMyAmount), price = nftPrice(nftLocal), collection = null ) diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV2/RmrkV2NftProvider.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV2/RmrkV2NftProvider.kt index 68ecca7ee4..244e5e8f80 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV2/RmrkV2NftProvider.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/rmrkV2/RmrkV2NftProvider.kt @@ -51,6 +51,7 @@ class RmrkV2NftProvider( type = NftLocal.Type.RMRK2, issuanceMyEdition = it.edition, wholeDetailsLoaded = false, + issuanceType = NftLocal.IssuanceType.LIMITED ) } @@ -72,7 +73,7 @@ class RmrkV2NftProvider( local.copy( media = image, - issuanceTotal = collection.max, + issuanceTotal = collection.max?.toBigInteger(), name = metadata?.name ?: local.name, label = metadata?.description ?: local.label, wholeDetailsLoaded = true diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt index c382e92c9e..b80971e15b 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/data/source/providers/uniques/UniquesNftProvider.kt @@ -12,9 +12,11 @@ import io.novafoundation.nova.core_db.model.NftLocal import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn +import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_nft_api.data.model.Nft import io.novafoundation.nova.feature_nft_api.data.model.NftDetails import io.novafoundation.nova.feature_nft_impl.data.mappers.nftIssuance +import io.novafoundation.nova.feature_nft_impl.data.mappers.nftPrice import io.novafoundation.nova.feature_nft_impl.data.network.distributed.FileStorageAdapter.adoptFileStorageLinkToHttps import io.novafoundation.nova.feature_nft_impl.data.source.NftProvider import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.network.IpfsApi @@ -91,8 +93,9 @@ class UniquesNftProvider( instanceId = instanceId.toString(), metadata = metadata, type = NftLocal.Type.UNIQUES, - issuanceTotal = totalIssuances.getValue(collectionId).toInt(), + issuanceTotal = totalIssuances.getValue(collectionId), issuanceMyEdition = instanceId.toString(), + issuanceType = NftLocal.IssuanceType.LIMITED, price = null, // to load at full sync @@ -169,13 +172,13 @@ class UniquesNftProvider( NftDetails( identifier = nftLocal.identifier, chain = chain, - owner = metaAccount.accountIdIn(chain)!!, + owner = metaAccount.requireAccountIdIn(chain), creator = classIssuer, media = nftLocal.media, name = nftLocal.name ?: nftLocal.instanceId!!, description = nftLocal.label, issuance = nftIssuance(nftLocal), - price = nftLocal.price, + price = nftPrice(nftLocal), collection = collection ) } diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/di/NftFeatureModule.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/di/NftFeatureModule.kt index e99646be7e..4a40d79e5f 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/di/NftFeatureModule.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/di/NftFeatureModule.kt @@ -9,9 +9,11 @@ import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository import io.novafoundation.nova.feature_nft_impl.data.repository.NftRepositoryImpl import io.novafoundation.nova.feature_nft_impl.data.source.JobOrchestrator import io.novafoundation.nova.feature_nft_impl.data.source.NftProvidersRegistry +import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.Pdc20Provider import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV1.RmrkV1NftProvider import io.novafoundation.nova.feature_nft_impl.data.source.providers.rmrkV2.RmrkV2NftProvider import io.novafoundation.nova.feature_nft_impl.data.source.providers.uniques.UniquesNftProvider +import io.novafoundation.nova.feature_nft_impl.di.modules.Pdc20Module import io.novafoundation.nova.feature_nft_impl.di.modules.RmrkV1Module import io.novafoundation.nova.feature_nft_impl.di.modules.RmrkV2Module import io.novafoundation.nova.feature_nft_impl.di.modules.UniquesModule @@ -21,7 +23,8 @@ import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry includes = [ UniquesModule::class, RmrkV1Module::class, - RmrkV2Module::class + RmrkV2Module::class, + Pdc20Module::class ] ) class NftFeatureModule { @@ -36,7 +39,8 @@ class NftFeatureModule { uniquesNftProvider: UniquesNftProvider, rmrkV1NftProvider: RmrkV1NftProvider, rmrkV2NftProvider: RmrkV2NftProvider, - ) = NftProvidersRegistry(uniquesNftProvider, rmrkV1NftProvider, rmrkV2NftProvider) + pdc20Provider: Pdc20Provider + ) = NftProvidersRegistry(uniquesNftProvider, rmrkV1NftProvider, rmrkV2NftProvider, pdc20Provider) @Provides @FeatureScope diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/di/modules/Pdc20Module.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/di/modules/Pdc20Module.kt new file mode 100644 index 0000000000..f58cbb5320 --- /dev/null +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/di/modules/Pdc20Module.kt @@ -0,0 +1,35 @@ +package io.novafoundation.nova.feature_nft_impl.di.modules + +import dagger.Module +import dagger.Provides +import io.novafoundation.nova.common.data.network.NetworkApiCreator +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.core_db.dao.NftDao +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.Pdc20Provider +import io.novafoundation.nova.feature_nft_impl.data.source.providers.pdc20.network.Pdc20Api +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry + +@Module +class Pdc20Module { + + @Provides + @FeatureScope + fun provideApi(networkApiCreator: NetworkApiCreator): Pdc20Api { + return networkApiCreator.create(Pdc20Api::class.java) + } + + @Provides + @FeatureScope + fun provideNftProvider( + api: Pdc20Api, + nftDao: NftDao, + accountRepository: AccountRepository, + chainRegistry: ChainRegistry, + ) = Pdc20Provider( + api = api, + nftDao = nftDao, + accountRepository = accountRepository, + chainRegistry = chainRegistry + ) +} diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/NftDetailsInteractor.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/NftDetailsInteractor.kt index 5f163a6a41..b9c00bd2b5 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/NftDetailsInteractor.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/NftDetailsInteractor.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.feature_nft_impl.domain.nft.details import io.novafoundation.nova.feature_nft_api.data.repository.NftRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository -import io.novafoundation.nova.feature_wallet_api.domain.model.Price import io.novafoundation.nova.runtime.ext.utilityAsset import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest @@ -18,12 +17,7 @@ class NftDetailsInteractor( tokenRepository.observeToken(nftDetails.chain.utilityAsset).map { token -> PricedNftDetails( nftDetails = nftDetails, - price = nftDetails.price?.let { - Price( - amount = it, - token = token - ) - } + priceToken = token ) } } diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/PricedNftDetails.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/PricedNftDetails.kt index d792c8c11d..2f9108d519 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/PricedNftDetails.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/domain/nft/details/PricedNftDetails.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_nft_impl.domain.nft.details import io.novafoundation.nova.feature_nft_api.data.model.NftDetails -import io.novafoundation.nova.feature_wallet_api.domain.model.Price +import io.novafoundation.nova.feature_wallet_api.domain.model.Token class PricedNftDetails( val nftDetails: NftDetails, - val price: Price? + val priceToken: Token ) diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/common/Mappers.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/common/Mappers.kt index 2d39ba27fe..3a88022c25 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/common/Mappers.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/common/Mappers.kt @@ -4,6 +4,9 @@ import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.formatting.format import io.novafoundation.nova.feature_nft_api.data.model.Nft import io.novafoundation.nova.feature_nft_impl.R +import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel +import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel fun ResourceManager.formatIssuance(issuance: Nft.Issuance): String { return when (issuance) { @@ -16,5 +19,37 @@ fun ResourceManager.formatIssuance(issuance: Nft.Issuance): String { issuance.max.format() ) } + + is Nft.Issuance.Fungible -> { + getString( + R.string.nft_issuance_fungible_format, + issuance.myAmount.format(), + issuance.totalSupply.format() + ) + } + } +} + +fun ResourceManager.formatNftPrice(price: Nft.Price?, priceToken: Token?): NftPriceModel? { + if (price == null || priceToken == null) return null + + return when (price) { + is Nft.Price.Fungible -> { + val units = price.units.format() + val amountModel = mapAmountToAmountModel(price.totalPrice, priceToken) + + NftPriceModel( + amountInfo = getString(R.string.nft_fungile_price, units, amountModel.token), + fiat = amountModel.fiat + ) + } + is Nft.Price.NonFungible -> { + val amountModel = mapAmountToAmountModel(price.nftPrice, priceToken) + + NftPriceModel( + amountInfo = amountModel.token, + fiat = amountModel.fiat + ) + } } } diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/common/model/NftPriceModel.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/common/model/NftPriceModel.kt new file mode 100644 index 0000000000..fc441dd55b --- /dev/null +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/common/model/NftPriceModel.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model + +class NftPriceModel( + val amountInfo: String, + val fiat: String? +) diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsFragment.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsFragment.kt index 520b7f3276..7ab489d419 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsFragment.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsFragment.kt @@ -11,6 +11,7 @@ import coil.load import io.novafoundation.nova.common.base.BaseFragment import io.novafoundation.nova.common.di.FeatureUtils import io.novafoundation.nova.common.utils.applyStatusBarInsets +import io.novafoundation.nova.common.utils.letOrHide import io.novafoundation.nova.common.utils.makeGone import io.novafoundation.nova.common.utils.makeVisible import io.novafoundation.nova.common.utils.setTextOrHide @@ -21,7 +22,8 @@ import io.novafoundation.nova.feature_account_api.view.showChain import io.novafoundation.nova.feature_nft_api.NftFeatureApi import io.novafoundation.nova.feature_nft_impl.R import io.novafoundation.nova.feature_nft_impl.di.NftFeatureComponent -import io.novafoundation.nova.feature_wallet_api.presentation.view.setPriceOrHide +import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel +import io.novafoundation.nova.feature_wallet_api.presentation.view.PriceSectionView import kotlinx.android.synthetic.main.fragment_nft_details.nftDetailsChain import kotlinx.android.synthetic.main.fragment_nft_details.nftDetailsCollection import kotlinx.android.synthetic.main.fragment_nft_details.nftDetailsCreator @@ -123,4 +125,8 @@ class NftDetailsFragment : BaseFragment() { } } } + + private fun PriceSectionView.setPriceOrHide(maybePrice: NftPriceModel?) = letOrHide(maybePrice) { price -> + setPrice(price.amountInfo, price.fiat) + } } diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsModel.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsModel.kt index 0854716d72..7819dc47e8 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsModel.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsModel.kt @@ -2,14 +2,14 @@ package io.novafoundation.nova.feature_nft_impl.presentation.nft.details import io.novafoundation.nova.common.address.AddressModel import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi -import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel +import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel class NftDetailsModel( val media: String?, val name: String, val issuance: String, val description: String?, - val price: AmountModel?, + val price: NftPriceModel?, val collection: Collection?, val owner: AddressModel, val creator: AddressModel?, diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsViewModel.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsViewModel.kt index f5bedfac40..f5b7bfb031 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsViewModel.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/details/NftDetailsViewModel.kt @@ -17,7 +17,7 @@ import io.novafoundation.nova.feature_nft_impl.NftRouter import io.novafoundation.nova.feature_nft_impl.domain.nft.details.NftDetailsInteractor import io.novafoundation.nova.feature_nft_impl.domain.nft.details.PricedNftDetails import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatIssuance -import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatNftPrice import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.flow.catch @@ -76,9 +76,7 @@ class NftDetailsViewModel( name = nftDetails.name, issuance = resourceManager.formatIssuance(nftDetails.issuance), description = nftDetails.description, - price = pricedNftDetails.price?.let { - mapAmountToAmountModel(it.amount, it.token) - }, + price = resourceManager.formatNftPrice(pricedNftDetails.nftDetails.price, pricedNftDetails.priceToken), collection = nftDetails.collection?.let { NftDetailsModel.Collection( name = it.name ?: it.id, diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftAdapter.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftAdapter.kt index de80e60564..7089886337 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftAdapter.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftAdapter.kt @@ -15,6 +15,7 @@ import io.novafoundation.nova.common.utils.dpF import io.novafoundation.nova.common.utils.inflateChild import io.novafoundation.nova.common.utils.makeGone import io.novafoundation.nova.common.utils.makeVisible +import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.common.view.shape.addRipple import io.novafoundation.nova.common.view.shape.getRippleMask import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable @@ -117,23 +118,23 @@ class NftHolder( itemNftIssuance.text = content.data.issuance itemNftTitle.text = content.data.title - val price = content.data.price - - if (price != null) { - itemNftPriceFiat.makeVisible() - itemNftPriceToken.makeVisible() - itemNftPricePlaceholder.makeGone() - - itemNftPriceToken.text = price.token - itemNftPriceFiat.text = price.fiat - } else { - itemNftPriceFiat.makeGone() - itemNftPriceToken.makeGone() - itemNftPricePlaceholder.makeVisible() - } + setPrice(content) } } setOnClickListener { itemHandler.itemClicked(item) } } + + private fun View.setPrice(content: LoadingState.Loaded) { + val price = content.data.price + + itemNftPriceToken.setVisible(price != null) + itemNftPriceFiat.setVisible(price != null) + itemNftPricePlaceholder.setVisible(price == null) + + if (price != null) { + itemNftPriceToken.text = price.amountInfo + itemNftPriceFiat.text = price.fiat + } + } } diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListItem.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListItem.kt index a9d93bcae4..4246a5c29a 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListItem.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListItem.kt @@ -1,7 +1,7 @@ package io.novafoundation.nova.feature_nft_impl.presentation.nft.list import io.novafoundation.nova.common.presentation.LoadingState -import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel +import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.model.NftPriceModel data class NftListItem( val content: LoadingState, @@ -11,7 +11,7 @@ data class NftListItem( data class Content( val issuance: String, val title: String, - val price: AmountModel?, + val price: NftPriceModel?, val media: String?, ) } diff --git a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListViewModel.kt b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListViewModel.kt index 82f7cbda23..90426c8e85 100644 --- a/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListViewModel.kt +++ b/feature-nft-impl/src/main/java/io/novafoundation/nova/feature_nft_impl/presentation/nft/list/NftListViewModel.kt @@ -15,7 +15,7 @@ import io.novafoundation.nova.feature_nft_impl.NftRouter import io.novafoundation.nova.feature_nft_impl.domain.nft.list.NftListInteractor import io.novafoundation.nova.feature_nft_impl.domain.nft.list.PricedNft import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatIssuance -import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel +import io.novafoundation.nova.feature_nft_impl.presentation.nft.common.formatNftPrice import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -69,17 +69,13 @@ class NftListViewModel( is Nft.Details.Loaded -> { val issuanceFormatted = resourceManager.formatIssuance(details.issuance) - val amountModel = if (details.price != null && pricedNft.nftPriceToken != null) { - mapAmountToAmountModel(details.price!!, pricedNft.nftPriceToken) - } else { - null - } + val price = resourceManager.formatNftPrice(details.price, pricedNft.nftPriceToken) LoadingState.Loaded( NftListItem.Content( issuance = issuanceFormatted, title = details.name ?: pricedNft.nft.instanceId ?: pricedNft.nft.collectionId, - price = amountModel, + price = price, media = details.media ) ) diff --git a/feature-nft-impl/src/main/res/layout/fragment_nft_details.xml b/feature-nft-impl/src/main/res/layout/fragment_nft_details.xml index 1f6fe4c63a..5d337b374e 100644 --- a/feature-nft-impl/src/main/res/layout/fragment_nft_details.xml +++ b/feature-nft-impl/src/main/res/layout/fragment_nft_details.xml @@ -48,6 +48,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" + android:layout_marginBottom="16dp" android:layout_marginTop="8dp" tools:text="#11 Edition of 9978" /> @@ -57,7 +58,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:layout_marginBottom="24dp" android:textColor="@color/text_secondary" diff --git a/feature-nft-impl/src/main/res/layout/item_nft.xml b/feature-nft-impl/src/main/res/layout/item_nft.xml index 2ccf6bc7b6..012edb3aa9 100644 --- a/feature-nft-impl/src/main/res/layout/item_nft.xml +++ b/feature-nft-impl/src/main/res/layout/item_nft.xml @@ -78,7 +78,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginTop="8dp" + android:layout_marginTop="10dp" android:layout_marginEnd="8dp" android:layout_marginBottom="12dp" android:gravity="center_vertical" @@ -89,18 +89,20 @@ style="@style/TextAppearance.NovaFoundation.SemiBold.Footnote" android:layout_width="wrap_content" android:layout_height="match_parent" + android:includeFontPadding="false" android:text="@string/nft_price_not_listed" android:textColor="@color/text_secondary" android:visibility="gone" - tools:visibility="visible" /> + /> + tools:text="10 DOT" /> diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt index 4aae91da9f..4b335cd857 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/StakingStatsDataSource.kt @@ -40,9 +40,9 @@ class RealStakingStatsDataSource( val response = api.fetchStakingStats(request, dashboardApiUrl).data val earnings = response.stakingApies.associatedById() - val rewards = response.rewards.associatedById() - val slashes = response.slashes.associatedById() - val activeStakers = response.activeStakers.groupedById() + val rewards = response.rewards?.associatedById() ?: emptyMap() + val slashes = response.slashes?.associatedById() ?: emptyMap() + val activeStakers = response.activeStakers?.groupedById() ?: emptyMap() request.stakingKeysMapping.mapValues { (originalStakingOptionId, stakingKeys) -> val totalReward = rewards.getPlanks(originalStakingOptionId) - slashes.getPlanks(originalStakingOptionId) diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/api/StakingStatsResponse.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/api/StakingStatsResponse.kt index 25c24cedfd..32edd52eb2 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/api/StakingStatsResponse.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/stats/api/StakingStatsResponse.kt @@ -8,10 +8,10 @@ import java.math.BigDecimal typealias StakingStatsRewards = SubQueryGroupedAggregates> class StakingStatsResponse( - val activeStakers: SubQueryNodes, + val activeStakers: SubQueryNodes?, val stakingApies: SubQueryNodes, - val rewards: SubQueryGroupedAggregates>, - val slashes: SubQueryGroupedAggregates> + val rewards: SubQueryGroupedAggregates>?, + val slashes: SubQueryGroupedAggregates>? ) { interface WithStakingId { diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/consensus/AuraSession.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/consensus/AuraSession.kt index d727d571cb..b5fcda9361 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/consensus/AuraSession.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/consensus/AuraSession.kt @@ -3,8 +3,6 @@ package io.novafoundation.nova.feature_staking_impl.data.repository.consensus import io.novafoundation.nova.common.utils.committeeManagementOrNull import io.novafoundation.nova.common.utils.electionsOrNull import io.novafoundation.nova.common.utils.numberConstantOrNull -import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.number -import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api.system import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.getRuntime @@ -12,6 +10,8 @@ import io.novafoundation.nova.runtime.network.updaters.BlockNumberUpdater import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull import io.novafoundation.nova.runtime.storage.source.query.metadata +import io.novafoundation.nova.runtime.storage.typed.number +import io.novafoundation.nova.runtime.storage.typed.system import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/ReQuoteTrigger.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/ReQuoteTrigger.kt new file mode 100644 index 0000000000..862456814a --- /dev/null +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/ReQuoteTrigger.kt @@ -0,0 +1,3 @@ +package io.novafoundation.nova.feature_swap_api.domain.model + +typealias ReQuoteTrigger = Unit diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt index cd716d9c69..8a89ea9768 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SlippageConfig.kt @@ -9,4 +9,19 @@ class SlippageConfig( val maxAvailableSlippage: Percent, val smallSlippage: Percent, val bigSlippage: Percent -) +) { + + companion object { + + fun default(): SlippageConfig { + return SlippageConfig( + defaultSlippage = Percent(0.5), + slippageTips = listOf(Percent(0.1), Percent(0.5), Percent(1.0)), + minAvailableSlippage = Percent(0.01), + maxAvailableSlippage = Percent(50.0), + smallSlippage = Percent(0.05), + bigSlippage = Percent(1.0) + ) + } + } +} diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt index 1e827dd495..be09630367 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuote.kt @@ -8,6 +8,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.model.ChainAssetWithAmou import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFee import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import java.math.BigDecimal data class SwapQuote( @@ -15,6 +16,7 @@ data class SwapQuote( val amountOut: ChainAssetWithAmount, val direction: SwapDirection, val priceImpact: Percent, + val path: QuotePath ) { val assetIn: Chain.Asset @@ -36,6 +38,11 @@ data class SwapQuote( } } +class QuotePath(val segments: List) { + + class Segment(val from: FullChainAssetId, val to: FullChainAssetId, val sourceId: String, val sourceParams: Map) +} + val SwapQuote.editedBalance: Balance get() = when (direction) { SwapDirection.SPECIFIED_IN -> planksIn diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt index 18f55da5b8..ad674c28d6 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/model/SwapQuoteArgs.kt @@ -22,6 +22,7 @@ class SwapExecuteArgs( val customFeeAsset: Chain.Asset?, val swapLimit: SwapLimit, val nativeAsset: Asset, + val path: QuotePath ) val SwapExecuteArgs.feeAsset: Chain.Asset @@ -42,13 +43,14 @@ sealed class SwapLimit(val expectedAmountIn: Balance, val expectedAmountOut: Bal ) : SwapLimit(expectedAmountIn, expectedAmountOut) } -fun SwapQuoteArgs.toExecuteArgs(quotedBalance: Balance, customFeeAsset: Chain.Asset?, nativeAsset: Asset): SwapExecuteArgs { +fun SwapQuoteArgs.toExecuteArgs(quote: SwapQuote, customFeeAsset: Chain.Asset?, nativeAsset: Asset): SwapExecuteArgs { return SwapExecuteArgs( assetIn = tokenIn.configuration, assetOut = tokenOut.configuration, - swapLimit = swapLimits(quotedBalance), + swapLimit = swapLimits(quote.quotedBalance), customFeeAsset = customFeeAsset, - nativeAsset = nativeAsset + nativeAsset = nativeAsset, + path = quote.path ) } diff --git a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt index 8edd6ce6fe..d56e2c79ae 100644 --- a/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt +++ b/feature-swap-api/src/main/java/io/novafoundation/nova/feature_swap_api/domain/swap/SwapService.kt @@ -1,6 +1,8 @@ package io.novafoundation.nova.feature_swap_api.domain.swap import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee @@ -27,4 +29,6 @@ interface SwapService { suspend fun swap(args: SwapExecuteArgs): Result suspend fun slippageConfig(chainId: ChainId): SlippageConfig? + + fun runSubscriptions(chainIn: Chain, metaAccount: MetaAccount): Flow } diff --git a/feature-swap-impl/build.gradle b/feature-swap-impl/build.gradle index 9793d06ddb..ebc6c3a84d 100644 --- a/feature-swap-impl/build.gradle +++ b/feature-swap-impl/build.gradle @@ -43,6 +43,8 @@ dependencies { implementation project(":common") implementation project(":runtime") + implementation project(":hydra-dx-math") + implementation materialDep implementation fearlessLibDep diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt index ff663744ea..b11a1903a9 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/AssetExchange.kt @@ -3,22 +3,25 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange import io.novafoundation.nova.common.utils.MultiMap 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_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn +import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow interface AssetExchange { interface Factory { - suspend fun create(chainId: ChainId, coroutineScope: CoroutineScope): AssetExchange? + suspend fun create(chain: Chain, coroutineScope: CoroutineScope): AssetExchange? } /** @@ -31,18 +34,41 @@ interface AssetExchange { suspend fun availableSwapDirections(): MultiMap @Throws(SwapQuoteException::class) - suspend fun quote(args: SwapQuoteArgs): AssetExchangeQuote + suspend fun quote(args: AssetExchangeQuoteArgs): AssetExchangeQuote suspend fun estimateFee(args: SwapExecuteArgs): AssetExchangeFee suspend fun swap(args: SwapExecuteArgs): Result suspend fun slippageConfig(): SlippageConfig + + fun runSubscriptions(chain: Chain, metaAccount: MetaAccount): Flow } +data class AssetExchangeQuoteArgs( + val chainAssetIn: Chain.Asset, + val chainAssetOut: Chain.Asset, + val amount: Balance, + val swapDirection: SwapDirection, +) + class AssetExchangeQuote( + val direction: SwapDirection, + val quote: Balance, -) + + val path: QuotePath +) : Comparable { + + override fun compareTo(other: AssetExchangeQuote): Int { + return when (direction) { + // When we want to sell a token, the bigger the quote - the better + SwapDirection.SPECIFIED_IN -> (quote - other.quote).signum() + // When we want to buy a token, the smaller the quote - the better + SwapDirection.SPECIFIED_OUT -> (other.quote - quote).signum() + } + } +} class AssetExchangeFee( val networkFee: Fee, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/network/blockhain/api/AssetConversionApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionApi.kt similarity index 94% rename from feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/network/blockhain/api/AssetConversionApi.kt rename to feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionApi.kt index c3624b3768..df9df237c6 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/network/blockhain/api/AssetConversionApi.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionApi.kt @@ -1,6 +1,6 @@ @file:Suppress("RedundantUnitExpression") -package io.novafoundation.nova.feature_swap_impl.data.network.blockhain.api +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion import io.novafoundation.nova.common.data.network.runtime.binding.bindPair import io.novafoundation.nova.common.utils.assetConversionOrNull diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt index 003eb1e32d..e18fcbf8d7 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/assetConversion/AssetConversionExchange.kt @@ -3,7 +3,6 @@ package io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConvers import io.novafoundation.nova.common.data.network.runtime.binding.bindNumberOrNull import io.novafoundation.nova.common.utils.Modules import io.novafoundation.nova.common.utils.MultiMap -import io.novafoundation.nova.common.utils.Percent import io.novafoundation.nova.common.utils.assetConversion import io.novafoundation.nova.common.utils.mutableMultiMapOf import io.novafoundation.nova.common.utils.put @@ -12,18 +11,19 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic 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_account_api.data.model.SubstrateFee +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn +import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit -import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeFee import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuote -import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.api.assetConversionOrNull -import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.api.pools +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset @@ -34,16 +34,14 @@ import io.novafoundation.nova.runtime.ext.emptyAccountId import io.novafoundation.nova.runtime.ext.fullId import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.extrinsic.CustomSignedExtensions.assetTxPayment -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.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.multiNetwork.getChainOrNull import io.novafoundation.nova.runtime.multiNetwork.multiLocation.MultiLocation import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverter import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.toMultiLocationOrThrow import io.novafoundation.nova.runtime.multiNetwork.multiLocation.toEncodableInstance +import io.novafoundation.nova.runtime.repository.ChainStateRepository import io.novafoundation.nova.runtime.storage.source.StorageDataSource import io.novafoundation.nova.runtime.storage.source.query.metadata import jp.co.soramitsu.fearless_utils.runtime.AccountId @@ -52,19 +50,20 @@ import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata import jp.co.soramitsu.fearless_utils.runtime.metadata.call import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map class AssetConversionExchangeFactory( - private val chainRegistry: ChainRegistry, private val multiLocationConverterFactory: MultiLocationConverterFactory, private val remoteStorageSource: StorageDataSource, private val runtimeCallsApi: MultiChainRuntimeCallsApi, private val extrinsicService: ExtrinsicService, private val assetSourceRegistry: AssetSourceRegistry, + private val chainStateRepository: ChainStateRepository, ) : AssetExchange.Factory { - override suspend fun create(chainId: ChainId, coroutineScope: CoroutineScope): AssetExchange? { - val chain = chainRegistry.getChainOrNull(chainId) ?: return null - + override suspend fun create(chain: Chain, coroutineScope: CoroutineScope): AssetExchange { val converter = multiLocationConverterFactory.default(chain, coroutineScope) return AssetConversionExchange( @@ -73,11 +72,14 @@ class AssetConversionExchangeFactory( remoteStorageSource = remoteStorageSource, multiChainRuntimeCallsApi = runtimeCallsApi, extrinsicService = extrinsicService, - assetSourceRegistry = assetSourceRegistry + assetSourceRegistry = assetSourceRegistry, + chainStateRepository = chainStateRepository ) } } +private const val SOURCE_ID = "AssetConversion" + private class AssetConversionExchange( private val chain: Chain, private val multiLocationConverter: MultiLocationConverter, @@ -85,6 +87,7 @@ private class AssetConversionExchange( private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, private val extrinsicService: ExtrinsicService, private val assetSourceRegistry: AssetSourceRegistry, + private val chainStateRepository: ChainStateRepository, ) : AssetExchange { override suspend fun canPayFeeInNonUtilityToken(asset: Chain.Asset): Boolean { @@ -100,16 +103,27 @@ private class AssetConversionExchange( } } - override suspend fun quote(args: SwapQuoteArgs): AssetExchangeQuote { + override suspend fun quote(args: AssetExchangeQuoteArgs): AssetExchangeQuote { val runtimeCallsApi = multiChainRuntimeCallsApi.forChain(chain.id) val quotedBalance = runtimeCallsApi.quote( swapDirection = args.swapDirection, - assetIn = args.tokenIn.configuration, - assetOut = args.tokenOut.configuration, + assetIn = args.chainAssetIn, + assetOut = args.chainAssetOut, amount = args.amount ) ?: throw SwapQuoteException.NotEnoughLiquidity - return AssetExchangeQuote(quote = quotedBalance) + val quotePath = QuotePath( + segments = listOf( + QuotePath.Segment( + from = args.chainAssetIn.fullId, + to = args.chainAssetOut.fullId, + sourceId = SOURCE_ID, + sourceParams = emptyMap() + ) + ) + ) + + return AssetExchangeQuote(quote = quotedBalance, path = quotePath, direction = args.swapDirection) } override suspend fun estimateFee(args: SwapExecuteArgs): AssetExchangeFee { @@ -128,14 +142,13 @@ private class AssetConversionExchange( } override suspend fun slippageConfig(): SlippageConfig { - return SlippageConfig( - defaultSlippage = Percent(0.5), - slippageTips = listOf(Percent(0.1), Percent(0.5), Percent(1.0)), - minAvailableSlippage = Percent(0.01), - maxAvailableSlippage = Percent(50.0), - smallSlippage = Percent(0.05), - bigSlippage = Percent(1.0) - ) + return SlippageConfig.default() + } + + override fun runSubscriptions(chain: Chain, metaAccount: MetaAccount): Flow { + return chainStateRepository.currentBlockNumberFlow(chain.id) + .drop(1) // skip immediate value from the cache to not perform double-quote on chain change + .map { ReQuoteTrigger } } private suspend fun constructAllAvailableDirections(pools: List>): MultiMap { diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt new file mode 100644 index 0000000000..675c419978 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxExchange.kt @@ -0,0 +1,592 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx + +import android.util.Log +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.common.utils.MultiMap +import io.novafoundation.nova.common.utils.firstById +import io.novafoundation.nova.common.utils.flatMap +import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.Path +import io.novafoundation.nova.common.utils.graph.create +import io.novafoundation.nova.common.utils.graph.findAllPossibleDirections +import io.novafoundation.nova.common.utils.graph.findDijkstraPathsBetween +import io.novafoundation.nova.common.utils.mapAsync +import io.novafoundation.nova.common.utils.mergeIfMultiple +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.common.utils.singleReplaySharedFlow +import io.novafoundation.nova.common.utils.structOf +import io.novafoundation.nova.common.utils.withFlowScope +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.awaitInBlock +import io.novafoundation.nova.feature_account_api.data.model.SubstrateFee +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_swap_api.domain.model.MinimumBalanceBuyIn +import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger +import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_impl.BuildConfig +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeFee +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuote +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.linkedAccounts +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals.referralsOrNull +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDepositInPlanks +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.isSystemAsset +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toChainAssetOrThrow +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatPlanks +import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilder +import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.ext.isUtilityAsset +import io.novafoundation.nova.runtime.ext.utilityAsset +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.metadata +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.BatchMode +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +private const val PATHS_LIMIT = 4 + +class HydraDxExchangeFactory( + private val remoteStorageSource: StorageDataSource, + private val sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, + private val extrinsicService: ExtrinsicService, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val hydraDxNovaReferral: HydraDxNovaReferral, + private val swapSourceFactories: Iterable, + private val assetSourceRegistry: AssetSourceRegistry, +) : AssetExchange.Factory { + + override suspend fun create(chain: Chain, coroutineScope: CoroutineScope): AssetExchange { + return HydraDxExchange( + remoteStorageSource = remoteStorageSource, + chain = chain, + storageSharedRequestsBuilderFactory = sharedRequestsBuilderFactory, + extrinsicService = extrinsicService, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + hydraDxNovaReferral = hydraDxNovaReferral, + swapSourceFactories = swapSourceFactories, + assetSourceRegistry = assetSourceRegistry + ) + } +} + +private typealias HydraSwapGraph = Graph +private typealias QuotePathsCacheKey = Pair + +private class HydraDxExchange( + private val remoteStorageSource: StorageDataSource, + private val chain: Chain, + private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, + private val extrinsicService: ExtrinsicService, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val hydraDxNovaReferral: HydraDxNovaReferral, + private val swapSourceFactories: Iterable, + private val assetSourceRegistry: AssetSourceRegistry, + private val debug: Boolean = BuildConfig.DEBUG +) : AssetExchange { + + private val swapSources: List = createSources() + + private val currentPaymentAsset: MutableSharedFlow = singleReplaySharedFlow() + + private val userReferralState: MutableSharedFlow = singleReplaySharedFlow() + + private val quotePathsCache: MutableStateFlow?> = MutableStateFlow(null) + + private val graphState: MutableSharedFlow = singleReplaySharedFlow() + + override suspend fun canPayFeeInNonUtilityToken(asset: Chain.Asset): Boolean { + val onChainId = hydraDxAssetIdConverter.toOnChainIdOrThrow(asset) + + if (hydraDxAssetIdConverter.isSystemAsset(onChainId)) return true + + val fallbackPrice = remoteStorageSource.query(chain.id) { + metadata.multiTransactionPayment.acceptedCurrencies.query(onChainId) + } + + return fallbackPrice != null + } + + override suspend fun availableSwapDirections(): MultiMap { + val allDirectDirections = swapSources.mapAsync { source -> + source.availableSwapDirections().mapValues { (from, directions) -> + directions.map { direction -> HydraDxSwapEdge(from, source.identifier, direction) } + } + } + + val graph = Graph.create(allDirectDirections).also { + graphState.emit(it) + } + + return graph.findAllPossibleDirections() + } + + override suspend fun quote(args: AssetExchangeQuoteArgs): AssetExchangeQuote { + val from = args.chainAssetIn.fullId + val to = args.chainAssetOut.fullId + + val paths = pathsFromCacheOrCompute(from, to) { + val graph = graphState.first() + + graph.findDijkstraPathsBetween(from, to, limit = PATHS_LIMIT) + } + + val quotedPaths = paths.mapNotNull { path -> quotePath(path, args.amount, args.swapDirection) } + if (paths.isEmpty()) { + throw SwapQuoteException.NotEnoughLiquidity + } + + if (debug) { + logQuotes(args, quotedPaths) + } + + return quotedPaths.max() + } + + override suspend fun estimateFee(args: SwapExecuteArgs): AssetExchangeFee { + val expectedFeeAsset = args.usedFeeAsset + + val currentFeeTokenId = currentPaymentAsset.first() + val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(expectedFeeAsset, currentFeeTokenId) + + val setCurrencyFee = if (paymentCurrencyToSet != null) { + extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) { + setFeeCurrency(paymentCurrencyToSet) + } + } else { + null + } + + val swapFee = extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { + executeSwap(args, paymentCurrencyToSet, currentFeeTokenId) + } + + val totalNativeFee = swapFee.amount + setCurrencyFee?.amount.orZero() + + val feeAmountInExpectedCurrency = if (!expectedFeeAsset.isUtilityAsset) { + convertNativeFeeToAssetFee(totalNativeFee, expectedFeeAsset) + } else { + totalNativeFee + } + val feeInExpectedCurrency = SubstrateFee( + amount = feeAmountInExpectedCurrency, + submissionOrigin = swapFee.submissionOrigin + ) + + return AssetExchangeFee(networkFee = feeInExpectedCurrency, MinimumBalanceBuyIn.NoBuyInNeeded) + } + + override suspend fun swap(args: SwapExecuteArgs): Result { + val expectedFeeAsset = args.usedFeeAsset + + val currentFeeTokenId = currentPaymentAsset.first() + val paymentCurrencyToSet = getPaymentCurrencyToSetIfNeeded(expectedFeeAsset, currentFeeTokenId) + + val setCurrencyResult = if (paymentCurrencyToSet != null) { + extrinsicService.submitAndWatchExtrinsic(chain, TransactionOrigin.SelectedWallet) { + setFeeCurrency(paymentCurrencyToSet) + }.awaitInBlock() // we need to wait for tx execution for currency update changes to be taken into account by runtime with executing swap itself + } else { + Result.success(Unit) + } + + return setCurrencyResult.flatMap { + extrinsicService.submitExtrinsic(chain, TransactionOrigin.SelectedWallet, BatchMode.FORCE_BATCH) { + executeSwap(args, paymentCurrencyToSet, currentFeeTokenId) + } + } + } + + override suspend fun slippageConfig(): SlippageConfig { + return SlippageConfig.default() + } + + override fun runSubscriptions(chain: Chain, metaAccount: MetaAccount): Flow { + return withFlowScope { scope -> + val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id) + val userAccountId = metaAccount.requireAccountIdIn(chain) + + val feeCurrency = remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { + metadata.multiTransactionPayment.accountCurrencyMap.observe(userAccountId) + } + + val userReferral = subscribeUserReferral(userAccountId, subscriptionBuilder).onEach { + userReferralState.emit(it) + } + + val sourcesSubscription = swapSources.map { + it.runSubscriptions(userAccountId, subscriptionBuilder) + }.mergeIfMultiple() + + subscriptionBuilder.subscribe(scope) + + val feeCurrencyUpdates = feeCurrency.onEach { tokenId -> + val feePaymentAsset = tokenId ?: hydraDxAssetIdConverter.systemAssetId + currentPaymentAsset.emit(feePaymentAsset) + } + + combine(sourcesSubscription, feeCurrencyUpdates, userReferral) { _, _, _ -> + ReQuoteTrigger + } + } + } + + private suspend fun quotePath( + path: Path, + amount: Balance, + swapDirection: SwapDirection + ): AssetExchangeQuote? { + val quote = when (swapDirection) { + SwapDirection.SPECIFIED_IN -> quotePathSell(path, amount) + SwapDirection.SPECIFIED_OUT -> quotePathBuy(path, amount) + } ?: return null + + return AssetExchangeQuote(swapDirection, quote, path.toQuotePath()) + } + + private suspend fun quotePathBuy(path: Path, amount: Balance): Balance? { + return runCatching { + path.foldRight(amount) { segment, currentAmount -> + val args = HydraDxSwapSourceQuoteArgs( + chainAssetIn = chain.assetsById.getValue(segment.from.assetId), + chainAssetOut = chain.assetsById.getValue(segment.to.assetId), + amount = currentAmount, + swapDirection = SwapDirection.SPECIFIED_OUT, + params = segment.direction.params + ) + + segment.swapSource().quote(args) + } + }.getOrNull() + } + + private suspend fun quotePathSell(path: Path, amount: Balance): Balance? { + return runCatching { + path.fold(amount) { currentAmount, segment -> + val args = HydraDxSwapSourceQuoteArgs( + chainAssetIn = chain.assetsById.getValue(segment.from.assetId), + chainAssetOut = chain.assetsById.getValue(segment.to.assetId), + amount = currentAmount, + swapDirection = SwapDirection.SPECIFIED_IN, + params = segment.direction.params + ) + + segment.swapSource().quote(args) + } + }.getOrNull() + } + + private val SwapExecuteArgs.usedFeeAsset: Chain.Asset + get() = customFeeAsset ?: chain.utilityAsset + + @Suppress("IfThenToElvis") + private suspend fun subscribeUserReferral( + userAccountId: AccountId, + subscriptionBuilder: StorageSharedRequestsBuilder + ): Flow { + return remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { + val referralsModule = metadata.referralsOrNull + + if (referralsModule != null) { + referralsModule.linkedAccounts.observe(userAccountId).map { linkedAccount -> + if (linkedAccount != null) ReferralState.SET else ReferralState.NOT_SET + } + } else { + flowOf(ReferralState.NOT_AVAILABLE) + } + } + } + + private suspend fun convertNativeFeeToAssetFee( + nativeFeeAmount: Balance, + targetAsset: Chain.Asset + ): Balance { + val args = AssetExchangeQuoteArgs( + chainAssetIn = targetAsset, + chainAssetOut = chain.utilityAsset, + amount = nativeFeeAmount, + swapDirection = SwapDirection.SPECIFIED_OUT + ) + + val quotedFee = quote(args).quote + + // TODO + // There is a issue in Router implementation in Hydra that doesn't allow asset balance to go below ED. We add it to fee for simplicity instead + // of refactoring SwapExistentialDepositAwareMaxActionProvider + // This should be removed once Router issue is fixed + val existentialDeposit = assetSourceRegistry.existentialDepositInPlanks(chain, targetAsset) + + return quotedFee + existentialDeposit + } + + private suspend fun getPaymentCurrencyToSetIfNeeded(expectedPaymentAsset: Chain.Asset, currentFeeTokenId: HydraDxAssetId): HydraDxAssetId? { + val expectedPaymentTokenId = hydraDxAssetIdConverter.toOnChainIdOrThrow(expectedPaymentAsset) + + return expectedPaymentTokenId.takeIf { currentFeeTokenId != expectedPaymentTokenId } + } + + private suspend fun ExtrinsicBuilder.executeSwap( + args: SwapExecuteArgs, + justSetFeeCurrency: HydraDxAssetId?, + previousFeeCurrency: HydraDxAssetId + ) { + maybeSetReferral() + + addSwapCall(args) + + setFeeCurrencyToNative(justSetFeeCurrency, previousFeeCurrency) + } + + private suspend fun ExtrinsicBuilder.addSwapCall(args: SwapExecuteArgs) { + val sourceForOptimizedTrade = args.path.checkForOptimizedTrade() + + if (sourceForOptimizedTrade != null) { + with(sourceForOptimizedTrade) { + executeSwap(args) + } + } else { + executeRouterSwap(args) + } + } + + private fun ExtrinsicBuilder.setFeeCurrencyToNative(justSetFeeCurrency: HydraDxAssetId?, previousFeeCurrency: HydraDxAssetId) { + val justSetFeeToNonNative = justSetFeeCurrency != null && justSetFeeCurrency != hydraDxAssetIdConverter.systemAssetId + val previousCurrencyRemainsNonNative = justSetFeeCurrency == null && previousFeeCurrency != hydraDxAssetIdConverter.systemAssetId + + if (justSetFeeToNonNative || previousCurrencyRemainsNonNative) { + setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) + } + } + + private suspend fun ExtrinsicBuilder.executeRouterSwap(args: SwapExecuteArgs) { + when (val limit = args.swapLimit) { + is SwapLimit.SpecifiedIn -> executeRouterSell(args, limit) + is SwapLimit.SpecifiedOut -> executeRouterBuy(args, limit) + } + } + + private suspend fun ExtrinsicBuilder.executeRouterBuy(args: SwapExecuteArgs, limit: SwapLimit.SpecifiedOut) { + call( + moduleName = Modules.ROUTER, + callName = "buy", + arguments = mapOf( + "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetIn), + "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetOut), + "amount_out" to limit.expectedAmountOut, + "max_amount_in" to limit.amountInMax, + "route" to args.path.convertToRouterTrade() + ) + ) + } + + private suspend fun ExtrinsicBuilder.executeRouterSell(args: SwapExecuteArgs, limit: SwapLimit.SpecifiedIn) { + call( + moduleName = Modules.ROUTER, + callName = "sell", + arguments = mapOf( + "asset_in" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetIn), + "asset_out" to hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetOut), + "amount_in" to limit.expectedAmountIn, + "min_amount_out" to limit.amountOutMin, + "route" to args.path.convertToRouterTrade() + ) + ) + } + + private suspend fun QuotePath.convertToRouterTrade(): List { + return segments.map { segment -> + val source = swapSources.firstById(segment.sourceId) + + structOf( + "pool" to source.routerPoolTypeFor(segment.sourceParams), + "assetIn" to hydraDxAssetIdConverter.toOnChainIdOrThrow(segment.from), + "assetOut" to hydraDxAssetIdConverter.toOnChainIdOrThrow(segment.to) + ) + } + } + + private suspend fun HydraDxAssetIdConverter.toOnChainIdOrThrow(localId: FullChainAssetId): HydraDxAssetId { + val chainAsset = chain.assetsById.getValue(localId.assetId) + + return toOnChainIdOrThrow(chainAsset) + } + + private suspend fun ExtrinsicBuilder.maybeSetReferral() { + val referralState = userReferralState.first() + + if (referralState == ReferralState.NOT_SET) { + val novaReferralCode = hydraDxNovaReferral.getNovaReferralCode() + + linkCode(novaReferralCode) + } + } + + private fun ExtrinsicBuilder.linkCode(referralCode: String) { + call( + moduleName = Modules.REFERRALS, + callName = "link_code", + arguments = mapOf( + "code" to referralCode.encodeToByteArray() + ) + ) + } + + private fun ExtrinsicBuilder.setFeeCurrency(onChainId: HydraDxAssetId) { + call( + moduleName = Modules.MULTI_TRANSACTION_PAYMENT, + callName = "set_currency", + arguments = mapOf( + "currency" to onChainId + ) + ) + } + + private inline fun pathsFromCacheOrCompute( + from: FullChainAssetId, + to: FullChainAssetId, + computation: () -> List> + ): List> { + val mapKey = from to to + val cachedMap = quotePathsCache.value.orEmpty() + val cachedValue = cachedMap[mapKey] + + if (cachedValue != null) { + return cachedValue.paths + } + + val computedPaths = computation() + + val updatedMap = cachedMap + (mapKey to QuotePathsCache(computedPaths)) + quotePathsCache.value = updatedMap + + return computedPaths + } + + private enum class ReferralState { + SET, NOT_SET, NOT_AVAILABLE + } + + private class QuotePathsCache( + val paths: List> + ) + + private fun HydraDxSwapEdge.swapSource(): HydraDxSwapSource { + return swapSources.firstById(sourceId) + } + + private fun QuotePath.checkForOptimizedTrade(): HydraDxSwapSource? { + if (segments.size != 1) return null + + val onlySegment = segments.single() + + return if (onlySegment.canOptimizeSingleSegmentTrade()) { + swapSources.findOmniPool() + } else { + null + } + } + + private fun QuotePath.Segment.canOptimizeSingleSegmentTrade(): Boolean { + return sourceId == OmniPoolSwapSourceFactory.SOURCE_ID + } + + private fun Iterable.findOmniPool(): HydraDxSwapSource { + return firstById(OmniPoolSwapSourceFactory.SOURCE_ID) + } + + private fun createSources(): List { + return swapSourceFactories.map { it.create(chain) } + } + + private suspend fun logQuotes(args: AssetExchangeQuoteArgs, quotes: List) { + val allCandidates = quotes.sortedDescending().map { + val formattedIn = args.amount.formatPlanks(args.chainAssetIn) + val formattedOut = it.quote.formatPlanks(args.chainAssetOut) + val formattedPath = formatPath(it.path) + + "$formattedIn to $formattedOut via $formattedPath" + }.joinToString(separator = "\n") + + Log.d("RealSwapService", "-------- New quote ----------") + Log.d("RealSwapService", allCandidates) + Log.d("RealSwapService", "-------- Done quote ----------\n\n\n") + } + + private suspend fun formatPath(path: QuotePath): String { + val assets = chain.assetsById + + return buildString { + val firstSegment = path.segments.first() + + append(assets.getValue(firstSegment.from.assetId).symbol) + + append(" -- ${formatSource(firstSegment)} --> ") + + append(assets.getValue(firstSegment.to.assetId).symbol) + + path.segments.subList(1, path.segments.size).onEach { segment -> + append(" -- ${formatSource(segment)} --> ") + + append(assets.getValue(segment.to.assetId).symbol) + } + } + } + + private suspend fun formatSource(segment: QuotePath.Segment): String { + return buildString { + append(segment.sourceId) + + val stableswapPoolId = segment.sourceParams["PoolId"] + if (stableswapPoolId != null) { + val onChainId = stableswapPoolId.toBigInteger() + val chainAsset = hydraDxAssetIdConverter.toChainAssetOrThrow(chain, onChainId) + + append("[${chainAsset.symbol}]") + } + } + } +} + +private class HydraDxSwapEdge( + override val from: FullChainAssetId, + val sourceId: HydraDxSwapSourceId, + val direction: HydraSwapDirection +) : Edge { + + override val to: FullChainAssetId = direction.to +} + +private fun Path.toQuotePath(): QuotePath { + val segments = map { + QuotePath.Segment(it.from, it.to, it.sourceId, it.direction.params) + } + + return QuotePath(segments) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt new file mode 100644 index 0000000000..8c4960ee68 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/HydraDxSwapSource.kt @@ -0,0 +1,56 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx + +import io.novafoundation.nova.common.utils.Identifiable +import io.novafoundation.nova.common.utils.MultiMapList +import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import kotlinx.coroutines.flow.Flow + +typealias HydraDxSwapSourceId = String + +interface HydraDxSwapSource : Identifiable { + + suspend fun availableSwapDirections(): MultiMapList + + suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) + + @Throws(SwapQuoteException::class) + suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance + + suspend fun runSubscriptions( + userAccountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow + + fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> + + interface Factory { + + fun create(chain: Chain): HydraDxSwapSource + } +} + +data class HydraDxSwapSourceQuoteArgs( + val chainAssetIn: Chain.Asset, + val chainAssetOut: Chain.Asset, + val amount: Balance, + val swapDirection: SwapDirection, + val params: Map +) + +interface HydraSwapDirection { + + val from: FullChainAssetId + + val to: FullChainAssetId + + val params: Map +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/MultiTransactionPaymentApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/MultiTransactionPaymentApi.kt new file mode 100644 index 0000000000..ff7a0d94c8 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/MultiTransactionPaymentApi.kt @@ -0,0 +1,36 @@ +@file:Suppress("RedundantUnitExpression") + +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx + +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.utils.multiTransactionPayment +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +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 jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +@JvmInline +value class MultiTransactionPaymentApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.multiTransactionPayment: MultiTransactionPaymentApi + get() = MultiTransactionPaymentApi(multiTransactionPayment()) + +context(StorageQueryContext) +val MultiTransactionPaymentApi.acceptedCurrencies: QueryableStorageEntry1 + get() = storage1( + name = "AcceptedCurrencies", + binding = { decoded, _ -> bindNumber(decoded) }, + ) + +context(StorageQueryContext) +val MultiTransactionPaymentApi.accountCurrencyMap: QueryableStorageEntry1 + get() = storage1( + name = "AccountCurrencyMap", + binding = { decoded, _ -> bindNumber(decoded) }, + ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/DynamicFeesApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/DynamicFeesApi.kt new file mode 100644 index 0000000000..0a7ceb1afd --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/DynamicFeesApi.kt @@ -0,0 +1,28 @@ +@file:Suppress("RedundantUnitExpression") + +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool + +import io.novafoundation.nova.common.utils.dynamicFees +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.DynamicFee +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.bindDynamicFee +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +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 jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +@JvmInline +value class DynamicFeesApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.dynamicFeesApi: DynamicFeesApi + get() = DynamicFeesApi(dynamicFees()) + +context(StorageQueryContext) +val DynamicFeesApi.assetFee: QueryableStorageEntry1 + get() = storage1( + name = "AssetFee", + binding = { decoded, _ -> bindDynamicFee(decoded) }, + ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt new file mode 100644 index 0000000000..110508e206 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmniPoolSwapSource.kt @@ -0,0 +1,290 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool + +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.common.utils.MultiMapList +import io.novafoundation.nova.common.utils.dynamicFees +import io.novafoundation.nova.common.utils.numberConstant +import io.novafoundation.nova.common.utils.omnipool +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.common.utils.padEnd +import io.novafoundation.nova.common.utils.singleReplaySharedFlow +import io.novafoundation.nova.common.utils.toMultiSubscription +import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapLimit +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceId +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceQuoteArgs +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraSwapDirection +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.DynamicFee +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPool +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPoolFees +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmniPoolToken +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmnipoolAssetState +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.feeParamsConstant +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.quote +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.multiNetwork.getRuntime +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.api.observeNonNull +import io.novafoundation.nova.runtime.storage.source.query.metadata +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import java.math.BigInteger + +class OmniPoolSwapSourceFactory( + private val remoteStorageSource: StorageDataSource, + private val chainRegistry: ChainRegistry, + private val assetSourceRegistry: AssetSourceRegistry, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, +) : HydraDxSwapSource.Factory { + + companion object { + + const val SOURCE_ID = "OmniPool" + } + + override fun create(chain: Chain): HydraDxSwapSource { + return OmniPoolSwapSource( + remoteStorageSource = remoteStorageSource, + chainRegistry = chainRegistry, + assetSourceRegistry = assetSourceRegistry, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + chain = chain + ) + } +} + +private class OmniPoolSwapSource( + private val remoteStorageSource: StorageDataSource, + private val chainRegistry: ChainRegistry, + private val assetSourceRegistry: AssetSourceRegistry, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val chain: Chain, +) : HydraDxSwapSource { + + override val identifier: HydraDxSwapSourceId = OmniPoolSwapSourceFactory.SOURCE_ID + + private val pooledOnChainAssetIdsState: MutableSharedFlow> = singleReplaySharedFlow() + + private val omniPoolFlow: MutableSharedFlow = singleReplaySharedFlow() + + override suspend fun availableSwapDirections(): MultiMapList { + val pooledOnChainAssetIds = getPooledOnChainAssetIds() + + val pooledChainAssetsIds = matchKnownChainAssetIds(pooledOnChainAssetIds) + pooledOnChainAssetIdsState.emit(pooledChainAssetsIds) + + return pooledChainAssetsIds.associateBy( + keySelector = { it.second }, + valueTransform = { (_, currentId) -> + // In OmniPool, each asset is tradable with any other except itself + pooledChainAssetsIds.mapNotNull { (_, otherId) -> + otherId.takeIf { currentId != otherId }?.let { OmniPoolSwapDirection(currentId, otherId) } + } + } + ) + } + + override suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) { + val assetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetIn) + val assetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.assetOut) + + when (val limit = args.swapLimit) { + is SwapLimit.SpecifiedIn -> sell( + assetIdIn = assetIdIn, + assetIdOut = assetIdOut, + amountIn = limit.expectedAmountIn, + minBuyAmount = limit.amountOutMin + ) + is SwapLimit.SpecifiedOut -> buy( + assetIdIn = assetIdIn, + assetIdOut = assetIdOut, + amountOut = limit.expectedAmountOut, + maxSellAmount = limit.amountInMax + ) + } + } + + override suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance { + val omniPool = omniPoolFlow.first() + + val omniPoolTokenIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) + val omniPoolTokenIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) + + return omniPool.quote(omniPoolTokenIdIn, omniPoolTokenIdOut, args.amount, args.swapDirection) + ?: throw SwapQuoteException.NotEnoughLiquidity + } + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun runSubscriptions( + userAccountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow { + omniPoolFlow.resetReplayCache() + + val pooledAssets = pooledOnChainAssetIdsState.first() + + val omniPoolStateFlow = pooledAssets.map { (onChainId, _) -> + remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { + metadata.omnipool.assets.observeNonNull(onChainId).map { + onChainId to it + } + } + } + .toMultiSubscription(pooledAssets.size) + + val poolAccountId = omniPoolAccountId() + + val omniPoolBalancesFlow = pooledAssets.map { (omniPoolTokenId, chainAssetId) -> + val chainAsset = chain.assetsById.getValue(chainAssetId.assetId) + val assetSource = assetSourceRegistry.sourceFor(chainAsset) + assetSource.balance.subscribeTransferableAccountBalance(chain, chainAsset, poolAccountId, subscriptionBuilder).map { + omniPoolTokenId to it + } + } + .toMultiSubscription(pooledAssets.size) + + val feesFlow = pooledAssets.map { (omniPoolTokenId, _) -> + remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { + metadata.dynamicFeesApi.assetFee.observe(omniPoolTokenId).map { + omniPoolTokenId to it + } + } + }.toMultiSubscription(pooledAssets.size) + + val defaultFees = getDefaultFees() + + return combine(omniPoolStateFlow, omniPoolBalancesFlow, feesFlow) { poolState, poolBalances, fees -> + createOmniPool(poolState, poolBalances, fees, defaultFees) + } + .onEach(omniPoolFlow::emit) + .map { } + } + + override fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> { + return DictEnum.Entry("Omnipool", null) + } + + private suspend fun getPooledOnChainAssetIds(): List { + return remoteStorageSource.query(chain.id) { + val hubAssetId = metadata.omnipool().numberConstant("HubAssetId", runtime) + val allAssets = runtime.metadata.omnipoolOrNull?.assets?.keys().orEmpty() + + // remove hubAssetId from trading paths + allAssets.filter { it != hubAssetId } + } + } + + private suspend fun matchKnownChainAssetIds(onChainIds: List): List { + val hydraDxAssetIds = hydraDxAssetIdConverter.allOnChainIds(chain) + + return onChainIds.mapNotNull { onChainId -> + val asset = hydraDxAssetIds[onChainId] ?: return@mapNotNull null + + onChainId to asset.fullId + } + } + + private fun createOmniPool( + poolAssetStates: Map, + poolBalances: Map, + fees: Map, + defaultFees: OmniPoolFees, + ): OmniPool { + val tokensState = poolAssetStates.mapValues { (tokenId, poolAssetState) -> + val assetBalance = poolBalances[tokenId].orZero() + val tokenFees = fees[tokenId]?.let { OmniPoolFees(it.protocolFee, it.assetFee) } ?: defaultFees + + OmniPoolToken( + hubReserve = poolAssetState.hubReserve, + shares = poolAssetState.shares, + protocolShares = poolAssetState.protocolShares, + tradeability = poolAssetState.tradeability, + balance = assetBalance, + fees = tokenFees + ) + } + + return OmniPool(tokensState) + } + + private fun ExtrinsicBuilder.sell( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountIn: Balance, + minBuyAmount: Balance + ) { + call( + moduleName = Modules.OMNIPOOL, + callName = "sell", + arguments = mapOf( + "asset_in" to assetIdIn, + "asset_out" to assetIdOut, + "amount" to amountIn, + "min_buy_amount" to minBuyAmount + ) + ) + } + + private fun ExtrinsicBuilder.buy( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountOut: Balance, + maxSellAmount: Balance + ) { + call( + moduleName = Modules.OMNIPOOL, + callName = "buy", + arguments = mapOf( + "asset_out" to assetIdOut, + "asset_in" to assetIdIn, + "amount" to amountOut, + "max_sell_amount" to maxSellAmount + ) + ) + } + + private suspend fun getDefaultFees(): OmniPoolFees { + val runtime = chainRegistry.getRuntime(chain.id) + + val assetFeeParams = runtime.metadata.dynamicFees().feeParamsConstant("AssetFeeParameters", runtime) + val protocolFeeParams = runtime.metadata.dynamicFees().feeParamsConstant("ProtocolFeeParameters", runtime) + + return OmniPoolFees( + protocolFee = protocolFeeParams.minFee, + assetFee = assetFeeParams.minFee + ) + } + + private class OmniPoolSwapDirection(override val from: FullChainAssetId, override val to: FullChainAssetId) : HydraSwapDirection { + + override val params: Map + get() = emptyMap() + } +} + +fun omniPoolAccountId(): AccountId { + return "modlomnipool".encodeToByteArray().padEnd(expectedSize = 32, padding = 0) +} + +typealias RemoteAndLocalId = Pair + +typealias RemoteAndLocalIdOptional = Pair diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmnipoolApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmnipoolApi.kt new file mode 100644 index 0000000000..668938d742 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/OmnipoolApi.kt @@ -0,0 +1,33 @@ +@file:Suppress("RedundantUnitExpression") + +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool + +import io.novafoundation.nova.common.utils.omnipool +import io.novafoundation.nova.common.utils.omnipoolOrNull +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.OmnipoolAssetState +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model.bindOmnipoolAssetState +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +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 jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +@JvmInline +value class OmnipoolApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.omnipoolOrNull: OmnipoolApi? + get() = omnipoolOrNull()?.let(::OmnipoolApi) + +context(StorageQueryContext) +val RuntimeMetadata.omnipool: OmnipoolApi + get() = OmnipoolApi(omnipool()) + +context(StorageQueryContext) +val OmnipoolApi.assets: QueryableStorageEntry1 + get() = storage1( + name = "Assets", + binding = ::bindOmnipoolAssetState, + ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/DynamicFee.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/DynamicFee.kt new file mode 100644 index 0000000000..e0b04d7716 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/DynamicFee.kt @@ -0,0 +1,39 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model + +import io.novafoundation.nova.common.data.network.runtime.binding.bindPermill +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.common.utils.Perbill +import io.novafoundation.nova.common.utils.constant +import io.novafoundation.nova.common.utils.decoded +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +class DynamicFee( + val assetFee: Perbill, + val protocolFee: Perbill +) + +fun bindDynamicFee(decoded: Any): DynamicFee { + val asStruct = decoded.castToStruct() + + return DynamicFee( + assetFee = bindPermill(asStruct["assetFee"]), + protocolFee = bindPermill(asStruct["protocolFee"]), + ) +} + +class FeeParams( + val minFee: Perbill, +) + +fun bindFeeParams(decoded: Any?): FeeParams { + val asStruct = decoded.castToStruct() + + return FeeParams( + minFee = bindPermill(asStruct["minFee"]), + ) +} + +fun Module.feeParamsConstant(name: String, runtimeSnapshot: RuntimeSnapshot): FeeParams { + return bindFeeParams(constant(name).decoded(runtimeSnapshot)) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/OmniPool.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/OmniPool.kt new file mode 100644 index 0000000000..64805b3a8a --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/OmniPool.kt @@ -0,0 +1,105 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model + +import io.novafoundation.nova.common.utils.Perbill +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import kotlin.math.floor + +class OmniPool( + val tokens: Map, +) + +class OmniPoolFees( + val protocolFee: Perbill, + val assetFee: Perbill +) + +class OmniPoolToken( + val hubReserve: Balance, + val shares: Balance, + val protocolShares: Balance, + val tradeability: Tradeability, + val balance: Balance, + val fees: OmniPoolFees +) + +fun OmniPool.quote( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amount: Balance, + direction: SwapDirection +): Balance? { + return when (direction) { + SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdIn, assetIdOut, amount) + SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdIn, assetIdOut, amount) + } +} + +fun OmniPool.calculateOutGivenIn( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountIn: Balance +): Balance { + val tokenInState = tokens.getValue(assetIdIn) + val tokenOutState = tokens.getValue(assetIdOut) + + val protocolFee = tokenInState.fees.protocolFee + val assetFee = tokenOutState.fees.assetFee + + val inHubReserve = tokenInState.hubReserve.toDouble() + val inReserve = tokenInState.balance.toDouble() + + val inAmount = amountIn.toDouble() + + val deltaHubReserveIn = inAmount * inHubReserve / (inReserve + inAmount) + + val protocolFeeAmount = floor(protocolFee.value * deltaHubReserveIn) + + val deltaHubReserveOut = deltaHubReserveIn - protocolFeeAmount + + val outReserveHp = tokenOutState.balance.toDouble() + val outHubReserveHp = tokenOutState.hubReserve.toDouble() + + val deltaReserveOut = outReserveHp * deltaHubReserveOut / (outHubReserveHp + deltaHubReserveOut) + val amountOut = deltaReserveOut.deductFraction(assetFee) + + return amountOut.toBigDecimal().toBigInteger() +} + +fun OmniPool.calculateInGivenOut( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amountOut: Balance +): Balance? { + val tokenInState = tokens.getValue(assetIdIn) + val tokenOutState = tokens.getValue(assetIdOut) + + val protocolFee = tokenInState.fees.protocolFee + val assetFee = tokenOutState.fees.assetFee + + val outHubReserve = tokenOutState.hubReserve.toDouble() + val outReserve = tokenOutState.balance.toDouble() + + val outAmount = amountOut.toDouble() + + val outReserveNoFee = outReserve.deductFraction(assetFee) + + val deltaHubReserveOut = outHubReserve * outAmount / (outReserveNoFee - outAmount) + 1 + + val deltaHubReserveIn = deltaHubReserveOut / (1.0 - protocolFee.value) + + val inHubReserveHp = tokenInState.hubReserve.toDouble() + + if (deltaHubReserveIn >= inHubReserveHp) { + return null + } + + val inReserveHp = tokenInState.balance.toDouble() + + val deltaReserveIn = inReserveHp * deltaHubReserveIn / (inHubReserveHp - deltaHubReserveIn) + 1 + + return deltaReserveIn.takeIf { it >= 0 }?.toBigDecimal()?.toBigInteger() +} + +private fun Double.deductFraction(perbill: Perbill): Double = this - this * perbill.value diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/OmnipoolAssetState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/OmnipoolAssetState.kt new file mode 100644 index 0000000000..6f6fd43db7 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/OmnipoolAssetState.kt @@ -0,0 +1,26 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model + +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance + +class OmnipoolAssetState( + val tokenId: HydraDxAssetId, + val hubReserve: Balance, + val shares: Balance, + val protocolShares: Balance, + val tradeability: Tradeability +) + +fun bindOmnipoolAssetState(decoded: Any?, tokenId: HydraDxAssetId): OmnipoolAssetState { + val struct = decoded.castToStruct() + + return OmnipoolAssetState( + tokenId = tokenId, + hubReserve = bindNumber(struct["hubReserve"]), + shares = bindNumber(struct["shares"]), + protocolShares = bindNumber(struct["protocolShares"]), + tradeability = bindTradeability(struct["tradable"]) + ) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/Tradeability.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/Tradeability.kt new file mode 100644 index 0000000000..fe6af8b274 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/omnipool/model/Tradeability.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.model + +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import java.math.BigInteger + +@JvmInline +value class Tradeability(val value: BigInteger) { + + companion object { + // / Asset is allowed to be sold into omnipool + val SELL = 0b0000_0001.toBigInteger() + + // / Asset is allowed to be bought into omnipool + val BUY = 0b0000_0010.toBigInteger() + } + + fun canBuy(): Boolean = flagEnabled(BUY) + + fun canSell(): Boolean = flagEnabled(SELL) + + private fun flagEnabled(flag: BigInteger) = value and flag == flag +} + +fun bindTradeability(value: Any?): Tradeability { + val asStruct = value.castToStruct() + + return Tradeability(bindNumber(asStruct["bits"])) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/referrals/HydraDxNovaReferral.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/referrals/HydraDxNovaReferral.kt new file mode 100644 index 0000000000..65132a48fc --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/referrals/HydraDxNovaReferral.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx + +interface HydraDxNovaReferral { + + fun getNovaReferralCode(): String +} + +class RealHydraDxNovaReferral : HydraDxNovaReferral { + + override fun getNovaReferralCode(): String { + return "NOVA" + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/referrals/ReferralsApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/referrals/ReferralsApi.kt new file mode 100644 index 0000000000..f8cfd5ce07 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/referrals/ReferralsApi.kt @@ -0,0 +1,27 @@ +@file:Suppress("RedundantUnitExpression") + +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.referrals + +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountId +import io.novafoundation.nova.common.utils.referralsOrNull +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 jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +@JvmInline +value class ReferralsApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.referralsOrNull: ReferralsApi? + get() = referralsOrNull()?.let(::ReferralsApi) + +context(StorageQueryContext) +val ReferralsApi.linkedAccounts: QueryableStorageEntry1 + get() = storage1( + name = "LinkedAccounts", + binding = { decoded, _ -> bindAccountId(decoded) }, + ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/AssetRegistryApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/AssetRegistryApi.kt new file mode 100644 index 0000000000..8fe69f9ec3 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/AssetRegistryApi.kt @@ -0,0 +1,30 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap + +import io.novafoundation.nova.common.data.network.runtime.binding.bindInt +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.common.utils.assetRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +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 jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +@JvmInline +value class AssetRegistryApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.assetRegistry: AssetRegistryApi + get() = AssetRegistryApi(assetRegistry()) + +context(StorageQueryContext) +val AssetRegistryApi.assetMetadataMap: QueryableStorageEntry1 + get() = storage1( + name = "AssetMetadataMap", + binding = { decoded, _ -> bindMetadataDecimals(decoded) }, + ) + +private fun bindMetadataDecimals(decoded: Any): Int { + return bindInt(decoded.castToStruct()["decimals"]) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapApi.kt new file mode 100644 index 0000000000..aa4bb0a50e --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapApi.kt @@ -0,0 +1,31 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap + +import io.novafoundation.nova.common.utils.stableSwap +import io.novafoundation.nova.common.utils.stableSwapOrNull +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StableSwapPoolInfo +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.bindStablePoolInfo +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +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 jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +@JvmInline +value class StableSwapApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.stableSwapOrNull: StableSwapApi? + get() = stableSwapOrNull()?.let(::StableSwapApi) + +context(StorageQueryContext) +val RuntimeMetadata.stableSwap: StableSwapApi + get() = StableSwapApi(stableSwap()) + +context(StorageQueryContext) +val StableSwapApi.pools: QueryableStorageEntry1 + get() = storage1( + name = "Pools", + binding = ::bindStablePoolInfo, + ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt new file mode 100644 index 0000000000..8fde5b6dd5 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/StableSwapSource.kt @@ -0,0 +1,311 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap + +import com.google.gson.Gson +import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber +import io.novafoundation.nova.common.data.network.runtime.binding.orEmpty +import io.novafoundation.nova.common.utils.MultiMapList +import io.novafoundation.nova.common.utils.graph.Edge +import io.novafoundation.nova.common.utils.graph.Graph +import io.novafoundation.nova.common.utils.graph.create +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.common.utils.singleReplaySharedFlow +import io.novafoundation.nova.common.utils.toMultiSubscription +import io.novafoundation.nova.core.updater.SharedRequestsBuilder +import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs +import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteException +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSourceQuoteArgs +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraSwapDirection +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.RemoteAndLocalIdOptional +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.omniPoolAccountId +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StablePool +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StablePoolAsset +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.StableSwapPoolInfo +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model.quote +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.toOnChainIdOrThrow +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable +import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId +import io.novafoundation.nova.runtime.repository.ChainStateRepository +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.metadata +import jp.co.soramitsu.fearless_utils.encrypt.json.asLittleEndianBytes +import jp.co.soramitsu.fearless_utils.hash.Hasher.blake2b256 +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum +import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +private const val POOL_ID_PARAM_KEY = "PoolId" + +class StableSwapSourceFactory( + private val remoteStorageSource: StorageDataSource, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val gson: Gson, + private val chainStateRepository: ChainStateRepository +) : HydraDxSwapSource.Factory { + + override fun create(chain: Chain): HydraDxSwapSource { + return StableSwapSource( + remoteStorageSource = remoteStorageSource, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + chain = chain, + gson = gson, + chainStateRepository = chainStateRepository + ) + } +} + +private class StableSwapSource( + private val remoteStorageSource: StorageDataSource, + private val hydraDxAssetIdConverter: HydraDxAssetIdConverter, + private val chain: Chain, + private val gson: Gson, + private val chainStateRepository: ChainStateRepository, +) : HydraDxSwapSource { + + override val identifier: String = "StableSwap" + + private val initialPoolsInfo: MutableSharedFlow> = singleReplaySharedFlow() + + private val stablePools: MutableSharedFlow> = singleReplaySharedFlow() + + override suspend fun availableSwapDirections(): MultiMapList { + val pools = getPools() + + val poolInitialInfo = pools.matchIdsWithLocal() + initialPoolsInfo.emit(poolInitialInfo) + + return poolInitialInfo.allPossibleDirections() + } + + override suspend fun ExtrinsicBuilder.executeSwap(args: SwapExecuteArgs) { + // We don't need a specific implementation for StableSwap extrinsics since it is done by HydraDxExchange on the upper level via Router + } + + override suspend fun quote(args: HydraDxSwapSourceQuoteArgs): Balance { + val allPools = stablePools.first() + val poolId = args.params.poolIdParam() + val relevantPool = allPools.first { it.sharedAsset.id == poolId } + + val hydraDxAssetIdIn = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetIn) + val hydraDxAssetIdOut = hydraDxAssetIdConverter.toOnChainIdOrThrow(args.chainAssetOut) + + return relevantPool.quote(hydraDxAssetIdIn, hydraDxAssetIdOut, args.amount, args.swapDirection) + ?: throw SwapQuoteException.NotEnoughLiquidity + } + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun runSubscriptions( + userAccountId: AccountId, + subscriptionBuilder: SharedRequestsBuilder + ): Flow = coroutineScope { + stablePools.resetReplayCache() + + val initialPoolsInfo = initialPoolsInfo.first() + + val poolInfoSubscriptions = initialPoolsInfo.map { poolInfo -> + remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { + runtime.metadata.stableSwap.pools.observe(poolInfo.sharedAsset.first).map { + poolInfo.sharedAsset.first to it + } + } + }.toMultiSubscription(initialPoolsInfo.size) + + val omniPoolAccountId = omniPoolAccountId() + + val poolSharedAssetBalanceSubscriptions = initialPoolsInfo.map { poolInfo -> + val sharedAssetRemoteId = poolInfo.sharedAsset.first + + subscribeTransferableBalance(subscriptionBuilder, omniPoolAccountId, sharedAssetRemoteId).map { + sharedAssetRemoteId to it + } + }.toMultiSubscription(initialPoolsInfo.size) + + val totalPooledAssets = initialPoolsInfo.sumOf { it.poolAssets.size } + + val poolParticipatingAssetsBalanceSubscription = initialPoolsInfo.flatMap { poolInfo -> + val poolAccountId = stableSwapPoolAccountId(poolInfo.sharedAsset.first) + + poolInfo.poolAssets.map { poolAsset -> + subscribeTransferableBalance(subscriptionBuilder, poolAccountId, poolAsset.first).map { + val key = poolInfo.sharedAsset.first to poolAsset.first + key to it + } + } + }.toMultiSubscription(totalPooledAssets) + + val totalIssuanceSubscriptions = initialPoolsInfo.map { poolInfo -> + remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { + runtime.metadata.hydraTokens.totalIssuance.observe(poolInfo.sharedAsset.first).map { + poolInfo.sharedAsset.first to it.orZero() + } + } + }.toMultiSubscription(initialPoolsInfo.size) + + val precisions = fetchAssetsPrecisionsAsync() + + combine( + poolInfoSubscriptions, + poolSharedAssetBalanceSubscriptions, + poolParticipatingAssetsBalanceSubscription, + totalIssuanceSubscriptions, + chainStateRepository.currentBlockNumberFlow(chain.id), + ) { poolInfos, poolSharedAssetBalances, poolParticipatingAssetBalances, totalIssuances, currentBlock -> + createStableSwapPool(poolInfos, poolSharedAssetBalances, poolParticipatingAssetBalances, totalIssuances, currentBlock, precisions.await()) + } + .onEach(stablePools::emit) + .map { } + } + + private suspend fun subscribeTransferableBalance(subscriptionBuilder: SharedRequestsBuilder, account: AccountId, assetId: HydraDxAssetId): Flow { + // We cant use AssetSource since it require Chain.Asset which might not always be present in case some stable pool assets are not yet in Nova configs + return remoteStorageSource.subscribe(chain.id, subscriptionBuilder) { + metadata.hydraTokens.accounts.observe(account, assetId).map { + Asset.TransferableMode.REGULAR.calculateTransferable(it.orEmpty()) + } + } + } + + private fun createStableSwapPool( + poolInfos: Map, + poolSharedAssetBalances: Map, + poolParticipatingAssetBalances: Map, Balance>, + totalIssuances: Map, + currentBlock: BlockNumber, + precisions: Map + ): List { + return poolInfos.mapNotNull outer@{ (poolId, poolInfo) -> + if (poolInfo == null) return@outer null + + val sharedAssetBalance = poolSharedAssetBalances[poolId].orZero() + val sharedChainAssetPrecision = precisions[poolId] ?: return@outer null + val sharedAsset = StablePoolAsset(sharedAssetBalance, poolId, sharedChainAssetPrecision) + val sharedAssetIssuance = totalIssuances[poolId].orZero() + + val pooledAssets = poolInfo.assets.mapNotNull { pooledAssetId -> + val pooledAssetBalance = poolParticipatingAssetBalances[poolId to pooledAssetId].orZero() + val decimals = precisions[pooledAssetId] ?: return@mapNotNull null + + StablePoolAsset(pooledAssetBalance, pooledAssetId, decimals) + } + + StablePool( + sharedAsset = sharedAsset, + assets = pooledAssets, + initialAmplification = poolInfo.initialAmplification, + finalAmplification = poolInfo.finalAmplification, + initialBlock = poolInfo.initialBlock, + finalBlock = poolInfo.finalBlock, + fee = poolInfo.fee, + sharedAssetIssuance = sharedAssetIssuance, + gson = gson, + currentBlock = currentBlock + ) + } + } + + private fun CoroutineScope.fetchAssetsPrecisionsAsync(): Deferred> { + return async { + remoteStorageSource.query(chain.id) { + metadata.assetRegistry.assetMetadataMap.entries() + } + } + } + + private fun stableSwapPoolAccountId(poolId: HydraDxAssetId): AccountId { + val prefix = "sts".encodeToByteArray() + val suffix = poolId.toInt().asLittleEndianBytes() + + return (prefix + suffix).blake2b256() + } + + override fun routerPoolTypeFor(params: Map): DictEnum.Entry<*> { + val poolId = params.getValue(POOL_ID_PARAM_KEY).toBigInteger() + + return DictEnum.Entry("Stableswap", poolId) + } + + private suspend fun getPools(): Map { + return remoteStorageSource.query(chain.id) { + runtime.metadata.stableSwapOrNull?.pools?.entries().orEmpty() + } + } + + private suspend fun Map.matchIdsWithLocal(): List { + val allOnChainIds = hydraDxAssetIdConverter.allOnChainIds(chain) + + return mapNotNull outer@{ (poolAssetId, poolInfo) -> + val poolAssetMatchedId = allOnChainIds[poolAssetId]?.fullId + + val participatingAssetsMatchedIds = poolInfo.assets.map { assetId -> + val localId = allOnChainIds[assetId]?.fullId + + assetId to localId + } + + PoolInitialInfo( + sharedAsset = poolAssetId to poolAssetMatchedId, + poolAssets = participatingAssetsMatchedIds + ) + } + } + + private fun List.allPossibleDirections(): MultiMapList { + val perPoolMaps = map { (poolAssetId, poolAssets) -> + val allPoolAssetIds = buildList { + addAll(poolAssets.mapNotNull { it.second }) + + val sharedAssetId = poolAssetId.second + + if (sharedAssetId != null) { + add(sharedAssetId) + } + } + + allPoolAssetIds.associateWith { assetId -> + allPoolAssetIds.mapNotNull { otherAssetId -> + otherAssetId.takeIf { assetId != otherAssetId } + ?.let { StableSwapDirection(assetId, otherAssetId, poolAssetId.first) } + } + } + } + + return Graph.create(perPoolMaps).adjacencyList + } + + private fun Map.poolIdParam(): HydraDxAssetId { + return getValue(POOL_ID_PARAM_KEY).toBigInteger() + } + + private class StableSwapDirection( + override val from: FullChainAssetId, + override val to: FullChainAssetId, + poolId: HydraDxAssetId + ) : HydraSwapDirection, Edge { + val poolIdRaw = poolId.toString() + + override val params: Map + get() = mapOf(POOL_ID_PARAM_KEY to poolIdRaw) + } + + private data class PoolInitialInfo( + val sharedAsset: RemoteAndLocalIdOptional, + val poolAssets: List + ) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/TokensApi.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/TokensApi.kt new file mode 100644 index 0000000000..3f30314bf5 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/TokensApi.kt @@ -0,0 +1,38 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap + +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance +import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber +import io.novafoundation.nova.common.data.network.runtime.binding.bindOrmlAccountData +import io.novafoundation.nova.common.utils.tokens +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +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.QueryableStorageEntry2 +import io.novafoundation.nova.runtime.storage.source.query.api.storage1 +import io.novafoundation.nova.runtime.storage.source.query.api.storage2 +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module + +@JvmInline +value class TokensApi(override val module: Module) : QueryableModule + +context(StorageQueryContext) +val RuntimeMetadata.hydraTokens: TokensApi + get() = TokensApi(tokens()) + +context(StorageQueryContext) +val TokensApi.totalIssuance: QueryableStorageEntry1 + get() = storage1( + name = "TotalIssuance", + binding = { decoded, _ -> bindNumber(decoded) }, + ) + +context(StorageQueryContext) +val TokensApi.accounts: QueryableStorageEntry2 + get() = storage2( + name = "Accounts", + binding = { decoded, _, _ -> bindOrmlAccountData(decoded) }, + ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StablePool.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StablePool.kt new file mode 100644 index 0000000000..38938fe033 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StablePool.kt @@ -0,0 +1,191 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber +import io.novafoundation.nova.common.utils.Perbill +import io.novafoundation.nova.common.utils.atLeastZero +import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.hydra_dx_math.stableswap.StableSwapMathBridge +import java.math.BigInteger + +class StablePool( + val sharedAsset: StablePoolAsset, + sharedAssetIssuance: Balance, + val assets: List, + val initialAmplification: BigInteger, + val finalAmplification: BigInteger, + val initialBlock: BigInteger, + val finalBlock: BigInteger, + val currentBlock: BlockNumber, + fee: Perbill, + val gson: Gson, +) { + + val sharedAssetIssuance = sharedAssetIssuance.toString() + val fee: String = fee.value.toBigDecimal().toPlainString() + + val reserves: String by lazy(LazyThreadSafetyMode.NONE) { + val reservesInput = assets.map { ReservesInput(it.balance.toString(), it.id.toInt(), it.decimals) } + gson.toJson(reservesInput) + } + + val amplification by lazy(LazyThreadSafetyMode.NONE) { + calculateAmplification() + } + + private fun calculateAmplification(): String { + return StableSwapMathBridge.calculate_amplification( + initialAmplification.toString(), + finalAmplification.toString(), + initialBlock.toString(), + finalBlock.toString(), + currentBlock.toString() + ) + } +} + +class StablePoolAsset( + val balance: Balance, + val id: HydraDxAssetId, + val decimals: Int +) + +fun StablePool.quote( + assetIdIn: HydraDxAssetId, + assetIdOut: HydraDxAssetId, + amount: Balance, + direction: SwapDirection +): Balance? { + return when (direction) { + SwapDirection.SPECIFIED_IN -> calculateOutGivenIn(assetIdIn, assetIdOut, amount) + SwapDirection.SPECIFIED_OUT -> calculateInGivenOut(assetIdIn, assetIdOut, amount) + } +} + +fun StablePool.calculateOutGivenIn( + assetIn: HydraDxAssetId, + assetOut: HydraDxAssetId, + amountIn: Balance, +): Balance? { + return when { + assetIn == sharedAsset.id -> calculateWithdrawOneAsset(assetOut, amountIn) + assetOut == sharedAsset.id -> calculateShares(assetIn, amountIn) + else -> calculateOut(assetIn, assetOut, amountIn) + } +} + +fun StablePool.calculateInGivenOut( + assetIn: HydraDxAssetId, + assetOut: HydraDxAssetId, + amountOut: Balance, +): Balance? { + return when { + assetOut == sharedAsset.id -> calculateAddOneAsset(assetIn, amountOut) + assetIn == sharedAsset.id -> calculateSharesForAmount(assetOut, amountOut) + else -> calculateIn(assetIn, assetOut, amountOut) + } +} + +private fun StablePool.calculateAddOneAsset( + assetIn: HydraDxAssetId, + amountOut: Balance, +): Balance? { + return StableSwapMathBridge.calculate_add_one_asset( + reserves, + amountOut.toString(), + assetIn.toInt(), + amplification, + sharedAssetIssuance, + fee + ).fromBridgeResultToBalance() +} + +private fun StablePool.calculateSharesForAmount( + assetOut: HydraDxAssetId, + amountOut: Balance, +): Balance? { + return StableSwapMathBridge.calculate_shares_for_amount( + reserves, + assetOut.toInt(), + amountOut.toString(), + amplification, + sharedAssetIssuance, + fee + ).fromBridgeResultToBalance() +} + +private fun StablePool.calculateIn( + assetIn: HydraDxAssetId, + assetOut: HydraDxAssetId, + amountOut: Balance, +): Balance? { + return StableSwapMathBridge.calculate_in_given_out( + reserves, + assetIn.toInt(), + assetOut.toInt(), + amountOut.toString(), + amplification, + fee + ).fromBridgeResultToBalance() +} + +private fun StablePool.calculateWithdrawOneAsset( + assetOut: HydraDxAssetId, + amountIn: Balance, +): Balance? { + return StableSwapMathBridge.calculate_liquidity_out_one_asset( + reserves, + amountIn.toString(), + assetOut.toInt(), + amplification, + sharedAssetIssuance, + fee + ).fromBridgeResultToBalance() +} + +private fun StablePool.calculateShares( + assetIn: HydraDxAssetId, + amountIn: Balance, +): Balance? { + val assets = listOf(SharesAssetInput(assetIn.toInt(), amountIn.toString())) + val assetsJson = gson.toJson(assets) + + return StableSwapMathBridge.calculate_shares( + reserves, + assetsJson, + amplification, + sharedAssetIssuance, + fee + ).fromBridgeResultToBalance() +} + +private fun StablePool.calculateOut( + assetIn: HydraDxAssetId, + assetOut: HydraDxAssetId, + amountIn: Balance, +): Balance? { + return StableSwapMathBridge.calculate_out_given_in( + this.reserves, + assetIn.toInt(), + assetOut.toInt(), + amountIn.toString(), + amplification, + fee + ).fromBridgeResultToBalance() +} + +private class SharesAssetInput(@SerializedName("asset_id") val assetId: Int, val amount: String) + +private class ReservesInput( + val amount: String, + @SerializedName("asset_id") + val id: Int, + val decimals: Int +) + +private fun String.fromBridgeResultToBalance(): Balance? { + return if (this == "-1") null else toBigInteger().atLeastZero() +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StableSwapPoolInfo.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StableSwapPoolInfo.kt new file mode 100644 index 0000000000..b56c683558 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/data/assetExchange/hydraDx/stableswap/model/StableSwapPoolInfo.kt @@ -0,0 +1,33 @@ +package io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.model + +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.bindPermill +import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct +import io.novafoundation.nova.common.utils.Perbill +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import java.math.BigInteger + +class StableSwapPoolInfo( + val poolAssetId: HydraDxAssetId, + val assets: List, + val initialAmplification: BigInteger, + val finalAmplification: BigInteger, + val initialBlock: BigInteger, + val finalBlock: BigInteger, + val fee: Perbill, +) + +fun bindStablePoolInfo(decoded: Any?, poolTokenId: HydraDxAssetId): StableSwapPoolInfo { + val struct = decoded.castToStruct() + + return StableSwapPoolInfo( + poolAssetId = poolTokenId, + assets = bindList(decoded["assets"], ::bindNumber), + initialAmplification = bindNumber(struct["initialAmplification"]), + finalAmplification = bindNumber(struct["finalAmplification"]), + initialBlock = bindNumber(struct["initialBlock"]), + finalBlock = bindNumber(struct["finalBlock"]), + fee = bindPermill(struct["fee"]) + ) +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt index a838ea6ac9..aaae6ca31f 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureDependencies.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_swap_impl.di import coil.ImageLoader +import com.google.gson.Gson import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.data.memory.ComputationalCache import io.novafoundation.nova.common.data.network.NetworkApiCreator @@ -24,6 +25,7 @@ import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixinUi import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase @@ -116,4 +118,8 @@ interface SwapFeatureDependencies { val operationDao: OperationDao val multiLocationConverterFactory: MultiLocationConverterFactory + + val hydraDxAssetIdConverter: HydraDxAssetIdConverter + + val gson: Gson } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt index 37d1b29f83..ba07b99b04 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/SwapFeatureModule.kt @@ -7,7 +7,6 @@ import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.OperationDao -import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_swap_api.domain.interactor.SwapAvailabilityInteractor @@ -15,9 +14,12 @@ import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory import io.novafoundation.nova.feature_swap_impl.data.repository.RealSwapTransactionHistoryRepository import io.novafoundation.nova.feature_swap_impl.data.repository.SwapTransactionHistoryRepository +import io.novafoundation.nova.feature_swap_impl.di.exchanges.AssetConversionExchangeModule +import io.novafoundation.nova.feature_swap_impl.di.exchanges.HydraDxExchangeModule import io.novafoundation.nova.feature_swap_impl.domain.interactor.RealSwapAvailabilityInteractor import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.domain.swap.RealSwapService @@ -32,47 +34,28 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.A import io.novafoundation.nova.feature_wallet_api.domain.interfaces.CrossChainTransfersUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.updater.AccountInfoUpdaterFactory -import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi -import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory -import io.novafoundation.nova.runtime.repository.ChainStateRepository -import io.novafoundation.nova.runtime.storage.source.StorageDataSource -import javax.inject.Named -@Module +@Module(includes = [HydraDxExchangeModule::class, AssetConversionExchangeModule::class]) class SwapFeatureModule { - @Provides - @FeatureScope - fun provideAssetConversionExchangeFactory( - chainRegistry: ChainRegistry, - @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, - runtimeCallsApi: MultiChainRuntimeCallsApi, - extrinsicService: ExtrinsicService, - assetSourceRegistry: AssetSourceRegistry, - multiLocationConverterFactory: MultiLocationConverterFactory, - ): AssetConversionExchangeFactory { - return AssetConversionExchangeFactory( - chainRegistry = chainRegistry, - remoteStorageSource = remoteStorageSource, - runtimeCallsApi = runtimeCallsApi, - extrinsicService = extrinsicService, - assetSourceRegistry = assetSourceRegistry, - multiLocationConverterFactory = multiLocationConverterFactory - ) - } - @FeatureScope @Provides fun provideSwapService( assetConversionExchangeFactory: AssetConversionExchangeFactory, + hydraDxExchangeFactory: HydraDxExchangeFactory, computationalCache: ComputationalCache, chainRegistry: ChainRegistry, accountRepository: AccountRepository ): SwapService { - return RealSwapService(assetConversionExchangeFactory, computationalCache, chainRegistry, accountRepository) + return RealSwapService( + assetConversionFactory = assetConversionExchangeFactory, + hydraDxOmnipoolFactory = hydraDxExchangeFactory, + computationalCache = computationalCache, + chainRegistry = chainRegistry, + accountRepository = accountRepository + ) } @Provides @@ -104,7 +87,6 @@ class SwapFeatureModule { chainRegistry: ChainRegistry, walletRepository: WalletRepository, accountRepository: AccountRepository, - chainStateRepository: ChainStateRepository, buyTokenRegistry: BuyTokenRegistry, crossChainTransfersUseCase: CrossChainTransfersUseCase, swapTransactionHistoryRepository: SwapTransactionHistoryRepository, @@ -112,7 +94,6 @@ class SwapFeatureModule { ): SwapInteractor { return SwapInteractor( swapService = swapService, - chainStateRepository = chainStateRepository, buyTokenRegistry = buyTokenRegistry, crossChainTransfersUseCase = crossChainTransfersUseCase, assetSourceRegistry = assetSourceRegistry, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt new file mode 100644 index 0000000000..65d7648c92 --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/AssetConversionExchangeModule.kt @@ -0,0 +1,38 @@ +package io.novafoundation.nova.feature_swap_impl.di.exchanges + +import dagger.Module +import dagger.Provides +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi +import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE +import io.novafoundation.nova.runtime.multiNetwork.multiLocation.converter.MultiLocationConverterFactory +import io.novafoundation.nova.runtime.repository.ChainStateRepository +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import javax.inject.Named + +@Module +class AssetConversionExchangeModule { + + @Provides + @FeatureScope + fun provideAssetConversionExchangeFactory( + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, + runtimeCallsApi: MultiChainRuntimeCallsApi, + extrinsicService: ExtrinsicService, + assetSourceRegistry: AssetSourceRegistry, + multiLocationConverterFactory: MultiLocationConverterFactory, + chainStateRepository: ChainStateRepository + ): AssetConversionExchangeFactory { + return AssetConversionExchangeFactory( + chainStateRepository = chainStateRepository, + remoteStorageSource = remoteStorageSource, + runtimeCallsApi = runtimeCallsApi, + extrinsicService = extrinsicService, + assetSourceRegistry = assetSourceRegistry, + multiLocationConverterFactory = multiLocationConverterFactory + ) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt new file mode 100644 index 0000000000..869f9e6a8e --- /dev/null +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/di/exchanges/HydraDxExchangeModule.kt @@ -0,0 +1,86 @@ +package io.novafoundation.nova.feature_swap_impl.di.exchanges + +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxNovaReferral +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxSwapSource +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.RealHydraDxNovaReferral +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.omnipool.OmniPoolSwapSourceFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.stableswap.StableSwapSourceFactory +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE +import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.repository.ChainStateRepository +import io.novafoundation.nova.runtime.storage.source.StorageDataSource +import javax.inject.Named + +@Module +class HydraDxExchangeModule { + + @Provides + @FeatureScope + fun provideHydraDxNovaReferral(): HydraDxNovaReferral { + return RealHydraDxNovaReferral() + } + + @Provides + @IntoSet + fun provideOmniPoolSourceFactory( + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, + chainRegistry: ChainRegistry, + assetSourceRegistry: AssetSourceRegistry, + hydraDxAssetIdConverter: HydraDxAssetIdConverter, + ): HydraDxSwapSource.Factory { + return OmniPoolSwapSourceFactory( + remoteStorageSource = remoteStorageSource, + chainRegistry = chainRegistry, + assetSourceRegistry = assetSourceRegistry, + hydraDxAssetIdConverter = hydraDxAssetIdConverter + ) + } + + @Provides + @IntoSet + fun provideStableSwapSourceFactory( + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, + hydraDxAssetIdConverter: HydraDxAssetIdConverter, + gson: Gson, + chainStateRepository: ChainStateRepository + ): HydraDxSwapSource.Factory { + return StableSwapSourceFactory( + remoteStorageSource = remoteStorageSource, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + gson = gson, + chainStateRepository = chainStateRepository + ) + } + + @Provides + @FeatureScope + fun provideHydraDxExchangeFactory( + @Named(REMOTE_STORAGE_SOURCE) remoteStorageSource: StorageDataSource, + sharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory, + extrinsicService: ExtrinsicService, + hydraDxAssetIdConverter: HydraDxAssetIdConverter, + hydraDxNovaReferral: HydraDxNovaReferral, + swapSourceFactories: Set<@JvmSuppressWildcards HydraDxSwapSource.Factory>, + assetSourceRegistry: AssetSourceRegistry, + ): HydraDxExchangeFactory { + return HydraDxExchangeFactory( + remoteStorageSource = remoteStorageSource, + sharedRequestsBuilderFactory = sharedRequestsBuilderFactory, + extrinsicService = extrinsicService, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + hydraDxNovaReferral = hydraDxNovaReferral, + swapSourceFactories = swapSourceFactories, + assetSourceRegistry = assetSourceRegistry + ) + } +} diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt index 07bde098af..fcdaa6989a 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/interactor/SwapInteractor.kt @@ -1,19 +1,19 @@ package io.novafoundation.nova.feature_swap_impl.domain.interactor -import io.novafoundation.nova.common.data.network.runtime.binding.BlockNumber import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.core.updater.UpdateSystem import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount.Type +import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_buy_api.domain.BuyTokenRegistry import io.novafoundation.nova.feature_buy_api.domain.hasProvidersFor +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.quotedBalance import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory @@ -45,18 +45,15 @@ import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId -import io.novafoundation.nova.runtime.repository.ChainStateRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext class SwapInteractor( private val swapService: SwapService, - private val chainStateRepository: ChainStateRepository, private val buyTokenRegistry: BuyTokenRegistry, private val crossChainTransfersUseCase: CrossChainTransfersUseCase, private val assetSourceRegistry: AssetSourceRegistry, @@ -123,9 +120,8 @@ class SwapInteractor( return swapService.slippageConfig(chainId) } - fun blockNumberUpdates(chainId: ChainId): Flow { - return chainStateRepository.currentBlockNumberFlow(chainId) - .drop(1) // skip immediate value from the cache to not perform double-quote on chain change + fun runSubscriptions(chainIn: Chain, metaAccount: MetaAccount): Flow { + return swapService.runSubscriptions(chainIn, metaAccount) } private fun buyAvailable(chainAssetFlow: Flow): Flow { @@ -186,7 +182,7 @@ class SwapInteractor( val nativeChainAssetIn = chainIn.commissionAsset val executeArgs = quoteArgs.toExecuteArgs( - quotedBalance = swapQuote.quotedBalance, + quote = swapQuote, customFeeAsset = feeAsset, nativeAsset = walletRepository.getAsset(metaAccount.id, nativeChainAssetIn) ?: return null ) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt index 36fb88693e..8574096247 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/domain/swap/RealSwapService.kt @@ -11,10 +11,14 @@ import io.novafoundation.nova.common.utils.filterNotNull import io.novafoundation.nova.common.utils.flatMap import io.novafoundation.nova.common.utils.flowOf import io.novafoundation.nova.common.utils.isZero +import io.novafoundation.nova.common.utils.throttleLast import io.novafoundation.nova.common.utils.toPercent +import io.novafoundation.nova.common.utils.withFlowScope import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission 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.requestedAccountPaysFees +import io.novafoundation.nova.feature_swap_api.domain.model.ReQuoteTrigger import io.novafoundation.nova.feature_swap_api.domain.model.SlippageConfig import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapExecuteArgs @@ -24,31 +28,37 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.swap.SwapService import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchange import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuote +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.AssetExchangeQuoteArgs import io.novafoundation.nova.feature_swap_impl.data.assetExchange.assetConversion.AssetConversionExchangeFactory +import io.novafoundation.nova.feature_swap_impl.data.assetExchange.hydraDx.HydraDxExchangeFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.withAmount +import io.novafoundation.nova.runtime.ext.assetConversionSupported import io.novafoundation.nova.runtime.ext.fullId +import io.novafoundation.nova.runtime.ext.hydraDxSupported import io.novafoundation.nova.runtime.ext.isCommissionAsset 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.chain.model.FullChainAssetId -import io.novafoundation.nova.runtime.multiNetwork.findChains import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import java.math.BigDecimal import kotlin.coroutines.coroutineContext +import kotlin.time.Duration.Companion.milliseconds private const val ALL_DIRECTIONS_CACHE = "RealSwapService.ALL_DIRECTIONS" private const val EXCHANGES_CACHE = "RealSwapService.EXCHANGES" internal class RealSwapService( private val assetConversionFactory: AssetConversionExchangeFactory, + private val hydraDxOmnipoolFactory: HydraDxExchangeFactory, private val computationalCache: ComputationalCache, private val chainRegistry: ChainRegistry, private val accountRepository: AccountRepository, @@ -80,20 +90,27 @@ internal class RealSwapService( } override suspend fun quote(args: SwapQuoteArgs): Result { - val computationScope = CoroutineScope(coroutineContext) - - return runCatching { - val exchange = exchanges(computationScope).getValue(args.tokenIn.configuration.chainId) - val quote = exchange.quote(args) - - val (amountIn, amountOut) = args.inAndOutAmounts(quote) - - SwapQuote( - amountIn = args.tokenIn.configuration.withAmount(amountIn), - amountOut = args.tokenOut.configuration.withAmount(amountOut), - direction = args.swapDirection, - priceImpact = args.calculatePriceImpact(amountIn, amountOut), - ) + return withContext(Dispatchers.Default) { + runCatching { + val exchange = exchanges(this).getValue(args.tokenIn.configuration.chainId) + val quoteArgs = AssetExchangeQuoteArgs( + chainAssetIn = args.tokenIn.configuration, + chainAssetOut = args.tokenOut.configuration, + amount = args.amount, + swapDirection = args.swapDirection + ) + val quote = exchange.quote(quoteArgs) + + val (amountIn, amountOut) = args.inAndOutAmounts(quote) + + SwapQuote( + amountIn = args.tokenIn.configuration.withAmount(amountIn), + amountOut = args.tokenOut.configuration.withAmount(amountOut), + direction = args.swapDirection, + priceImpact = args.calculatePriceImpact(amountIn, amountOut), + path = quote.path + ) + } } } @@ -119,6 +136,13 @@ internal class RealSwapService( return exchanges[chainId]?.slippageConfig() } + override fun runSubscriptions(chainIn: Chain, metaAccount: MetaAccount): Flow { + return withFlowScope { scope -> + val exchanges = exchanges(scope) + exchanges.getValue(chainIn.id).runSubscriptions(chainIn, metaAccount) + }.throttleLast(500.milliseconds) + } + private fun SwapQuoteArgs.calculatePriceImpact(amountIn: Balance, amountOut: Balance): Percent { val fiatIn = tokenIn.planksToFiat(amountIn) val fiatOut = tokenOut.planksToFiat(amountOut) @@ -150,7 +174,7 @@ internal class RealSwapService( .catch { emit(emptyMap()) - Log.d("RealSwapService", "Failed to fetch directions for exchange ${exchange::class} in chain $chainId") + Log.e("RealSwapService", "Failed to fetch directions for exchange ${exchange::class} in chain $chainId", it) } } @@ -167,8 +191,19 @@ internal class RealSwapService( } private suspend fun createExchanges(coroutineScope: CoroutineScope): Map { - return chainRegistry.findChains { it.swap.isNotEmpty() } - .associateBy(Chain::id) { assetConversionFactory.create(it.id, coroutineScope) } + return chainRegistry.chainsById.first().mapValues { (_, chain) -> + createExchange(coroutineScope, chain) + } .filterNotNull() } + + private suspend fun createExchange(computationScope: CoroutineScope, chain: Chain): AssetExchange? { + val factory = when { + chain.swap.assetConversionSupported() -> assetConversionFactory + chain.swap.hydraDxSupported() -> hydraDxOmnipoolFactory + else -> null + } + + return factory?.create(chain, computationScope) + } } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt index df5bacff48..573c2568a1 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/SwapConfirmationViewModel.kt @@ -32,7 +32,6 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs import io.novafoundation.nova.feature_swap_api.domain.model.editedBalance -import io.novafoundation.nova.feature_swap_api.domain.model.quotedBalance import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter @@ -50,13 +49,13 @@ import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.model. import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayload import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayloadFormatter import io.novafoundation.nova.feature_swap_impl.presentation.main.mapSwapValidationFailureToUI +import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.maxAction.MaxActionProvider -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeStatus import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.GenericFeeLoaderMixin @@ -361,7 +360,7 @@ class SwapConfirmationViewModel( .getOrNull() ?: return@launch val nativeAsset = walletRepository.getAsset(metaAccount.id, newSwapQuoteArgs.tokenOut.configuration)!! - val executeArgs = newSwapQuoteArgs.toExecuteArgs(swapQuote.quotedBalance, confirmationState.feeAsset, nativeAsset) + val executeArgs = newSwapQuoteArgs.toExecuteArgs(swapQuote, confirmationState.feeAsset, nativeAsset) feeMixin.loadFeeV2Generic( coroutineScope = viewModelScope, feeConstructor = { swapInteractor.estimateFee(executeArgs) }, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt index 71d86d2da6..1fbe930532 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayload.kt @@ -25,6 +25,15 @@ class SwapConfirmationPayload( val planksOut: Balance, val direction: SwapDirectionModel, val priceImpact: Double, + val path: List + ) : Parcelable + + @Parcelize + class SwapQuotePathModel( + val from: AssetPayload, + val to: AssetPayload, + val sourceId: String, + val sourceParams: Map ) : Parcelable @Parcelize diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt index ee7e08b01c..0fe72da771 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/confirmation/payload/SwapConfirmationPayloadFormatter.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_swap_impl.presentation.confirmation.paylo import io.novafoundation.nova.common.utils.asPercent import io.novafoundation.nova.feature_swap_api.domain.model.MinimumBalanceBuyIn +import io.novafoundation.nova.feature_swap_api.domain.model.QuotePath import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.presentation.model.mapFromModel @@ -26,19 +27,37 @@ class SwapConfirmationPayloadFormatter( amountIn = chainRegistry.asset(assetIn.fullChainAssetId).withAmount(planksIn), amountOut = chainRegistry.asset(assetOut.fullChainAssetId).withAmount(planksOut), direction = model.direction.mapFromModel(), - priceImpact = model.priceImpact.asPercent() + priceImpact = model.priceImpact.asPercent(), + path = QuotePath( + segments = model.path.map { + QuotePath.Segment( + from = it.from.fullChainAssetId, + to = it.to.fullChainAssetId, + sourceId = it.sourceId, + sourceParams = it.sourceParams + ) + } + ) ) } } fun mapSwapQuoteToModel(model: SwapQuote): SwapConfirmationPayload.SwapQuoteModel { return SwapConfirmationPayload.SwapQuoteModel( - model.assetIn.fullId.toAssetPayload(), - model.assetOut.fullId.toAssetPayload(), - model.planksIn, - model.planksOut, - model.direction.mapToModel(), - model.priceImpact.value + assetIn = model.assetIn.fullId.toAssetPayload(), + assetOut = model.assetOut.fullId.toAssetPayload(), + planksIn = model.planksIn, + planksOut = model.planksOut, + direction = model.direction.mapToModel(), + priceImpact = model.priceImpact.value, + path = model.path.segments.map { + SwapConfirmationPayload.SwapQuotePathModel( + from = it.from.toAssetPayload(), + to = it.to.toAssetPayload(), + sourceId = it.sourceId, + sourceParams = it.sourceParams + ) + } ) } diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt index dd675020c8..9e975ea494 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/SwapMainSettingsViewModel.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_swap_impl.presentation.main +import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import io.novafoundation.nova.common.base.BaseViewModel @@ -9,6 +10,7 @@ import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.presentation.DescriptiveButtonState import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event +import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.accumulate import io.novafoundation.nova.common.utils.combineToPair import io.novafoundation.nova.common.utils.event @@ -41,7 +43,6 @@ import io.novafoundation.nova.feature_swap_api.domain.model.SwapDirection import io.novafoundation.nova.feature_swap_api.domain.model.SwapFee import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuote import io.novafoundation.nova.feature_swap_api.domain.model.SwapQuoteArgs -import io.novafoundation.nova.feature_swap_api.domain.model.quotedBalance import io.novafoundation.nova.feature_swap_api.domain.model.swapRate import io.novafoundation.nova.feature_swap_api.domain.model.toExecuteArgs import io.novafoundation.nova.feature_swap_api.domain.model.totalDeductedPlanks @@ -102,11 +103,11 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull @@ -488,7 +489,9 @@ class SwapMainSettingsViewModel( combineToPair(nativeAssetFlow, feeMixin.loadedFeeModelOrNullFlow()) .filter { (nativeAsset, feeModel) -> val canChangeAutomatically = !feeTokenOnceChangedManually.value + val canAcceptAssetInAsPayment = canChangeFeeToken.first() + if (!canAcceptAssetInAsPayment) return@filter false if (!canChangeAutomatically) return@filter false if (nativeAsset.transferable.isZero) return@filter true if (feeModel == null) return@filter false @@ -530,16 +533,16 @@ class SwapMainSettingsViewModel( previous != current || feeMixin.feeLiveData.value !is FeeStatus.Loaded } } - .onEach { quoteState -> + .mapLatest { quoteState -> val swapArgs = quoteState.quoteArgs.toExecuteArgs( - quotedBalance = quoteState.value.quotedBalance, + quote = quoteState.value, customFeeAsset = quoteState.feeAsset, nativeAsset = nativeAssetFlow.first() ) - loadFeeV2Generic( - coroutineScope = viewModelScope, + loadFeeSuspending( feeConstructor = { swapInteractor.estimateFee(swapArgs) }, + retryScope = coroutineScope, onRetryCancelled = {} ) } @@ -613,7 +616,7 @@ class SwapMainSettingsViewModel( private fun setupQuoting() { setupPerSwapSettingQuoting() - setupPerBlockQuoting() + setupSubscriptionQuoting() } private fun setupPerSwapSettingQuoting() { @@ -621,14 +624,17 @@ class SwapMainSettingsViewModel( .launchIn(viewModelScope) } - private fun setupPerBlockQuoting() { - swapSettings.map { it.assetIn?.chainId } + private fun setupSubscriptionQuoting() { + swapSettings.mapNotNull { it.assetIn?.chainId } .distinctUntilChanged() .flatMapLatest { chainId -> - if (chainId == null) return@flatMapLatest emptyFlow() + val chain = chainRegistry.getChain(chainId) - swapInteractor.blockNumberUpdates(chainId) + swapInteractor.runSubscriptions(chain, selectedAccountUseCase.getSelectedMetaAccount()) + .catch { Log.e(this@SwapMainSettingsViewModel.LOG_TAG, "Failure during subscriptions run", it) } }.onEach { + Log.d("Swap", "ReQuote triggered from subscription") + val currentSwapSettings = swapSettings.first() performQuote(currentSwapSettings, shouldShowLoading = false) diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt index 84432e4fac..51f0b15b37 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/main/di/SwapMainSettingsModule.kt @@ -16,22 +16,21 @@ import io.novafoundation.nova.common.view.bottomSheet.description.DescriptionBot import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_buy_api.presentation.mixin.BuyMixin import io.novafoundation.nova.feature_swap_api.presentation.formatters.SwapRateFormatter -import io.novafoundation.nova.feature_swap_impl.data.network.blockhain.updaters.SwapUpdateSystemFactory +import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload +import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_swap_impl.domain.interactor.SwapInteractor import io.novafoundation.nova.feature_swap_impl.presentation.SwapRouter -import io.novafoundation.nova.feature_swap_impl.presentation.main.SwapMainSettingsViewModel -import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsStateProvider import io.novafoundation.nova.feature_swap_impl.presentation.common.PriceImpactFormatter import io.novafoundation.nova.feature_swap_impl.presentation.confirmation.payload.SwapConfirmationPayloadFormatter import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.EnoughAmountToSwapValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.LiquidityFieldValidatorFactory import io.novafoundation.nova.feature_swap_impl.presentation.fieldValidation.SwapReceiveAmountAboveEDFieldValidatorFactory -import io.novafoundation.nova.feature_swap_api.presentation.model.SwapSettingsPayload +import io.novafoundation.nova.feature_swap_impl.presentation.main.SwapMainSettingsViewModel import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapAmountInputMixinFactory import io.novafoundation.nova.feature_swap_impl.presentation.main.input.SwapInputMixinPriceImpactFiatFormatterFactory +import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase -import io.novafoundation.nova.feature_swap_impl.presentation.mixin.maxAction.MaxActionProviderFactory import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -90,7 +89,6 @@ class SwapMainSettingsModule { enoughAmountToSwapValidatorFactory: EnoughAmountToSwapValidatorFactory, swapReceiveAmountAboveEDFieldValidatorFactory: SwapReceiveAmountAboveEDFieldValidatorFactory, payload: SwapSettingsPayload, - swapUpdateSystemFactory: SwapUpdateSystemFactory, swapInputMixinPriceImpactFiatFormatterFactory: SwapInputMixinPriceImpactFiatFormatterFactory, accountUseCase: SelectedAccountUseCase, buyMixinFactory: BuyMixin.Factory, diff --git a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt index 052c70c49d..6c5f0a74ba 100644 --- a/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt +++ b/feature-swap-impl/src/main/java/io/novafoundation/nova/feature_swap_impl/presentation/state/RealSwapSettingsState.kt @@ -6,6 +6,8 @@ import io.novafoundation.nova.feature_swap_api.domain.model.flip import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettings import io.novafoundation.nova.feature_swap_api.presentation.state.SwapSettingsState import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.ext.isCommissionAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -21,22 +23,29 @@ class RealSwapSettingsState( override suspend fun setAssetInUpdatingFee(asset: Chain.Asset) { val current = selectedOption.value + val chain = chainRegistry.getChain(asset.chainId) + val newPlanks = current.convertedAmountForNewAssetIn(asset) + val feeIsNotCommissionAsset = current.feeAsset?.isCommissionAsset == false val feeIsInAnotherChain = current.feeAsset?.chainId != chain.id val needToResetFeeToNative = feeIsNotCommissionAsset || feeIsInAnotherChain val new = if (current.feeAsset == null || needToResetFeeToNative) { - current.copy(assetIn = asset, feeAsset = chain.commissionAsset) + current.copy(assetIn = asset, feeAsset = chain.commissionAsset, amount = newPlanks) } else { - current.copy(assetIn = asset) + current.copy(assetIn = asset, amount = newPlanks) } selectedOption.value = new } override fun setAssetOut(asset: Chain.Asset) { - selectedOption.value = selectedOption.value.copy(assetOut = asset) + val current = selectedOption.value + + val newPlanks = current.convertedAmountForNewAssetOut(asset) + + selectedOption.value = selectedOption.value.copy(assetOut = asset, amount = newPlanks) } override fun setFeeAsset(asset: Chain.Asset) { @@ -71,4 +80,26 @@ class RealSwapSettingsState( override fun setSwapSettings(swapSettings: SwapSettings) { selectedOption.value = swapSettings } + + private fun SwapSettings.convertedAmountForNewAssetIn(newAssetIn: Chain.Asset): Balance? { + val shouldConvertAsset = assetIn != null && amount != null && swapDirection == SwapDirection.SPECIFIED_IN + + return if (shouldConvertAsset) { + val decimalAmount = assetIn!!.amountFromPlanks(amount!!) + newAssetIn.planksFromAmount(decimalAmount) + } else { + amount + } + } + + private fun SwapSettings.convertedAmountForNewAssetOut(newAssetOut: Chain.Asset): Balance? { + val shouldConvertAsset = assetOut != null && amount != null && swapDirection == SwapDirection.SPECIFIED_OUT + + return if (shouldConvertAsset) { + val decimalAmount = assetOut!!.amountFromPlanks(amount!!) + newAssetOut.planksFromAmount(decimalAmount) + } else { + amount + } + } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/HydraDxAssetIdConverter.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/HydraDxAssetIdConverter.kt new file mode 100644 index 0000000000..75344230cd --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/HydraDxAssetIdConverter.kt @@ -0,0 +1,29 @@ +package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets + +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import java.math.BigInteger + +typealias HydraDxAssetId = BigInteger + +interface HydraDxAssetIdConverter { + + val systemAssetId: HydraDxAssetId + + suspend fun toOnChainIdOrNull(chainAsset: Chain.Asset): HydraDxAssetId? + + suspend fun toChainAssetOrNull(chain: Chain, onChainId: HydraDxAssetId): Chain.Asset? + + suspend fun allOnChainIds(chain: Chain): Map +} + +fun HydraDxAssetIdConverter.isSystemAsset(assetId: HydraDxAssetId): Boolean { + return assetId == systemAssetId +} + +suspend fun HydraDxAssetIdConverter.toOnChainIdOrThrow(chainAsset: Chain.Asset): HydraDxAssetId { + return requireNotNull(toOnChainIdOrNull(chainAsset)) +} + +suspend fun HydraDxAssetIdConverter.toChainAssetOrThrow(chain: Chain, onChainId: HydraDxAssetId): Chain.Asset { + return requireNotNull(toChainAssetOrNull(chain, onChainId)) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt index abc18282d9..2b08fb138f 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/balances/AssetBalance.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.data.network.runtime.binding.BlockHash import io.novafoundation.nova.core.updater.SharedRequestsBuilder import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.history.realtime.RealtimeHistoryUpdate +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.flow.Flow @@ -42,6 +43,13 @@ interface AssetBalance { accountId: AccountId ): AccountBalance + suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder, + ): Flow + suspend fun queryTotalBalance( chain: Chain, chainAsset: Chain.Asset, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt index 5d767e3dc0..22aee7c578 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/history/realtime/substrate/SubstrateRealtimeOperationFetcher.kt @@ -32,7 +32,7 @@ interface SubstrateRealtimeOperationFetcher { class Known(val id: Id) : Source() { enum class Id { - ASSET_CONVERSION_SWAP + ASSET_CONVERSION_SWAP, HYDRA_DX_SWAP } } } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt index 8a22d8f4af..5e5bea33c3 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/di/WalletFeatureApi.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_api.di import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.BalanceLocksUpdaterFactory import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.updaters.PaymentUpdaterFactory import io.novafoundation.nova.feature_wallet_api.data.network.coingecko.CoingeckoApi @@ -72,4 +73,6 @@ interface WalletFeatureApi { val crossChainTransfersUseCase: CrossChainTransfersUseCase val arbitraryTokenUseCase: ArbitraryTokenUseCase + + val hydraDxAssetIdConverter: HydraDxAssetIdConverter } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt index 914a90f75f..3627fd2af4 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Asset.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.domain.model +import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance import io.novafoundation.nova.common.utils.atLeastZero import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable @@ -42,6 +43,10 @@ data class Asset( } } + fun TransferableMode.calculateTransferable(accountBalance: AccountBalance): Balance { + return calculateTransferable(accountBalance.free, accountBalance.frozen, accountBalance.reserved) + } + fun EDCountingMode.calculateBalanceCountedTowardsEd(free: Balance, reserved: Balance): Balance { return when (this) { EDCountingMode.TOTAL -> totalBalance(free, reserved) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt index b3ba96fb15..c042ab3fbe 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/view/PriceSectionView.kt @@ -2,11 +2,10 @@ package io.novafoundation.nova.feature_wallet_api.presentation.view import android.content.Context import android.util.AttributeSet -import io.novafoundation.nova.common.utils.makeGone +import io.novafoundation.nova.common.utils.setTextOrHide import io.novafoundation.nova.common.utils.useAttributes import io.novafoundation.nova.common.view.section.SectionView import io.novafoundation.nova.feature_wallet_api.R -import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountModel import kotlinx.android.synthetic.main.section_price.view.sectionPriceFiat import kotlinx.android.synthetic.main.section_price.view.sectionPriceToken import kotlinx.android.synthetic.main.section_price.view.sectionTitle @@ -21,9 +20,9 @@ class PriceSectionView @JvmOverloads constructor( attrs?.let(::applyAttrs) } - fun setPrice(amountModel: AmountModel) { - sectionPriceToken.text = amountModel.token - sectionPriceFiat.text = amountModel.fiat + fun setPrice(token: String, fiat: String?) { + sectionPriceToken.text = token + sectionPriceFiat.setTextOrHide(fiat) } fun setTitle(title: String) { @@ -35,9 +34,3 @@ class PriceSectionView @JvmOverloads constructor( title?.let(::setTitle) } } - -fun PriceSectionView.setPriceOrHide(amountModel: AmountModel?) = if (amountModel != null) { - setPrice(amountModel) -} else { - makeGone() -} diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureDependencies.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureDependencies.kt index 73e9664d20..3f82ea1738 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureDependencies.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureDependencies.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.di +import android.content.Context import coil.ImageLoader import com.google.gson.Gson import io.novafoundation.nova.caip.caip2.Caip2Parser @@ -49,5 +50,7 @@ interface WalletConnectFeatureDependencies { val selectWalletMixinFactory: SelectWalletMixin.Factory + val appContext: Context + fun rootScope(): RootScope } diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureModule.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureModule.kt index ae7a3a3658..b8637aa1eb 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureModule.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/di/WalletConnectFeatureModule.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.di +import android.content.Context import com.google.gson.Gson import dagger.Module import dagger.Provides @@ -35,8 +36,12 @@ class WalletConnectFeatureModule { @Provides @FeatureScope - fun providePolkadotRequestFactory(gson: Gson, caip2Parser: Caip2Parser): PolkadotWalletConnectRequestFactory { - return PolkadotWalletConnectRequestFactory(gson, caip2Parser) + fun providePolkadotRequestFactory( + gson: Gson, + caip2Parser: Caip2Parser, + appContext: Context + ): PolkadotWalletConnectRequestFactory { + return PolkadotWalletConnectRequestFactory(gson, caip2Parser, appContext) } @Provides @@ -45,8 +50,9 @@ class WalletConnectFeatureModule { gson: Gson, caip2Parser: Caip2Parser, typedMessageParser: EvmTypedMessageParser, + appContext: Context ): EvmWalletConnectRequestFactory { - return EvmWalletConnectRequestFactory(gson, caip2Parser, typedMessageParser) + return EvmWalletConnectRequestFactory(gson, caip2Parser, typedMessageParser, appContext) } @Provides diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/Base.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/Base.kt index b7c4dfcb0d..36a9934d3b 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/Base.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/Base.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests +import android.content.Context import com.walletconnect.web3.wallet.client.Wallet import com.walletconnect.web3.wallet.client.Web3Wallet import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator @@ -11,7 +12,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext abstract class BaseWalletConnectRequest( - private val sessionRequest: Wallet.Model.SessionRequest + private val sessionRequest: Wallet.Model.SessionRequest, + private val context: Context, ) : WalletConnectRequest { override val id: String = sessionRequest.request.id.toString() @@ -30,13 +32,23 @@ abstract class BaseWalletConnectRequest( } Web3Wallet.respondSessionRequest(walletConnectResponse).getOrThrow() + + // TODO this code is untested since no dapp currently use redirect param + // We cant really enable this code without testing since we need to verify a corner-case when wc is used with redirect param inside dapp browser + // This might potentially break user flow since it might direct user to external browser instead of staying in our dapp browser + +// val redirect = sessionRequest.peerMetaData?.redirect +// if (!redirect.isNullOrEmpty()) { +// context.startActivity(Intent(Intent.ACTION_VIEW, redirect.toUri())) +// } } } } abstract class SignWalletConnectRequest( - sessionRequest: Wallet.Model.SessionRequest -) : BaseWalletConnectRequest(sessionRequest) { + sessionRequest: Wallet.Model.SessionRequest, + context: Context +) : BaseWalletConnectRequest(sessionRequest, context) { override suspend fun sentResponse(response: ExternalSignCommunicator.Response.Sent): Wallet.Params.SessionRequestResponse { error("Expected Signed response, got: Sent") @@ -44,8 +56,9 @@ abstract class SignWalletConnectRequest( } abstract class SendTxWalletConnectRequest( - sessionRequest: Wallet.Model.SessionRequest -) : BaseWalletConnectRequest(sessionRequest) { + sessionRequest: Wallet.Model.SessionRequest, + context: Context +) : BaseWalletConnectRequest(sessionRequest, context) { override suspend fun signedResponse(response: ExternalSignCommunicator.Response.Signed): Wallet.Params.SessionRequestResponse { error("Expected Sent response, got: Signed") diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmPersonalSignRequest.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmPersonalSignRequest.kt index 2832b928ea..99b283e627 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmPersonalSignRequest.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmPersonalSignRequest.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm +import android.content.Context import com.walletconnect.web3.wallet.client.Wallet import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest @@ -11,8 +12,9 @@ import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.request class EvmPersonalSignRequest( private val originAddress: String, private val message: EvmPersonalSignMessage, - private val sessionRequest: Wallet.Model.SessionRequest -) : SignWalletConnectRequest(sessionRequest) { + private val sessionRequest: Wallet.Model.SessionRequest, + context: Context +) : SignWalletConnectRequest(sessionRequest, context) { override suspend fun signedResponse(response: ExternalSignCommunicator.Response.Signed): Wallet.Params.SessionRequestResponse { return sessionRequest.approved(response.signature) diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSendTransactionRequest.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSendTransactionRequest.kt index c555f4cf08..b5f7fdd92d 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSendTransactionRequest.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSendTransactionRequest.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm +import android.content.Context import com.walletconnect.web3.wallet.client.Wallet import com.walletconnect.web3.wallet.client.Wallet.Params.SessionRequestResponse import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response @@ -13,8 +14,9 @@ import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.request class EvmSendTransactionRequest( private val transaction: EvmTransaction.Struct, private val chainId: Int, - private val sessionRequest: Wallet.Model.SessionRequest -) : SendTxWalletConnectRequest(sessionRequest) { + private val sessionRequest: Wallet.Model.SessionRequest, + context: Context +) : SendTxWalletConnectRequest(sessionRequest, context) { override suspend fun sentResponse(response: Response.Sent): SessionRequestResponse { return sessionRequest.approved(response.txHash) diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTransactionRequest.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTransactionRequest.kt index edf441b5bd..b50765abc9 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTransactionRequest.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTransactionRequest.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm +import android.content.Context import com.walletconnect.web3.wallet.client.Wallet import com.walletconnect.web3.wallet.client.Wallet.Params.SessionRequestResponse import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response @@ -13,8 +14,9 @@ import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.request class EvmSignTransactionRequest( private val transaction: EvmTransaction.Struct, private val chainId: Int, - private val sessionRequest: Wallet.Model.SessionRequest -) : SignWalletConnectRequest(sessionRequest) { + private val sessionRequest: Wallet.Model.SessionRequest, + context: Context +) : SignWalletConnectRequest(sessionRequest, context) { override suspend fun signedResponse(response: Response.Signed): SessionRequestResponse { return sessionRequest.approved(response.signature) diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTypedDataRequest.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTypedDataRequest.kt index 37a17b7ac0..a52b2d745f 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTypedDataRequest.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmSignTypedDataRequest.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm +import android.content.Context import com.walletconnect.web3.wallet.client.Wallet import com.walletconnect.web3.wallet.client.Wallet.Params.SessionRequestResponse import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response @@ -12,8 +13,9 @@ import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.request class EvmSignTypedDataRequest( private val originAddress: String, private val typedMessage: EvmTypedMessage, - private val sessionRequest: Wallet.Model.SessionRequest -) : SignWalletConnectRequest(sessionRequest) { + private val sessionRequest: Wallet.Model.SessionRequest, + context: Context +) : SignWalletConnectRequest(sessionRequest, context) { override suspend fun signedResponse(response: Response.Signed): SessionRequestResponse { return sessionRequest.approved(response.signature) diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmWalletConnectRequestFactory.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmWalletConnectRequestFactory.kt index 062af1b226..d3b1ca202e 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmWalletConnectRequestFactory.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/evm/EvmWalletConnectRequestFactory.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.evm +import android.content.Context import com.google.gson.Gson import com.walletconnect.web3.wallet.client.Wallet import io.novafoundation.nova.caip.caip2.Caip2Parser @@ -17,6 +18,7 @@ class EvmWalletConnectRequestFactory( private val gson: Gson, private val caip2Parser: Caip2Parser, private val typedMessageParser: EvmTypedMessageParser, + private val context: Context ) : WalletConnectRequest.Factory { override fun create(sessionRequest: Wallet.Model.SessionRequest): WalletConnectRequest? { @@ -39,25 +41,25 @@ class EvmWalletConnectRequestFactory( val (message, address) = gson.fromJson>(sessionRequest.request.params) val personalSignMessage = EvmPersonalSignMessage(message) - return EvmPersonalSignRequest(address, personalSignMessage, sessionRequest) + return EvmPersonalSignRequest(address, personalSignMessage, sessionRequest, context) } private fun parseEvmSignTypedMessage(sessionRequest: Wallet.Model.SessionRequest): WalletConnectRequest { val (address, typedMessage) = parseEvmSignTypedDataParams(sessionRequest.request.params) - return EvmSignTypedDataRequest(address, typedMessage, sessionRequest) + return EvmSignTypedDataRequest(address, typedMessage, sessionRequest, context) } private fun parseEvmSendTx(sessionRequest: Wallet.Model.SessionRequest, chainId: Int): WalletConnectRequest { val transaction = parseStructTransaction(sessionRequest.request.params) - return EvmSendTransactionRequest(transaction, chainId, sessionRequest) + return EvmSendTransactionRequest(transaction, chainId, sessionRequest, context) } private fun parseEvmSignTx(sessionRequest: Wallet.Model.SessionRequest, chainId: Int): WalletConnectRequest { val transaction = parseStructTransaction(sessionRequest.request.params) - return EvmSignTransactionRequest(transaction, chainId, sessionRequest) + return EvmSignTransactionRequest(transaction, chainId, sessionRequest, context) } private fun Wallet.Model.SessionRequest.eipChainId(): Int { diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotSignTransactionRequest.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotSignRequest.kt similarity index 88% rename from feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotSignTransactionRequest.kt rename to feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotSignRequest.kt index 2b6a8336f7..ac24a9d5f0 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotSignTransactionRequest.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotSignRequest.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.polkadot +import android.content.Context import com.google.gson.Gson import com.walletconnect.web3.wallet.client.Wallet import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator @@ -12,8 +13,9 @@ import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.request class PolkadotSignRequest( private val gson: Gson, private val polkadotSignPayload: PolkadotSignPayload, - private val sessionRequest: Wallet.Model.SessionRequest -) : SignWalletConnectRequest(sessionRequest) { + private val sessionRequest: Wallet.Model.SessionRequest, + context: Context +) : SignWalletConnectRequest(sessionRequest, context) { override suspend fun signedResponse(response: ExternalSignCommunicator.Response.Signed): Wallet.Params.SessionRequestResponse { val responseData = PolkadotSignerResult(id, response.signature) diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotWalletConnectRequestFactory.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotWalletConnectRequestFactory.kt index 86100630ab..7161afb866 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotWalletConnectRequestFactory.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/domain/session/requests/polkadot/PolkadotWalletConnectRequestFactory.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_connect_impl.domain.session.requests.polkadot +import android.content.Context import com.google.gson.Gson import com.walletconnect.web3.wallet.client.Wallet.Model.SessionRequest import io.novafoundation.nova.caip.caip2.Caip2Parser @@ -12,7 +13,8 @@ import io.novafoundation.nova.feature_wallet_connect_impl.domain.session.request class PolkadotWalletConnectRequestFactory( private val gson: Gson, - private val caip2Parser: Caip2Parser + private val caip2Parser: Caip2Parser, + private val context: Context ) : WalletConnectRequest.Factory { override fun create(sessionRequest: SessionRequest): WalletConnectRequest? { @@ -38,7 +40,8 @@ class PolkadotWalletConnectRequestFactory( return PolkadotSignRequest( gson = gson, polkadotSignPayload = signTxPayload.transactionPayload, - sessionRequest = sessionRequest + sessionRequest = sessionRequest, + context = context ) } @@ -52,7 +55,8 @@ class PolkadotWalletConnectRequestFactory( address = signMessagePayload.address, type = null ), - sessionRequest = sessionRequest + sessionRequest = sessionRequest, + context = context ) } } diff --git a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/list/WalletConnectSessionsEvent.kt b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/list/WalletConnectSessionsEvent.kt index 9b71a1b17c..e5fb8cc633 100644 --- a/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/list/WalletConnectSessionsEvent.kt +++ b/feature-wallet-connect-impl/src/main/java/io/novafoundation/nova/feature_wallet_connect_impl/presentation/sessions/list/WalletConnectSessionsEvent.kt @@ -25,7 +25,7 @@ fun Web3Wallet.sessionEventsFlow(scope: CoroutineScope): Flow + val omniPoolId = chainAsset.omniPoolTokenIdOrNull(runtime) + + omniPoolId == onChainId + } + } + + override suspend fun allOnChainIds(chain: Chain): Map { + val runtime = chainRegistry.getRuntime(chain.id) + + return chain.assets.mapNotNull { chainAsset -> + chainAsset.omniPoolTokenIdOrNull(runtime)?.let { it to chainAsset } + }.toMap() + } + + private fun Chain.Asset.omniPoolTokenIdOrNull(runtimeSnapshot: RuntimeSnapshot): HydraDxAssetId? { + return when (val type = type) { + is Chain.Asset.Type.Orml -> bindNumberOrNull(type.decodeOrNull(runtimeSnapshot)) + is Chain.Asset.Type.Native -> systemAssetId + else -> null + } + } + + private fun Chain.Asset.requireHydraDxAssetId(runtimeSnapshot: RuntimeSnapshot): HydraDxAssetId { + return requireNotNull(omniPoolTokenIdOrNull(runtimeSnapshot)) + } +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt index c284688ced..6085a31df0 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/UnsupportedAssetBalance.kt @@ -4,6 +4,7 @@ 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.types.Balance import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.flow.Flow @@ -24,6 +25,12 @@ class UnsupportedAssetBalance : AssetBalance { override suspend fun existentialDeposit(chain: Chain, chainAsset: Chain.Asset) = unsupported() override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId) = unsupported() + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder + ): Flow = unsupported() override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId) = unsupported() diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt index e14d88cc95..f07c49b72e 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/equilibrium/EquilibriumAssetBalance.kt @@ -31,6 +31,7 @@ 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.types.Balance 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 @@ -131,6 +132,15 @@ class EquilibriumAssetBalance( ) } + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder + ): Flow { + TODO("Not yet implemented") + } + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { val accountBalance = queryAccountBalance(chain, chainAsset, accountId) return accountBalance.free + accountBalance.reserved diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt index 2a239fc2f4..496dba1683 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmErc20/EvmErc20AssetBalance.kt @@ -20,8 +20,8 @@ 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.getCallEthereumApiOrThrow import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.getCallEthereumApiOrThrow import jp.co.soramitsu.fearless_utils.extensions.asEthereumAddress import jp.co.soramitsu.fearless_utils.extensions.toAccountId import jp.co.soramitsu.fearless_utils.runtime.AccountId @@ -79,6 +79,15 @@ class EvmErc20AssetBalance( ) } + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder + ): Flow { + TODO("Not yet implemented") + } + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { return queryAccountBalance(chain, chainAsset, accountId).free } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt index d8f089a4b1..a06370db34 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/evmNative/EvmNativeAssetBalance.kt @@ -12,9 +12,9 @@ import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Ba 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.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll @@ -61,6 +61,15 @@ class EvmNativeAssetBalance( ) } + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder + ): Flow { + TODO("Not yet implemented") + } + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { return queryAccountBalance(chain, chainAsset, accountId).free } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt index 717b9064e1..8ec1cc31d6 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlAssetBalance.kt @@ -1,6 +1,7 @@ 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.tokens import io.novafoundation.nova.core.updater.SharedRequestsBuilder @@ -11,6 +12,9 @@ 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.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable 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 @@ -19,6 +23,7 @@ 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.novafoundation.nova.runtime.storage.source.query.metadata import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.metadata.storage @@ -70,6 +75,23 @@ class OrmlAssetBalance( ) } + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder + ): Flow { + return remoteStorageSource.subscribe(chain.id, sharedSubscriptionBuilder) { + metadata.tokens().storage("Accounts").observe( + accountId, + chainAsset.ormlCurrencyId(runtime), + binding = ::bindOrmlAccountBalanceOrEmpty + ).map { + Asset.TransferableMode.REGULAR.calculateTransferable(it) + } + } + } + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { val accountBalance = queryAccountBalance(chain, chainAsset, accountId) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlBalanceBinding.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlBalanceBinding.kt index 5343fd9486..c627b18dcd 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlBalanceBinding.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/orml/OrmlBalanceBinding.kt @@ -2,12 +2,10 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset 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.bindNumber -import io.novafoundation.nova.common.data.network.runtime.binding.cast +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 jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot -import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.Struct import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHexOrNull import jp.co.soramitsu.fearless_utils.runtime.metadata.storage @@ -15,11 +13,7 @@ import jp.co.soramitsu.fearless_utils.runtime.metadata.storage fun bindOrmlAccountData(scale: String, runtime: RuntimeSnapshot): AccountBalance { val type = runtime.metadata.tokens().storage("Accounts").returnType() - val dynamicInstance = type.fromHexOrNull(runtime, scale).cast() + val dynamicInstance = type.fromHexOrNull(runtime, scale) - return AccountBalance( - free = bindNumber(dynamicInstance["free"]), - reserved = bindNumber(dynamicInstance["reserved"]), - frozen = bindNumber(dynamicInstance["frozen"]), - ) + return bindOrmlAccountData(dynamicInstance) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt index e086024a17..58f6492e49 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/statemine/StatemineAssetBalance.kt @@ -10,6 +10,7 @@ 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.types.Balance 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 @@ -82,6 +83,15 @@ class StatemineAssetBalance( ) } + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder + ): Flow { + TODO("Not yet implemented") + } + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { return queryAccountBalance(chain, chainAsset, accountId).free } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt index 7edad10b9d..5a1ab39c11 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/balances/utility/NativeAssetBalance.kt @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import android.util.Log import io.novafoundation.nova.common.data.network.runtime.binding.AccountBalance +import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.balances import io.novafoundation.nova.common.utils.decodeValue @@ -15,6 +16,9 @@ import io.novafoundation.nova.feature_wallet_api.data.cache.bindAccountInfoOrDef 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.types.Balance +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset +import io.novafoundation.nova.feature_wallet_api.domain.model.Asset.Companion.calculateTransferable import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.SubstrateRemoteSource 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 @@ -22,6 +26,10 @@ 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.storage.source.StorageDataSource +import io.novafoundation.nova.runtime.storage.source.query.metadata +import io.novafoundation.nova.runtime.storage.typed.account +import io.novafoundation.nova.runtime.storage.typed.system import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.metadata.storage import jp.co.soramitsu.fearless_utils.runtime.metadata.storageKey @@ -34,6 +42,7 @@ class NativeAssetBalance( private val chainRegistry: ChainRegistry, private val assetCache: AssetCache, private val substrateRemoteSource: SubstrateRemoteSource, + private val remoteStorage: StorageDataSource, private val lockDao: LockDao ) : AssetBalance { @@ -69,6 +78,21 @@ class NativeAssetBalance( return substrateRemoteSource.getAccountInfo(chain.id, accountId).data } + override suspend fun subscribeTransferableAccountBalance( + chain: Chain, + chainAsset: Chain.Asset, + accountId: AccountId, + sharedSubscriptionBuilder: SharedRequestsBuilder + ): Flow { + return remoteStorage.subscribe(chain.id, sharedSubscriptionBuilder) { + metadata.system.account.observe(accountId).map { + val accountInfo = it ?: AccountInfo.empty() + + accountInfo.transferableBalance() + } + } + } + override suspend fun queryTotalBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): BigInteger { val accountData = queryAccountBalance(chain, chainAsset, accountId) @@ -104,4 +128,15 @@ class NativeAssetBalance( } } } + + private fun AccountInfo.transferableBalance(): Balance { + return transferableMode.calculateTransferable(data) + } + + private val AccountInfo.transferableMode: Asset.TransferableMode + get() = if (data.flags.holdsAndFreezesEnabled()) { + Asset.TransferableMode.HOLDS_AND_FREEZES + } else { + Asset.TransferableMode.REGULAR + } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/SubstrateAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/SubstrateAssetHistory.kt index 09629f21ca..fb914c0bd9 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/SubstrateAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/SubstrateAssetHistory.kt @@ -36,7 +36,7 @@ abstract class SubstrateAssetHistory( coinPriceRepository: CoinPriceRepository ) : BaseAssetHistory(coinPriceRepository) { - abstract fun realtimeFetcherSources(): List + abstract fun realtimeFetcherSources(chain: Chain): List override suspend fun fetchOperationsForBalanceChange( chain: Chain, @@ -44,7 +44,7 @@ abstract class SubstrateAssetHistory( blockHash: String, accountId: AccountId ): List { - val sources = realtimeFetcherSources() + val sources = realtimeFetcherSources(chain) val realtimeOperationFetcher = realtimeOperationFetcherFactory.create(sources) return realtimeOperationFetcher.extractRealtimeHistoryUpdates(chain, chainAsset, blockHash) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt index 6a6f05a175..6e7466f70a 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/equilibrium/EquilibriumAssetHistory.kt @@ -30,7 +30,7 @@ class EquilibriumAssetHistory( realtimeOperationFetcherFactory: SubstrateRealtimeOperationFetcher.Factory ) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) { - override fun realtimeFetcherSources(): List { + override fun realtimeFetcherSources(chain: Chain): List { return listOf(TransferExtractor().asSource()) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt index c3675ecbea..199f89a371 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/orml/OrmlAssetHistory.kt @@ -7,6 +7,7 @@ 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.domain.interfaces.CoinPriceRepository import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFilter @@ -14,6 +15,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets 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.api.ExtrinsicVisit @@ -33,8 +35,14 @@ class OrmlAssetHistory( coinPriceRepository: CoinPriceRepository ) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) { - override fun realtimeFetcherSources(): List { - return listOf(TransferExtractor().asSource()) + override fun realtimeFetcherSources(chain: Chain): List { + 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 { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt index fc402d3409..9ecb58ee11 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/RealSubstrateRealtimeOperationFetcher.kt @@ -1,10 +1,13 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.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.api.ExtrinsicWalk import io.novafoundation.nova.runtime.extrinsic.visitor.api.walkToList import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -13,32 +16,42 @@ import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.EventsRepo 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): SubstrateRealtimeOperationFetcher { - val extractors = sources.map { it.extractor() } + val extractors = sources.flatMap { it.extractors() } return RealSubstrateRealtimeOperationFetcher(eventsRepository, extractors, extrinsicWalk) } - private fun Factory.Source.extractor(): Extractor { + private fun Factory.Source.extractors(): List { return when (this) { - is Factory.Source.FromExtractor -> extractor - is Factory.Source.Known -> id.extractor() + is Factory.Source.FromExtractor -> listOf(extractor) + is Factory.Source.Known -> id.extractors() } } - private fun Factory.Source.Known.Id.extractor(): Extractor { + private fun Factory.Source.Known.Id.extractors(): List { return when (this) { - Factory.Source.Known.Id.ASSET_CONVERSION_SWAP -> assetConversionSwap() + 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( diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/BaseHydraDxSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/BaseHydraDxSwapExtractor.kt new file mode 100644 index 0000000000..841b0095b5 --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/BaseHydraDxSwapExtractor.kt @@ -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_wallet_api.data.network.blockhain.assets.HydraDxAssetId +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.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.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 jp.co.soramitsu.fearless_utils.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 + ) +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxOmniPoolSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxOmniPoolSwapExtractor.kt new file mode 100644 index 0000000000..e8763a1204 --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxOmniPoolSwapExtractor.kt @@ -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_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent +import jp.co.soramitsu.fearless_utils.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") + } + } +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxRouterSwapExtractor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxRouterSwapExtractor.kt new file mode 100644 index 0000000000..3ccff03f6d --- /dev/null +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/realtime/substrate/hydraDx/HydraDxRouterSwapExtractor.kt @@ -0,0 +1,59 @@ +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_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.findEvent +import jp.co.soramitsu.fearless_utils.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") + + 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") + } + } +} diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt index 513d05d38e..f0c5ddb9d9 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/statemine/StatemineAssetHistory.kt @@ -12,6 +12,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFi 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 @@ -34,11 +35,14 @@ class StatemineAssetHistory( coinPriceRepository: CoinPriceRepository ) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) { - override fun realtimeFetcherSources(): List { - return listOf( - TransferExtractor().asSource(), - Source.Known.Id.ASSET_CONVERSION_SWAP.asSource() - ) + override fun realtimeFetcherSources(chain: Chain): List { + 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 { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt index 67be5eede8..c4def4f882 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/history/utility/NativeAssetHistory.kt @@ -13,6 +13,8 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TransactionFi 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.api.ExtrinsicVisit import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -30,11 +32,18 @@ class NativeAssetHistory( coinPriceRepository: CoinPriceRepository ) : SubstrateAssetHistory(walletOperationsApi, cursorStorage, realtimeOperationFetcherFactory, coinPriceRepository) { - override fun realtimeFetcherSources(): List { - return listOf( - TransferExtractor().asSource(), - Source.Known.Id.ASSET_CONVERSION_SWAP.asSource() - ) + override fun realtimeFetcherSources(chain: Chain): List { + 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 { diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt index 0af3ab1fb5..59d62196ae 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/WalletFeatureModule.kt @@ -25,6 +25,7 @@ import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRep import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache import io.novafoundation.nova.feature_wallet_api.data.cache.CoinPriceLocalDataSourceImpl import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.HydraDxAssetIdConverter 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.updaters.PaymentUpdaterFactory import io.novafoundation.nova.feature_wallet_api.data.network.coingecko.CoingeckoApi @@ -54,6 +55,7 @@ import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoade import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderProviderFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.SubstrateRemoteSource import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.WssSubstrateSource +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.RealHydraDxAssetIdConverter import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.realtime.substrate.SubstrateRealtimeOperationFetcherFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.updaters.balance.RealPaymentUpdaterFactory import io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.CrossChainConfigApi @@ -332,9 +334,15 @@ class WalletFeatureModule { fun provideSubstrateRealtimeOperationFetcherFactory( multiLocationConverterFactory: MultiLocationConverterFactory, eventsRepository: EventsRepository, - extrinsicWalk: ExtrinsicWalk + extrinsicWalk: ExtrinsicWalk, + hydraDxAssetIdConverter: HydraDxAssetIdConverter ): SubstrateRealtimeOperationFetcher.Factory { - return SubstrateRealtimeOperationFetcherFactory(multiLocationConverterFactory, eventsRepository, extrinsicWalk) + return SubstrateRealtimeOperationFetcherFactory( + multiLocationConverterFactory = multiLocationConverterFactory, + hydraDxAssetIdConverter = hydraDxAssetIdConverter, + eventsRepository = eventsRepository, + extrinsicWalk = extrinsicWalk + ) } @Provides @@ -348,4 +356,12 @@ class WalletFeatureModule { walletRepository, extrinsicService ) + + @Provides + @FeatureScope + fun provideHydraDxAssetIdConverter( + chainRegistry: ChainRegistry + ): HydraDxAssetIdConverter { + return RealHydraDxAssetIdConverter(chainRegistry) + } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt index f0a097ab7e..51ac18bc61 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/di/modules/NativeAssetsModule.kt @@ -21,6 +21,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets 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.di.LOCAL_STORAGE_SOURCE +import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.storage.source.StorageDataSource import javax.inject.Named @@ -38,8 +39,15 @@ class NativeAssetsModule { chainRegistry: ChainRegistry, assetCache: AssetCache, substrateRemoteSource: SubstrateRemoteSource, + @Named(REMOTE_STORAGE_SOURCE) remoteSource: StorageDataSource, lockDao: LockDao - ) = NativeAssetBalance(chainRegistry, assetCache, substrateRemoteSource, lockDao) + ) = NativeAssetBalance( + chainRegistry = chainRegistry, + assetCache = assetCache, + substrateRemoteSource = substrateRemoteSource, + remoteStorage = remoteSource, + lockDao = lockDao + ) @Provides @FeatureScope diff --git a/gradle.properties b/gradle.properties index 52f43f63ed..4eaf4f784c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,17 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +#Fri Feb 16 12:00:34 MSK 2024 org.gradle.parallel=true +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true diff --git a/hydra-dx-math/.gitignore b/hydra-dx-math/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/hydra-dx-math/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/hydra-dx-math/bindings/.gitignore b/hydra-dx-math/bindings/.gitignore new file mode 100644 index 0000000000..688c3c4b3d --- /dev/null +++ b/hydra-dx-math/bindings/.gitignore @@ -0,0 +1,3 @@ +.idea +Cargo.lock +target \ No newline at end of file diff --git a/hydra-dx-math/bindings/Cargo.toml b/hydra-dx-math/bindings/Cargo.toml new file mode 100644 index 0000000000..dc87a8f4aa --- /dev/null +++ b/hydra-dx-math/bindings/Cargo.toml @@ -0,0 +1,34 @@ +[package] +authors = ['Novasama Technologies'] +edition = '2021' +license = 'Apache 2.0' +name = "hydra-dx-math-java" +repository = 'https://github.com/nova-wallet/nova-wallet-android' +version = "0.1.0" + +[dependencies] +#wee_alloc = "0.4.5" +serde = { version = "1.0.169", features = ["derive"] } +serde_json = "1.0.100" +serde-aux = "4.2.0" +sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38", default-features = false } +hydra-dx-math = { git = "https://github.com/galacticcouncil/HydraDX-node", rev="9e733374233e2bdef039d5b3e73c5e939d7512f4"} +jni = { version = "0.17.0", default-features = false } + +[dev-dependencies] +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38", default-features = false} +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38", default-features = false } + +[profile.release] +strip = true +lto = true +opt-level = "s" + +[lib] +name = "hydra_dx_math_java" +crate_type = ["cdylib"] + +[features] +default = ["std"] +std = ["sp-arithmetic/std", "sp-runtime/std"] +stableswap = [] \ No newline at end of file diff --git a/hydra-dx-math/bindings/src/lib.rs b/hydra-dx-math/bindings/src/lib.rs new file mode 100644 index 0000000000..7aed006e09 --- /dev/null +++ b/hydra-dx-math/bindings/src/lib.rs @@ -0,0 +1,526 @@ +#![allow(non_snake_case)] + +extern crate core; +extern crate hydra_dx_math; +extern crate jni; +extern crate serde; +extern crate sp_arithmetic; + +use std::collections::HashMap; + +use hydra_dx_math::stableswap::types::AssetReserve; +use jni::JNIEnv; +use jni::objects::{JClass, JString}; +use jni::sys::{jint}; +use serde::Deserialize; +use sp_arithmetic::Permill; + +use serde_aux::prelude::*; +#[cfg(test)] +use sp_core::crypto::UncheckedFrom; +#[cfg(test)] +use sp_core::Hasher; +#[cfg(test)] +use sp_runtime::traits::IdentifyAccount; + +fn error() -> String { + "-1".to_string() +} + +macro_rules! parse_into { + ($x:ty, $y:expr) => {{ + let r = if let Some(x) = $y.parse::<$x>().ok() { + x + } else { + println!("Parse failed"); + return error(); + }; + r + }}; +} + +const D_ITERATIONS: u8 = 128; +const Y_ITERATIONS: u8 = 64; + +#[derive(Deserialize, Copy, Clone, Debug)] +pub struct AssetBalance { + asset_id: u32, + #[serde(deserialize_with = "deserialize_number_from_string")] + amount: u128, + decimals: u8, +} + +impl From<&AssetBalance> for AssetReserve { + fn from(value: &AssetBalance) -> Self { + Self { + amount: value.amount, + decimals: value.decimals, + } + } +} + +#[derive(Deserialize, Copy, Clone, Debug)] +pub struct AssetAmount { + asset_id: u32, + #[serde(deserialize_with = "deserialize_number_from_string")] + amount: u128, +} + +fn get_str<'a>(jni: &'a JNIEnv<'a>, string: JString<'a>) -> String { + return jni.get_string(string).unwrap().to_str().unwrap().to_string(); +} + + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_stableswap_StableSwapMathBridge_calculate_1out_1given_1in<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + reserves: JString, + asset_in: jint, + asset_out: jint, + amount_in: JString, + amplification: JString, + fee: JString, +) -> JString<'a> { + let reserves = get_str(&jni_env,reserves); + let asset_in = asset_in as u32; + let asset_out = asset_out as u32; + let amount_in = get_str(&jni_env,amount_in); + let amplification = get_str(&jni_env,amplification); + let fee = get_str(&jni_env,fee); + + let out = calculate_out_given_in(reserves, asset_in, asset_out, amount_in, amplification, fee); + + return jni_env.new_string(out).unwrap() +} + + +fn calculate_out_given_in( + reserves: String, + asset_in: u32, + asset_out: u32, + amount_in: String, + amplification: String, + fee: String, +) -> String { + let reserves: serde_json::Result> = serde_json::from_str(&reserves); + if reserves.is_err() { + return error(); + } + let mut reserves = reserves.unwrap(); + reserves.sort_by_key(|v| v.asset_id); + + let idx_in = reserves.iter().position(|v| v.asset_id == asset_in); + let idx_out = reserves.iter().position(|v| v.asset_id == asset_out); + + if idx_in.is_none() || idx_out.is_none() { + return error(); + } + + let amount_in = parse_into!(u128, amount_in); + let amplification = parse_into!(u128, amplification); + let fee = Permill::from_float(parse_into!(f64, fee)); + + let balances: Vec = reserves.iter().map(|v| v.into()).collect(); + + let result = hydra_dx_math::stableswap::calculate_out_given_in_with_fee::( + &balances, + idx_in.unwrap(), + idx_out.unwrap(), + amount_in, + amplification, + fee, + ); + + if let Some(r) = result { + r.0.to_string() + } else { + error() + } +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_stableswap_StableSwapMathBridge_calculate_1in_1given_1out<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + reserves: JString, + asset_in: jint, + asset_out: jint, + amount_in: JString, + amplification: JString, + fee: JString, +) -> JString<'a> { + let reserves = get_str(&jni_env,reserves); + let asset_in = asset_in as u32; + let asset_out = asset_out as u32; + let amount_in = get_str(&jni_env,amount_in); + let amplification = get_str(&jni_env,amplification); + let fee = get_str(&jni_env,fee); + + let result = calculate_in_given_out(reserves, asset_in, asset_out, amount_in, amplification, fee); + + return jni_env.new_string(result).unwrap() +} + +fn calculate_in_given_out( + reserves: String, + asset_in: u32, + asset_out: u32, + amount_out: String, + amplification: String, + fee: String, +) -> String { + let reserves: serde_json::Result> = serde_json::from_str(&reserves); + if reserves.is_err() { + return error(); + } + let mut reserves = reserves.unwrap(); + reserves.sort_by_key(|v| v.asset_id); + + let idx_in = reserves.iter().position(|v| v.asset_id == asset_in); + let idx_out = reserves.iter().position(|v| v.asset_id == asset_out); + + if idx_in.is_none() || idx_out.is_none() { + return error(); + } + + let amount_out = parse_into!(u128, amount_out); + let amplification = parse_into!(u128, amplification); + let fee = Permill::from_float(parse_into!(f64, fee)); + + let balances: Vec = reserves.iter().map(|v| v.into()).collect(); + + let result = hydra_dx_math::stableswap::calculate_in_given_out_with_fee::( + &balances, + idx_in.unwrap(), + idx_out.unwrap(), + amount_out, + amplification, + fee, + ); + + if let Some(r) = result { + r.0.to_string() + } else { + error() + } +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_stableswap_StableSwapMathBridge_calculate_1amplification<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + initial_amplification: JString, + final_amplification: JString, + initial_block: JString, + final_block: JString, + current_block: JString, +) -> JString<'a> { + let initial_amplification = get_str(&jni_env,initial_amplification); + let final_amplification = get_str(&jni_env,final_amplification); + let initial_block = get_str(&jni_env,initial_block); + let final_block = get_str(&jni_env,final_block); + let current_block = get_str(&jni_env,current_block); + + let result = calculate_amplification(initial_amplification, final_amplification, initial_block, final_block, current_block); + + return jni_env.new_string(result).unwrap() +} + +fn calculate_amplification( + initial_amplification: String, + final_amplification: String, + initial_block: String, + final_block: String, + current_block: String, +) -> String { + let initial_amplification = parse_into!(u128, initial_amplification); + let final_amplification = parse_into!(u128, final_amplification); + let initial_block = parse_into!(u128, initial_block); + let final_block = parse_into!(u128, final_block); + let current_block = parse_into!(u128, current_block); + + hydra_dx_math::stableswap::calculate_amplification( + initial_amplification, + final_amplification, + initial_block, + final_block, + current_block, + ) + .to_string() +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_stableswap_StableSwapMathBridge_calculate_1shares<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + reserves: JString, + assets: JString, + amplification: JString, + share_issuance: JString, + fee: JString, +) -> JString<'a> { + let reserves = get_str(&jni_env,reserves); + let assets = get_str(&jni_env,assets); + let amplification = get_str(&jni_env,amplification); + let share_issuance = get_str(&jni_env,share_issuance); + let fee = get_str(&jni_env,fee); + + let result = calculate_shares(reserves, assets, amplification, share_issuance, fee); + + return jni_env.new_string(result).unwrap() +} + +fn calculate_shares( + reserves: String, + assets: String, + amplification: String, + share_issuance: String, + fee: String, +) -> String { + let reserves: serde_json::Result> = serde_json::from_str(&reserves); + if reserves.is_err() { + return error(); + } + let mut reserves = reserves.unwrap(); + reserves.sort_by_key(|v| v.asset_id); + + let assets: serde_json::Result> = serde_json::from_str(&assets); + if assets.is_err() { + return error(); + } + let assets = assets.unwrap(); + if assets.len() > reserves.len() { + return error(); + } + + let mut updated_reserves = reserves.clone(); + + let mut liquidity: HashMap = HashMap::new(); + for a in assets.iter() { + let r = liquidity.insert(a.asset_id, a.amount); + if r.is_some() { + return error(); + } + } + for reserve in updated_reserves.iter_mut() { + if let Some(v) = liquidity.get(&reserve.asset_id) { + reserve.amount += v; + } + } + let balances: Vec = reserves.iter().map(|v| v.into()).collect(); + let updated_balances: Vec = updated_reserves.iter().map(|v| v.into()).collect(); + let amplification = parse_into!(u128, amplification); + let issuance = parse_into!(u128, share_issuance); + let fee = Permill::from_float(parse_into!(f64, fee)); + + let result = hydra_dx_math::stableswap::calculate_shares::( + &balances, + &updated_balances, + amplification, + issuance, + fee, + ); + + if let Some(r) = result { + r.to_string() + } else { + error() + } +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_stableswap_StableSwapMathBridge_calculate_1shares_1for_1amount<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + reserves: JString, + asset_in: jint, + amount: JString, + amplification: JString, + share_issuance: JString, + fee: JString, +) -> JString<'a> { + let reserves = get_str(&jni_env,reserves); + let asset_in = asset_in as u32; + let amount = get_str(&jni_env,amount); + let amplification = get_str(&jni_env,amplification); + let share_issuance = get_str(&jni_env,share_issuance); + let fee = get_str(&jni_env,fee); + + let result = calculate_shares_for_amount(reserves, asset_in, amount, amplification, share_issuance, fee); + + return jni_env.new_string(result).unwrap() +} + + +fn calculate_shares_for_amount( + reserves: String, + asset_in: u32, + amount: String, + amplification: String, + share_issuance: String, + fee: String, +) -> String { + let reserves: serde_json::Result> = serde_json::from_str(&reserves); + if reserves.is_err() { + return error(); + } + let mut reserves = reserves.unwrap(); + reserves.sort_by_key(|v| v.asset_id); + let idx_in = reserves.iter().position(|v| v.asset_id == asset_in); + if idx_in.is_none() { + return error(); + } + let amount_in = parse_into!(u128, amount); + let balances: Vec = reserves.iter().map(|v| v.into()).collect(); + let amplification = parse_into!(u128, amplification); + let issuance = parse_into!(u128, share_issuance); + let fee = Permill::from_float(parse_into!(f64, fee)); + + let result = hydra_dx_math::stableswap::calculate_shares_for_amount::( + &balances, + idx_in.unwrap(), + amount_in, + amplification, + issuance, + fee, + ); + + if let Some(r) = result { + r.to_string() + } else { + error() + } +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_stableswap_StableSwapMathBridge_calculate_1add_1one_1asset<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + reserves: JString, + shares: JString, + asset_in: jint, + amplification: JString, + share_issuance: JString, + fee: JString, +) -> JString<'a> { + let reserves = get_str(&jni_env,reserves); + let shares = get_str(&jni_env,shares); + let asset_in = asset_in as u32; + let amplification = get_str(&jni_env,amplification); + let share_issuance = get_str(&jni_env,share_issuance); + let fee = get_str(&jni_env,fee); + + let result = calculate_add_one_asset(reserves, shares, asset_in, amplification, share_issuance, fee); + + return jni_env.new_string(result).unwrap() +} + + +fn calculate_add_one_asset( + reserves: String, + shares: String, + asset_in: u32, + amplification: String, + share_issuance: String, + fee: String, +) -> String { + let reserves: serde_json::Result> = serde_json::from_str(&reserves); + if reserves.is_err() { + return error(); + } + let mut reserves = reserves.unwrap(); + reserves.sort_by_key(|v| v.asset_id); + let idx_in = reserves.iter().position(|v| v.asset_id == asset_in); + if idx_in.is_none() { + return error(); + } + + let balances: Vec = reserves.iter().map(|v| v.into()).collect(); + let shares = parse_into!(u128, shares); + let amplification = parse_into!(u128, amplification); + let issuance = parse_into!(u128, share_issuance); + let fee = Permill::from_float(parse_into!(f64, fee)); + + let result = hydra_dx_math::stableswap::calculate_add_one_asset::( + &balances, + shares, + idx_in.unwrap(), + issuance, + amplification, + fee, + ); + + if let Some(r) = result { + r.0.to_string() + } else { + error() + } +} + +#[no_mangle] +pub fn Java_io_novafoundation_nova_hydra_1dx_1math_stableswap_StableSwapMathBridge_calculate_1liquidity_1out_1one_1asset<'a>( + jni_env: JNIEnv<'a>, + _: JClass, + reserves: JString, + shares: JString, + asset_out: jint, + amplification: JString, + share_issuance: JString, + withdraw_fee: JString, +) -> JString<'a> { + let reserves = get_str(&jni_env,reserves); + let shares = get_str(&jni_env,shares); + let asset_out = asset_out as u32; + let amplification = get_str(&jni_env,amplification); + let share_issuance = get_str(&jni_env,share_issuance); + let withdraw_fee = get_str(&jni_env,withdraw_fee); + + let result = calculate_liquidity_out_one_asset(reserves, shares, asset_out, amplification, share_issuance, withdraw_fee); + + return jni_env.new_string(result).unwrap() +} + + +fn calculate_liquidity_out_one_asset( + reserves: String, + shares: String, + asset_out: u32, + amplification: String, + share_issuance: String, + withdraw_fee: String, +) -> String { + let reserves: serde_json::Result> = serde_json::from_str(&reserves); + if reserves.is_err() { + return error(); + } + let mut reserves = reserves.unwrap(); + reserves.sort_by_key(|v| v.asset_id); + + let idx_out = reserves.iter().position(|v| v.asset_id == asset_out); + if idx_out.is_none() { + return error(); + } + + let shares_out = parse_into!(u128, shares); + let amplification = parse_into!(u128, amplification); + let issuance = parse_into!(u128, share_issuance); + let fee = Permill::from_float(parse_into!(f64, withdraw_fee)); + + let balances: Vec = reserves.iter().map(|v| v.into()).collect(); + + let result = hydra_dx_math::stableswap::calculate_withdraw_one_asset::( + &balances, + shares_out, + idx_out.unwrap(), + issuance, + amplification, + fee, + ); + + if let Some(r) = result { + r.0.to_string() + } else { + error() + } +} \ No newline at end of file diff --git a/hydra-dx-math/build.gradle b/hydra-dx-math/build.gradle new file mode 100644 index 0000000000..858de23ee4 --- /dev/null +++ b/hydra-dx-math/build.gradle @@ -0,0 +1,55 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'org.mozilla.rust-android-gradle.rust-android' + +android { + compileSdkVersion rootProject.compileSdkVersion + + ndkVersion "26.1.10909125" + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = '1.8' + freeCompilerArgs = ["-Xcontext-receivers"] + } +} + +dependencies { + implementation kotlinDep + + testImplementation jUnitDep + + androidTestImplementation androidTestRunnerDep + androidTestImplementation androidTestRulesDep + androidTestImplementation androidJunitDep +} + +cargo { + module = "bindings/" + libname = "hydra_dx_math_java" + targets = ["arm", "arm64", "x86", "x86_64"] + profile = "release" +} + +tasks.matching { it.name.matches(/merge.*JniLibFolders/) }.configureEach { + it.inputs.dir(new File(buildDir, "rustJniLibs/android")) + it.dependsOn("cargoBuild") +} diff --git a/hydra-dx-math/src/androidTest/java/io/novafoundation/nova/hydra_dx_math/stableswap/StableSwapTest.kt b/hydra-dx-math/src/androidTest/java/io/novafoundation/nova/hydra_dx_math/stableswap/StableSwapTest.kt new file mode 100644 index 0000000000..1a2f70fa7c --- /dev/null +++ b/hydra-dx-math/src/androidTest/java/io/novafoundation/nova/hydra_dx_math/stableswap/StableSwapTest.kt @@ -0,0 +1,271 @@ +package io.novafoundation.nova.hydra_dx_math.stableswap + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Ignore +import org.junit.Test + +class StableSwapTest { + + @Test + fun shouldCalculateOutGivenIn() { + val data = """ + [{ + "asset_id": 1, + "amount": "1000000000000", + "decimals": 12 + }, + { + "asset_id": 0, + "amount": "1000000000000", + "decimals": 12 + } + ] + """ + + val result = StableSwapMathBridge.calculate_out_given_in( + data, + 0, + 1, + "1000000000", + "1", + "0" + ) + + assertEquals("999500248", result) + } + + @Test + fun shouldCalculateInGiveOut() { + val data = """ + [{ + "asset_id": 1, + "amount": "1000000000000", + "decimals": 12 + }, + { + "asset_id": 0, + "amount": "1000000000000", + "decimals": 12 + } + ] + """ + + val result = StableSwapMathBridge.calculate_in_given_out( + data, + 0, + 1, + "1000000000", + "1", + "0" + ) + + assertNotEquals("-1", result) + } + + @Test + fun shouldCalculateAmplification() { + val result = StableSwapMathBridge.calculate_amplification("10", "10", "0", "100", "50") + + assertEquals("10", result) + } + + @Test + fun shouldCalculateShares() { + val data = """ + [{ + "asset_id": 0, + "amount":"90000000000", + "decimals": 12 + }, + { + "asset_id": 1, + "amount": "5000000000000000000000", + "decimals": 12 + } + ] + """ + + val assets = """ + [{"asset_id":1,"amount":"43000000000000000000"}] + """ + + val result = StableSwapMathBridge.calculate_shares( + data, + assets, + "1000", + "64839594451719860", + "0" + ) + + assertEquals("371541351762585", result.toString()) + } + + @Test + fun shouldCalculateSharesForAmount() { + val data = """ + [ + { + "asset_id": 0, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 1, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 2, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 3, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 4, + "amount": "10000000000000000", + "decimals": 12 + } +] + """ + + val result = StableSwapMathBridge.calculate_shares_for_amount( + data, + 0, + "100000000000000", + "100", + "20000000000000000000000", + "0" + ) + + assertEquals("40001593768209443008", result.toString()) + } + + @Test + @Ignore("The test fails with last digit being 0 instead of 1. We need to check why it happens later") + fun shouldCalculateAddOneAsset() { + val data = """ + [ + { + "asset_id": 0, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 1, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 2, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 3, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 4, + "amount": "10000000000000000", + "decimals": 12 + } +] + """ + + val result = StableSwapMathBridge.calculate_add_one_asset( + data, + "399850144492663029649", + 2, + "100", + "20000000000000000000000", + "0" + ) + + assertEquals("1000000000000001", result.toString()) + } + + @Test + fun shouldcalculateLiquidityOutOneAsset() { + val data = """ + [ + { + "asset_id": 0, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 1, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 2, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 3, + "amount": "10000000000000000", + "decimals": 12 + }, + { + "asset_id": 4, + "amount": "10000000000000000", + "decimals": 12 + } +] + """ + + val result = StableSwapMathBridge.calculate_liquidity_out_one_asset( + data, + "40001593768209443008", + 0, + "100", + "20000000000000000000000", + "0" + ) + + assertEquals("99999999999999", result.toString()) + } + + @Test + fun failingCase() { + val data = """ + [{"amount":"2246975221087","decimals":6,"asset_id":10},{"amount":"2256486088023","decimals":6,"asset_id":22}] + """ + + val result = StableSwapMathBridge.calculate_liquidity_out_one_asset( + data, + "1000000000", + 10, + "100", + "4502091550542833181457210", + "0.00040" + ) + + assertEquals("99999999999999", result.toString()) + } + + @Test + fun failingCase2() { + val data = """ +[{"amount":"505342304916","decimals":6,"asset_id":10},{"amount":"368030436758902944990436","decimals":18,"asset_id":18},{"amount":"410374848833","decimals":6,"asset_id":21},{"amount":"0","decimals":6,"asset_id":23}] """ + + val result = StableSwapMathBridge.calculate_shares_for_amount( + data, + 10, + "10", + "320", + "1662219218861236418723363", + "0.00040" + ) + + assertEquals("99999999999999", result.toString()) + } +} diff --git a/hydra-dx-math/src/main/AndroidManifest.xml b/hydra-dx-math/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2a1b44c606 --- /dev/null +++ b/hydra-dx-math/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/stableswap/StableSwapMathBridge.java b/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/stableswap/StableSwapMathBridge.java new file mode 100644 index 0000000000..df0493ef29 --- /dev/null +++ b/hydra-dx-math/src/main/java/io/novafoundation/nova/hydra_dx_math/stableswap/StableSwapMathBridge.java @@ -0,0 +1,69 @@ +package io.novafoundation.nova.hydra_dx_math.stableswap; + +public class StableSwapMathBridge { + + static { + System.loadLibrary("hydra_dx_math_java"); + } + + public static native String calculate_out_given_in( + String reserves, + int asset_in, + int asset_out, + String amount_in, + String amplification, + String fee + ); + + public static native String calculate_in_given_out( + String reserves, + int asset_in, + int asset_out, + String amount_out, + String amplification, + String fee + ); + + public static native String calculate_amplification( + String initial_amplification, + String final_amplification, + String initial_block, + String final_block, + String current_block + ); + + public static native String calculate_shares( + String reserves, + String assets, + String amplification, + String share_issuance, + String fee + ); + + public static native String calculate_shares_for_amount( + String reserves, + int asset_in, + String amount, + String amplification, + String share_issuance, + String fee + ); + + public static native String calculate_add_one_asset( + String reserves, + String shares, + int asset_in, + String amplification, + String share_issuance, + String fee + ); + + public static native String calculate_liquidity_out_one_asset( + String reserves, + String shares, + int asset_out, + String amplification, + String share_issuance, + String withdraw_fee + ); +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt index 3094f513c9..bbc5ba6d98 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt @@ -31,6 +31,7 @@ import jp.co.soramitsu.fearless_utils.extensions.toAddress import jp.co.soramitsu.fearless_utils.extensions.toHexString import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHex +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.fromHexOrNull import jp.co.soramitsu.fearless_utils.runtime.definitions.types.toHexUntyped import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.addressPrefix import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.toAccountId @@ -64,6 +65,14 @@ fun Chain.Asset.supportedStakingOptions(): List { fun Chain.isSwapSupported(): Boolean = swap.isNotEmpty() +fun List.assetConversionSupported(): Boolean { + return Chain.Swap.ASSET_CONVERSION in this +} + +fun List.hydraDxSupported(): Boolean { + return Chain.Swap.HYDRA_DX in this +} + val Chain.ConnectionState.isFullSync: Boolean get() = this == Chain.ConnectionState.FULL_SYNC @@ -301,6 +310,8 @@ object ChainGeneses { const val ZEITGEIST = "1bf2a2ecb4a868de66ea8610f2ce7c8c43706561b6476031315f6640fe38e060" const val WESTMINT = "67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9" + + const val HYDRA_DX = "afdc188f45c71dacbaa0b62e16a91f726c7b8699a9748cdf715459de6b7f366d" } object ChainIds { @@ -337,6 +348,10 @@ fun Chain.Asset.requireOrml(): Type.Orml { return type } +fun Chain.Asset.ormlOrNull(): Type.Orml? { + return type as? Type.Orml +} + fun Chain.Asset.requireErc20(): Type.EvmErc20 { require(type is Type.EvmErc20) @@ -376,5 +391,10 @@ fun Chain.findAssetByOrmlCurrencyId(runtime: RuntimeSnapshot, currencyId: Any?): } } +fun Type.Orml.decodeOrNull(runtime: RuntimeSnapshot): Any? { + val currencyType = runtime.typeRegistry[currencyIdType] ?: return null + return currencyType.fromHexOrNull(runtime, currencyIdScale) +} + val Chain.Asset.localId: AssetAndChainId get() = AssetAndChainId(chainId, id) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt index 5c390069b3..685bc355d2 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/MutableEventQueue.kt @@ -52,6 +52,13 @@ internal inline fun MutableEventQueue.takeFromEndOrThrow(vararg eventTypes: Even } } +@Suppress("NOTHING_TO_INLINE") +internal inline fun EventQueue.indexOfLastOrThrow(vararg eventTypes: Event, endExclusive: Int): Int { + return requireNotNull(indexOfLast(*eventTypes, endExclusive = endExclusive)) { + "No required event found for types ${eventTypes.joinToString { it.name }}" + } +} + data class EventWithIndex(val event: GenericEvent.Instance, val eventIndex: Int) class RealEventQueue(event: List) : MutableEventQueue { @@ -112,6 +119,8 @@ class RealEventQueue(event: List) : MutableEventQueue { } private fun removeAllAfterInclusive(index: Int): List { + if (index > this.events.size) return emptyList() + val subList = this.events.subList(index, this.events.size) val subListCopy = subList.toList() diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt index 65a5064519..882fb4c406 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/RealExtrinsicWalk.kt @@ -3,6 +3,8 @@ package io.novafoundation.nova.runtime.extrinsic.visitor.impl import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisitor import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.batch.BatchAllNode +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.batch.ForceBatchNode import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.proxy.ProxyNode import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId @@ -22,7 +24,7 @@ internal class RealExtrinsicWalk( companion object { - fun defaultNodes() = listOf(ProxyNode()) + fun defaultNodes() = listOf(ProxyNode(), BatchAllNode(), ForceBatchNode()) } override suspend fun walk( diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/BatchAllNode.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/BatchAllNode.kt new file mode 100644 index 0000000000..f1b2c01188 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/BatchAllNode.kt @@ -0,0 +1,77 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.batch + +import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCallList +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.EventCountingContext +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.NestedCallNode +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.VisitingContext +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.indexOfLastOrThrow +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +internal class BatchAllNode : NestedCallNode { + + override fun canVisit(call: GenericCall.Instance): Boolean { + return call.module.name == Modules.UTILITY && call.function.name == "batch_all" + } + + override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int { + val innerCalls = bindGenericCallList(call.arguments["calls"]) + + val batchCompletedEventType = context.runtime.batchCompletedEvent() + val itemCompletedEventType = context.runtime.itemCompletedEvent() + + var endExclusive = context.endExclusive + + // Safe since `endExclusiveToSkipInternalEvents` should not be called on failed items + val indexOfCompletedEvent = context.eventQueue.indexOfLastOrThrow(batchCompletedEventType, endExclusive = endExclusive) + endExclusive = indexOfCompletedEvent + + innerCalls.reversed().forEach { innerCall -> + val itemIdx = context.eventQueue.indexOfLastOrThrow(itemCompletedEventType, endExclusive = endExclusive) + endExclusive = context.endExclusiveToSkipInternalEvents(innerCall, itemIdx) + } + + return endExclusive + } + + override fun visit(call: GenericCall.Instance, context: VisitingContext) { + val innerCalls = bindGenericCallList(call.arguments["calls"]) + val itemCompletedEventType = context.runtime.itemCompletedEvent() + + context.logger.info("Visiting utility.batchAll with ${innerCalls.size} inner calls") + + if (context.callSucceeded) { + context.logger.info("BatchAll succeeded") + } else { + context.logger.info("BatchAll failed") + } + + val subItemsToVisit = innerCalls.reversed().map { innerCall -> + if (context.callSucceeded) { + context.eventQueue.popFromEnd(itemCompletedEventType) + val alNestedEvents = context.takeCompletedBatchItemEvents(innerCall) + + ExtrinsicVisit( + rootExtrinsic = context.rootExtrinsic, + call = innerCall, + success = true, + events = alNestedEvents, + origin = context.origin + ) + } else { + ExtrinsicVisit( + rootExtrinsic = context.rootExtrinsic, + call = innerCall, + success = false, + events = emptyList(), + origin = context.origin + ) + } + } + + subItemsToVisit.forEach { subItem -> + context.nestedVisit(subItem) + } + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/Common.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/Common.kt new file mode 100644 index 0000000000..634967f2c2 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/Common.kt @@ -0,0 +1,38 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.batch + +import io.novafoundation.nova.common.utils.utility +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.VisitingContext +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent +import jp.co.soramitsu.fearless_utils.runtime.metadata.event +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event + +internal fun RuntimeSnapshot.batchCompletedEvent(): Event { + return metadata.utility().event("BatchCompleted") +} + +internal fun RuntimeSnapshot.batchCompletedWithErrorsEvent(): Event { + return metadata.utility().event("BatchCompletedWithErrors") +} + +internal fun RuntimeSnapshot.itemCompletedEvent(): Event { + return metadata.utility().event("ItemCompleted") +} + +internal fun RuntimeSnapshot.itemFailedEvent(): Event { + return metadata.utility().event("ItemFailed") +} + +internal fun VisitingContext.takeCompletedBatchItemEvents(call: GenericCall.Instance): List { + val internalEventsEndExclusive = endExclusiveToSkipInternalEvents(call) + + // internalEnd is exclusive => it holds index of last internal event + // thus, we delete them inclusively + val someOfNestedEvents = eventQueue.takeAllAfterInclusive(internalEventsEndExclusive) + + // now it is safe to go until ItemCompleted\ItemFailed since we removed all potential nested events above + val remainingNestedEvents = eventQueue.takeTail(runtime.itemCompletedEvent(), runtime.itemFailedEvent()) + + return remainingNestedEvents + someOfNestedEvents +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/ForceBatchNode.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/ForceBatchNode.kt new file mode 100644 index 0000000000..5430714a81 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/nodes/batch/ForceBatchNode.kt @@ -0,0 +1,99 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.batch + +import io.novafoundation.nova.common.data.network.runtime.binding.bindGenericCallList +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.common.utils.instanceOf +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.EventCountingContext +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.NestedCallNode +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.VisitingContext +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.indexOfLastOrThrow +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.peekItemFromEndOrThrow +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.takeFromEndOrThrow +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall + +internal class ForceBatchNode : NestedCallNode { + + override fun canVisit(call: GenericCall.Instance): Boolean { + return call.module.name == Modules.UTILITY && call.function.name == "force_batch" + } + + override fun endExclusiveToSkipInternalEvents(call: GenericCall.Instance, context: EventCountingContext): Int { + val innerCalls = bindGenericCallList(call.arguments["calls"]) + + val batchCompletedEventType = context.runtime.batchCompletedEvent() + val batchCompletedWithErrorsType = context.runtime.batchCompletedWithErrorsEvent() + + val itemCompletedEventType = context.runtime.itemCompletedEvent() + val itemFailedEventType = context.runtime.itemFailedEvent() + + var endExclusive = context.endExclusive + + // Safe since batch all always completes + val indexOfCompletedEvent = context.eventQueue.indexOfLastOrThrow(batchCompletedEventType, batchCompletedWithErrorsType, endExclusive = endExclusive) + endExclusive = indexOfCompletedEvent + + innerCalls.reversed().forEach { innerCall -> + val (itemEvent, itemEventIdx) = context.eventQueue.peekItemFromEndOrThrow(itemCompletedEventType, itemFailedEventType, endExclusive = endExclusive) + + endExclusive = if (itemEvent.instanceOf(itemCompletedEventType)) { + // only completed items emit nested events + context.endExclusiveToSkipInternalEvents(innerCall, itemEventIdx) + } else { + itemEventIdx + } + } + + return endExclusive + } + + override fun visit(call: GenericCall.Instance, context: VisitingContext) { + val innerCalls = bindGenericCallList(call.arguments["calls"]) + + val batchCompletedEventType = context.runtime.batchCompletedEvent() + val batchCompletedWithErrorsType = context.runtime.batchCompletedWithErrorsEvent() + + val itemCompletedEventType = context.runtime.itemCompletedEvent() + val itemFailedEventType = context.runtime.itemFailedEvent() + + context.logger.info("Visiting utility.forceBatch with ${innerCalls.size} inner calls") + + if (context.callSucceeded) { + context.logger.info("ForceBatch succeeded") + + context.eventQueue.popFromEnd(batchCompletedEventType, batchCompletedWithErrorsType) + } else { + context.logger.info("ForceBatch failed") + } + + val subItemsToVisit = innerCalls.reversed().map { innerCall -> + if (context.callSucceeded) { + val itemEvent = context.eventQueue.takeFromEndOrThrow(itemCompletedEventType, itemFailedEventType) + + if (itemEvent.instanceOf(itemCompletedEventType)) { + val allEvents = context.takeCompletedBatchItemEvents(innerCall) + + return@map ExtrinsicVisit( + rootExtrinsic = context.rootExtrinsic, + call = innerCall, + success = true, + events = allEvents, + origin = context.origin + ) + } + } + + ExtrinsicVisit( + rootExtrinsic = context.rootExtrinsic, + call = innerCall, + success = false, + events = emptyList(), + origin = context.origin + ) + } + + subItemsToVisit.forEach { subItem -> + context.nestedVisit(subItem) + } + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt index f79518b3d5..2ec31b2f11 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/ChainMappers.kt @@ -361,6 +361,10 @@ private fun mapGovernanceListFromLocal(governanceLocal: String) = governanceLoca runCatching { Chain.Governance.valueOf(it) }.getOrNull() } -private fun mapSwapListFromLocal(swapLocal: String) = swapLocal.split(",").mapNotNull { - enumValueOfOrNull(swapLocal) +private fun mapSwapListFromLocal(swapLocal: String): List { + if (swapLocal.isEmpty()) return emptyList() + + return swapLocal.split(",").mapNotNull { + enumValueOfOrNull(swapLocal) + } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt index 2a3786b106..33fb752ce7 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/mappers/RemoteToLocalChainMappers.kt @@ -22,6 +22,7 @@ private const val CROWDLOAN_OPTION = "crowdloans" private const val TESTNET_OPTION = "testnet" private const val PROXY_OPTION = "proxy" private const val SWAP_HUB = "swap-hub" +private const val HYDRA_DX_SWAPS = "hydradx-swaps" private const val NO_SUBSTRATE_RUNTIME = "noSubstrateRuntime" private const val FULL_SYNC_BY_DEFAULT = "fullSyncByDefault" @@ -250,6 +251,7 @@ private fun Set.swapTypesFromOptions(): List { return mapNotNull { option -> when (option) { SWAP_HUB -> Chain.Swap.ASSET_CONVERSION + HYDRA_DX_SWAPS -> Chain.Swap.HYDRA_DX else -> null } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt index 859a1753f0..d45a9e7bec 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/chain/model/Chain.kt @@ -186,7 +186,7 @@ data class Chain( } enum class Swap { - ASSET_CONVERSION + ASSET_CONVERSION, HYDRA_DX } enum class ConnectionState { diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt index 95dd5b6dc4..aefe21ddb1 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/repository/ExtrinsicWithEvents.kt @@ -65,6 +65,10 @@ fun List.findEvent(module: String, event: String): Generi return find { it.instanceOf(module, event) } } +fun List.findLastEvent(module: String, event: String): GenericEvent.Instance? { + return findLast { it.instanceOf(module, event) } +} + fun List.hasEvent(module: String, event: String): Boolean { return any { it.instanceOf(module, event) } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableModule.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableModule.kt index 69fb2ca1d8..760fc80923 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableModule.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableModule.kt @@ -5,6 +5,7 @@ import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module import jp.co.soramitsu.fearless_utils.runtime.metadata.storage typealias QueryableStorageKeyBinder = (keyInstance: Any) -> K +typealias QueryableStorageKeyBinder2 = (keyInstance: Any) -> Pair interface QueryableModule { @@ -24,3 +25,10 @@ fun QueryableModule.storage1( ): QueryableStorageEntry1 { return RealQueryableStorageEntry1(module.storage(name), binding, keyBinding) } +context(StorageQueryContext) +fun QueryableModule.storage2( + name: String, + binding: QueryableStorageBinder2, +): QueryableStorageEntry2 { + return RealQueryableStorageEntry2(module.storage(name), binding) +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableStorageEntry1.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableStorageEntry1.kt index fb57c8ff5c..0ef0e6c429 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableStorageEntry1.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableStorageEntry1.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.runtime.storage.source.query.api +import io.novafoundation.nova.runtime.storage.source.query.StorageKeyComponents import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext import io.novafoundation.nova.runtime.storage.source.query.WithRawValue import io.novafoundation.nova.runtime.storage.source.query.wrapSingleArgumentKeys @@ -14,6 +15,9 @@ interface QueryableStorageEntry1 { context(StorageQueryContext) suspend fun keys(): List + context(StorageQueryContext) + suspend fun entries(): Map + context(StorageQueryContext) suspend fun query(argument: I): T? @@ -95,13 +99,25 @@ internal class RealQueryableStorageEntry1( context(StorageQueryContext) override suspend fun keys(): List { - return storageEntry.keys().map { (firstKey: Any?) -> - @Suppress("UNCHECKED_CAST") - if (firstKey != null && keyBinding != null) { - keyBinding.invoke(firstKey) - } else { - firstKey as I - } + return storageEntry.keys().map(::bindKey) + } + + context(StorageQueryContext) + override suspend fun entries(): Map { + return storageEntry.entries( + keyExtractor = ::bindKey, + binding = { decoded, key -> decoded?.let { binding(it, key) } as T } + ) + } + + private fun bindKey(storageKeyComponents: StorageKeyComponents): I { + val firstComponent = storageKeyComponents.component1() + + @Suppress("UNCHECKED_CAST") + return if (firstComponent != null && keyBinding != null) { + keyBinding.invoke(firstComponent) + } else { + firstComponent as I } } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableStorageEntry2.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableStorageEntry2.kt new file mode 100644 index 0000000000..d5a8b4e50c --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/source/query/api/QueryableStorageEntry2.kt @@ -0,0 +1,24 @@ +package io.novafoundation.nova.runtime.storage.source.query.api + +import io.novafoundation.nova.runtime.storage.source.query.StorageQueryContext +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.StorageEntry +import kotlinx.coroutines.flow.Flow + +typealias QueryableStorageBinder2 = (dynamicInstance: Any, key1: K1, key2: K2) -> V + +interface QueryableStorageEntry2 { + + context(StorageQueryContext) + fun observe(argument1: I1, argument2: I2): Flow +} + +internal class RealQueryableStorageEntry2( + private val storageEntry: StorageEntry, + private val binding: QueryableStorageBinder2, +) : QueryableStorageEntry2 { + + context(StorageQueryContext) + override fun observe(argument1: I1, argument2: I2): Flow { + return storageEntry.observe(argument1, argument2, binding = { decoded -> decoded?.let { binding(it, argument1, argument2) } }) + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/api/SystemRuntimeApi.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/typed/SystemRuntimeApi.kt similarity index 61% rename from feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/api/SystemRuntimeApi.kt rename to runtime/src/main/java/io/novafoundation/nova/runtime/storage/typed/SystemRuntimeApi.kt index baaee2493e..8c7d0244fd 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/network/blockhain/api/SystemRuntimeApi.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/storage/typed/SystemRuntimeApi.kt @@ -1,11 +1,16 @@ -package io.novafoundation.nova.feature_staking_impl.data.network.blockhain.api +package io.novafoundation.nova.runtime.storage.typed +import io.novafoundation.nova.common.data.network.runtime.binding.AccountInfo +import io.novafoundation.nova.common.data.network.runtime.binding.bindAccountInfo import io.novafoundation.nova.common.data.network.runtime.binding.bindBlockNumber import io.novafoundation.nova.common.utils.system 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.QueryableStorageEntry0 +import io.novafoundation.nova.runtime.storage.source.query.api.QueryableStorageEntry1 import io.novafoundation.nova.runtime.storage.source.query.api.storage0 +import io.novafoundation.nova.runtime.storage.source.query.api.storage1 +import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module import java.math.BigInteger @@ -20,3 +25,7 @@ val RuntimeMetadata.system: SystemRuntimeApi context(StorageQueryContext) val SystemRuntimeApi.number: QueryableStorageEntry0 get() = storage0("Number", binding = ::bindBlockNumber) + +context(StorageQueryContext) +val SystemRuntimeApi.account: QueryableStorageEntry1 + get() = storage1("Account", binding = { decoded, _ -> bindAccountInfo(decoded) }) diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/BatchAllWalkTest.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/BatchAllWalkTest.kt new file mode 100644 index 0000000000..50e2acffe6 --- /dev/null +++ b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/BatchAllWalkTest.kt @@ -0,0 +1,233 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.batch.BatchAllNode +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider +import io.novafoundation.nova.test_shared.any +import io.novafoundation.nova.test_shared.whenever +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent +import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +internal class BatchAllWalkTest { + + @Mock + private lateinit var runtimeProvider: RuntimeProvider + + @Mock + private lateinit var chainRegistry: ChainRegistry + + @Mock + private lateinit var runtime: RuntimeSnapshot + + @Mock + private lateinit var metadata: RuntimeMetadata + + @Mock + private lateinit var utilityModule: Module + + private lateinit var extrinsicWalk: ExtrinsicWalk + + private val itemCompletedType = Event("ItemCompleted", index = 0 to 0, documentation = emptyList(), arguments = emptyList()) + private val batchCompletedType = Event("BatchCompleted", index = 0 to 1, documentation = emptyList(), arguments = emptyList()) + private val itemFailedType = Event("ItemFailed", index = 0 to 2, documentation = emptyList(), arguments = emptyList()) + + private val signer = byteArrayOf(0x00) + + private val testModuleMocker = TestModuleMocker() + private val testEvent = testModuleMocker.testEvent + private val testInnerCall = testModuleMocker.testInnerCall + + @Before + fun setup() = runBlocking { + whenever(utilityModule.events).thenReturn( + mapOf( + batchCompletedType.name to batchCompletedType, + itemCompletedType.name to itemCompletedType, + itemFailedType.name to itemFailedType + ) + ) + whenever(metadata.modules).thenReturn(mapOf("Utility" to utilityModule)) + whenever(runtime.metadata).thenReturn(metadata) + whenever(runtimeProvider.get()).thenReturn(runtime) + whenever(chainRegistry.getRuntimeProvider(any())).thenReturn(runtimeProvider) + + extrinsicWalk = RealExtrinsicWalk(chainRegistry, knownNodes = listOf(BatchAllNode())) + } + + @Test + fun shouldVisitSucceededSingleBatchedCall() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = innerBatchEvents + listOf(itemCompleted(), batchCompleted(), extrinsicSuccess()) + + val extrinsic = createExtrinsic( + call = batchCall(testInnerCall), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(true, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerBatchEvents, visit.events) + } + + @Test + fun shouldVisitFailedSingleBatchedCall() = runBlocking { + val events = listOf(extrinsicFailed()) + + val extrinsic = createExtrinsic( + call = batchCall(testInnerCall), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(false, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(true, visit.events.isEmpty()) + } + + @Test + fun shouldVisitSucceededMultipleBatchedCalls() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = buildList { + addAll(innerBatchEvents) + add(itemCompleted()) + + addAll(innerBatchEvents) + add(itemCompleted()) + + add(batchCompleted()) + add(extrinsicSuccess()) + } + + val extrinsic = createExtrinsic( + call = batchCall(testInnerCall, testInnerCall), + events = events + ) + + val visits = extrinsicWalk.walkMultiple(extrinsic, expectedSize = 2) + visits.forEach { visit -> + assertEquals(true, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerBatchEvents, visit.events) + } + } + + @Test + fun shouldVisitFailedMultipleBatchedCalls() = runBlocking { + val events = listOf(extrinsicFailed()) + + val extrinsic = createExtrinsic( + call = batchCall(testInnerCall, testInnerCall), + events = events + ) + + val visits = extrinsicWalk.walkMultiple(extrinsic, expectedSize = 2) + + visits.forEach { visit -> + assertEquals(false, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(true, visit.events.isEmpty()) + } + } + + @Test + fun shouldVisitNestedBatches() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = buildList { + // first level batch starts + addAll(innerBatchEvents) + add(itemCompleted()) + + run { + addAll(innerBatchEvents) + add(itemCompleted()) + + run { + addAll(innerBatchEvents) + add(itemCompleted()) + + addAll(innerBatchEvents) + add(itemCompleted()) + + add(batchCompleted()) + } + add(itemCompleted()) + + add(batchCompleted()) + } + add(itemCompleted()) + + addAll(innerBatchEvents) + add(itemCompleted()) + + // first leve batch ends + add(batchCompleted()) + add(extrinsicSuccess()) + } + + val extrinsic = createExtrinsic( + call = batchCall( + testInnerCall, + batchCall( + testInnerCall, + batchCall( + testInnerCall, + testInnerCall + ) + ), + testInnerCall + ), + events = events + ) + + val visits = extrinsicWalk.walkMultiple(extrinsic, expectedSize = 5) + visits.forEach { visit -> + assertEquals(true, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerBatchEvents, visit.events) + } + } + + private fun createExtrinsic( + call: GenericCall.Instance, + events: List + ) = createExtrinsic(signer, call, events) + + private fun itemCompleted(): GenericEvent.Instance { + return GenericEvent.Instance(utilityModule, itemCompletedType, arguments = emptyList()) + } + + private fun batchCompleted(): GenericEvent.Instance { + return GenericEvent.Instance(utilityModule, batchCompletedType, arguments = emptyList()) + } + + private fun batchCall(vararg innerCalls: GenericCall.Instance): GenericCall.Instance { + return mockCall( + moduleName = Modules.UTILITY, + callName = "batch_all", + arguments = mapOf( + "calls" to innerCalls.toList() + ) + ) + } +} diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/Common.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/Common.kt new file mode 100644 index 0000000000..67e2613282 --- /dev/null +++ b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/Common.kt @@ -0,0 +1,132 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress +import io.novafoundation.nova.common.data.network.runtime.binding.bindMultiAddress +import io.novafoundation.nova.runtime.ext.Geneses +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.api.walkToList +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents +import io.novafoundation.nova.test_shared.whenever +import jp.co.soramitsu.fearless_utils.runtime.AccountId +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent +import jp.co.soramitsu.fearless_utils.runtime.metadata.call +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.MetadataFunction +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module +import org.junit.Assert +import org.mockito.Mockito +import java.math.BigInteger + +fun createTestModuleWithCall( + moduleName: String, + callName: String +): Module { + return Module( + name = moduleName, + storage = null, + calls = mapOf( + callName to MetadataFunction( + name = callName, + arguments = emptyList(), + documentation = emptyList(), + index = 0 to 0 + ) + ), + events = emptyMap(), + constants = emptyMap(), + errors = emptyMap(), + index = BigInteger.ZERO + ) +} + +fun createExtrinsic( + signer: AccountId, + call: GenericCall.Instance, + events: List +) = ExtrinsicWithEvents( + extrinsic = Extrinsic.DecodedInstance( + signature = Extrinsic.Signature( + accountIdentifier = bindMultiAddress(MultiAddress.Id(signer)), + signature = null, + signedExtras = emptyMap() + ), + call = call, + ), + extrinsicHash = "0x", + events = events +) + +fun extrinsicSuccess(): GenericEvent.Instance { + return mockEvent("System", "ExtrinsicSuccess") +} + +fun extrinsicFailed(): GenericEvent.Instance { + return mockEvent("System", "ExtrinsicFailed") +} + +fun mockEvent(moduleName: String, eventName: String, arguments: List = emptyList()): GenericEvent.Instance { + val module = Mockito.mock(Module::class.java) + whenever(module.name).thenReturn(moduleName) + + val event = Mockito.mock(Event::class.java) + whenever(event.name).thenReturn(eventName) + + return GenericEvent.Instance( + module = module, + event = event, + arguments = arguments + ) +} + +fun mockCall(moduleName: String, callName: String, arguments: Map = emptyMap()): GenericCall.Instance { + val module = Mockito.mock(Module::class.java) + whenever(module.name).thenReturn(moduleName) + + val function = Mockito.mock(MetadataFunction::class.java) + whenever(function.name).thenReturn(callName) + + return GenericCall.Instance( + module = module, + function = function, + arguments = arguments + ) +} + +class TestModuleMocker { + + val testModule = createTestModuleWithCall(moduleName = "Test", callName = "test") + + val testInnerCall = GenericCall.Instance( + module = testModule, + function = testModule.call("test"), + arguments = emptyMap() + ) + + val testEvent = mockEvent(testModule.name, "test") + + operator fun component1(): GenericCall.Instance { + return testInnerCall + } + + operator fun component2():GenericEvent.Instance { + return testEvent + } +} + +suspend fun ExtrinsicWalk.walkSingle(extrinsicWithEvents: ExtrinsicWithEvents): ExtrinsicVisit { + val visits = walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT) + Assert.assertEquals(1, visits.size) + + return visits.single() +} + +suspend fun ExtrinsicWalk.walkMultiple(extrinsicWithEvents: ExtrinsicWithEvents, expectedSize: Int): List { + val visits = walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT) + Assert.assertEquals(expectedSize, visits.size) + + return visits +} diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ForceBatchWalkTest.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ForceBatchWalkTest.kt new file mode 100644 index 0000000000..26b2a5e829 --- /dev/null +++ b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ForceBatchWalkTest.kt @@ -0,0 +1,263 @@ +package io.novafoundation.nova.runtime.extrinsic.visitor.impl + +import io.novafoundation.nova.common.utils.Modules +import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk +import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.batch.ForceBatchNode +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider +import io.novafoundation.nova.test_shared.any +import io.novafoundation.nova.test_shared.whenever +import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall +import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent +import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event +import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +internal class ForceBatchWalkTest { + + @Mock + private lateinit var runtimeProvider: RuntimeProvider + + @Mock + private lateinit var chainRegistry: ChainRegistry + + @Mock + private lateinit var runtime: RuntimeSnapshot + + @Mock + private lateinit var metadata: RuntimeMetadata + + @Mock + private lateinit var utilityModule: Module + + private lateinit var extrinsicWalk: ExtrinsicWalk + + private val itemCompletedType = Event("ItemCompleted", index = 0 to 0, documentation = emptyList(), arguments = emptyList()) + private val batchCompletedType = Event("BatchCompleted", index = 0 to 1, documentation = emptyList(), arguments = emptyList()) + private val itemFailedType = Event("ItemFailed", index = 0 to 2, documentation = emptyList(), arguments = emptyList()) + private val batchCompletedWithErrorsType = Event("BatchCompletedWithErrors", index = 0 to 3, documentation = emptyList(), arguments = emptyList()) + + private val signer = byteArrayOf(0x00) + + private val testModuleMocker = TestModuleMocker() + private val testEvent = testModuleMocker.testEvent + private val testInnerCall = testModuleMocker.testInnerCall + + @Before + fun setup() = runBlocking { + whenever(utilityModule.events).thenReturn( + mapOf( + batchCompletedType.name to batchCompletedType, + itemCompletedType.name to itemCompletedType, + itemFailedType.name to itemFailedType, + batchCompletedWithErrorsType.name to batchCompletedWithErrorsType + ) + ) + whenever(metadata.modules).thenReturn(mapOf("Utility" to utilityModule)) + whenever(runtime.metadata).thenReturn(metadata) + whenever(runtimeProvider.get()).thenReturn(runtime) + whenever(chainRegistry.getRuntimeProvider(any())).thenReturn(runtimeProvider) + + extrinsicWalk = RealExtrinsicWalk(chainRegistry, knownNodes = listOf(ForceBatchNode())) + } + + @Test + fun shouldVisitSucceededSingleBatchedCall() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = innerBatchEvents + listOf(itemCompleted(), batchCompleted(), extrinsicSuccess()) + + val extrinsic = createExtrinsic( + call = forceBatchCall(testInnerCall), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(true, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerBatchEvents, visit.events) + } + + @Test + fun shouldVisitFailedSingleBatchedCall() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = innerBatchEvents + listOf(itemFailed(), batchCompletedWithErrors(), extrinsicSuccess()) + + val extrinsic = createExtrinsic( + call = forceBatchCall(testInnerCall), + events = events + ) + + val visit = extrinsicWalk.walkSingle(extrinsic) + assertEquals(false, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(true, visit.events.isEmpty()) + } + + @Test + fun shouldVisitSucceededMultipleBatchedCalls() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = buildList { + addAll(innerBatchEvents) + add(itemCompleted()) + + addAll(innerBatchEvents) + add(itemCompleted()) + + add(batchCompleted()) + add(extrinsicSuccess()) + } + + val extrinsic = createExtrinsic( + call = forceBatchCall(testInnerCall, testInnerCall), + events = events + ) + + val visits = extrinsicWalk.walkMultiple(extrinsic, expectedSize = 2) + visits.forEach { visit -> + assertEquals(true, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerBatchEvents, visit.events) + } + } + + @Test + fun shouldVisitMixedMultipleBatchedCalls() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = buildList { + addAll(innerBatchEvents) + add(itemCompleted()) + + add(itemFailed()) + + addAll(innerBatchEvents) + add(itemCompleted()) + + add(batchCompletedWithErrors()) + add(extrinsicSuccess()) + } + + val extrinsic = createExtrinsic( + call = forceBatchCall(testInnerCall, testInnerCall, testInnerCall), + events = events + ) + + val visits = extrinsicWalk.walkMultiple(extrinsic, expectedSize = 3) + + val expected: List>> = listOf( + true to innerBatchEvents, + false to emptyList(), + true to innerBatchEvents + ) + + visits.zip(expected).forEach { (visit, expected) -> + val (expectedSuccess, expectedEvents) = expected + + assertEquals(expectedSuccess, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(expectedEvents, visit.events) + } + } + + @Test + fun shouldVisitNestedBatches() = runBlocking { + val innerBatchEvents = listOf(testEvent) + val events = buildList { + addAll(innerBatchEvents) + add(itemCompleted()) + + run { + addAll(innerBatchEvents) + add(itemCompleted()) + + run { + addAll(innerBatchEvents) + add(itemCompleted()) + + addAll(innerBatchEvents) + add(itemCompleted()) + + add(batchCompleted()) + } + add(itemCompleted()) + + add(batchCompleted()) + } + add(itemCompleted()) + + addAll(innerBatchEvents) + add(itemCompleted()) + + // first leve batch ends + add(batchCompleted()) + add(extrinsicSuccess()) + } + + val extrinsic = createExtrinsic( + call = forceBatchCall( + testInnerCall, + forceBatchCall( + testInnerCall, + forceBatchCall( + testInnerCall, + testInnerCall + ) + ), + testInnerCall + ), + events = events + ) + + val visits = extrinsicWalk.walkMultiple(extrinsic, expectedSize = 5) + visits.forEach { visit -> + assertEquals(true, visit.success) + assertArrayEquals(signer, visit.origin) + assertEquals(testInnerCall, visit.call) + assertEquals(innerBatchEvents, visit.events) + } + } + + private fun createExtrinsic( + call: GenericCall.Instance, + events: List + ) = createExtrinsic(signer, call, events) + + private fun itemCompleted(): GenericEvent.Instance { + return GenericEvent.Instance(utilityModule, itemCompletedType, arguments = emptyList()) + } + + private fun itemFailed(): GenericEvent.Instance { + return GenericEvent.Instance(utilityModule, itemFailedType, arguments = emptyList()) + } + + private fun batchCompleted(): GenericEvent.Instance { + return GenericEvent.Instance(utilityModule, batchCompletedType, arguments = emptyList()) + } + + private fun batchCompletedWithErrors(): GenericEvent.Instance { + return GenericEvent.Instance(utilityModule, batchCompletedWithErrorsType, arguments = emptyList()) + } + + private fun forceBatchCall(vararg innerCalls: GenericCall.Instance): GenericCall.Instance { + return mockCall( + moduleName = Modules.UTILITY, + callName = "force_batch", + arguments = mapOf( + "calls" to innerCalls.toList() + ) + ) + } +} diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt index b939390bcd..4a58ae7ce1 100644 --- a/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt +++ b/runtime/src/test/java/io/novafoundation/nova/runtime/extrinsic/visitor/impl/ProxyWalkTest.kt @@ -2,27 +2,19 @@ package io.novafoundation.nova.runtime.extrinsic.visitor.impl import io.novafoundation.nova.common.data.network.runtime.binding.MultiAddress import io.novafoundation.nova.common.data.network.runtime.binding.bindMultiAddress -import io.novafoundation.nova.runtime.ext.Geneses -import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicVisit import io.novafoundation.nova.runtime.extrinsic.visitor.api.ExtrinsicWalk -import io.novafoundation.nova.runtime.extrinsic.visitor.api.walkToList import io.novafoundation.nova.runtime.extrinsic.visitor.impl.nodes.proxy.ProxyNode import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProvider -import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.ExtrinsicWithEvents import io.novafoundation.nova.test_shared.any import io.novafoundation.nova.test_shared.whenever import jp.co.soramitsu.fearless_utils.runtime.AccountId import jp.co.soramitsu.fearless_utils.runtime.RuntimeSnapshot import jp.co.soramitsu.fearless_utils.runtime.definitions.types.composite.DictEnum -import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.Extrinsic import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericCall import jp.co.soramitsu.fearless_utils.runtime.definitions.types.generics.GenericEvent import jp.co.soramitsu.fearless_utils.runtime.metadata.RuntimeMetadata -import jp.co.soramitsu.fearless_utils.runtime.metadata.call import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Event -import jp.co.soramitsu.fearless_utils.runtime.metadata.module.MetadataFunction import jp.co.soramitsu.fearless_utils.runtime.metadata.module.Module import junit.framework.Assert.assertEquals import kotlinx.coroutines.runBlocking @@ -31,9 +23,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito import org.mockito.junit.MockitoJUnitRunner -import java.math.BigInteger @RunWith(MockitoJUnitRunner::class) internal class ProxyWalkTest { @@ -60,12 +50,9 @@ internal class ProxyWalkTest { private val proxy = byteArrayOf(0x00) private val proxied = byteArrayOf(0x01) - private val testModule = createTestModuleWithCall(moduleName = "Test", callName = "test") - private val testInnerCall = GenericCall.Instance( - module = testModule, - function = testModule.call("test"), - arguments = emptyMap() - ) + private val testModuleMocker = TestModuleMocker() + private val testEvent = testModuleMocker.testEvent + private val testInnerCall = testModuleMocker.testInnerCall @Before fun setup() = runBlocking { @@ -80,7 +67,7 @@ internal class ProxyWalkTest { @Test fun shouldVisitSucceededSimpleCall() = runBlocking { - val events = listOf(testEvent(), extrinsicSuccess()) + val events = listOf(testEvent, extrinsicSuccess()) val extrinsic = createExtrinsic( signer = proxied, @@ -114,7 +101,7 @@ internal class ProxyWalkTest { @Test fun shouldVisitSucceededSingleProxyCall() = runBlocking { - val innerProxyEvents = listOf(testEvent()) + val innerProxyEvents = listOf(testEvent) val events = innerProxyEvents + listOf(proxyExecuted(success = true), extrinsicSuccess()) val extrinsic = createExtrinsic( @@ -156,7 +143,7 @@ internal class ProxyWalkTest { @Test fun shouldVisitSucceededMultipleProxyCalls() = runBlocking { - val innerProxyEvents = listOf(testEvent()) + val innerProxyEvents = listOf(testEvent) val events = innerProxyEvents + listOf(proxyExecuted(success = true), proxyExecuted(success = true), proxyExecuted(success = true), extrinsicSuccess()) val proxy1 = byteArrayOf(0x00) @@ -218,64 +205,6 @@ internal class ProxyWalkTest { assertEquals(innerProxyEvents, visit.events) } - private suspend fun ExtrinsicWalk.walkToList(extrinsicWithEvents: ExtrinsicWithEvents): List { - return walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT) - } - - private suspend fun ExtrinsicWalk.walkSingle(extrinsicWithEvents: ExtrinsicWithEvents): ExtrinsicVisit { - val visits = walkToList(extrinsicWithEvents, Chain.Geneses.POLKADOT) - assertEquals(1, visits.size) - - return visits.single() - } - - private fun createExtrinsic( - signer: AccountId, - call: GenericCall.Instance, - events: List - ) = ExtrinsicWithEvents( - extrinsic = Extrinsic.DecodedInstance( - signature = Extrinsic.Signature( - accountIdentifier = bindMultiAddress(MultiAddress.Id(signer)), - signature = null, - signedExtras = emptyMap() - ), - call = call, - ), - extrinsicHash = "0x", - events = events - ) - - private fun createTestModuleWithCall( - moduleName: String, - callName: String - ): Module { - return Module( - name = moduleName, - storage = null, - calls = mapOf( - callName to MetadataFunction( - name = callName, - arguments = emptyList(), - documentation = emptyList(), - index = 0 to 0 - ) - ), - events = emptyMap(), - constants = emptyMap(), - errors = emptyMap(), - index = BigInteger.ZERO - ) - } - - private fun extrinsicSuccess(): GenericEvent.Instance { - return mockEvent("System", "ExtrinsicSuccess") - } - - private fun extrinsicFailed(): GenericEvent.Instance { - return mockEvent("System", "ExtrinsicFailed") - } - private fun proxyExecuted(success: Boolean): GenericEvent.Instance { val outcomeVariant = if (success) "Ok" else "Err" val outcome = DictEnum.Entry(name = outcomeVariant, value = null) @@ -283,11 +212,6 @@ internal class ProxyWalkTest { return GenericEvent.Instance(proxyModule, proxyExecutedType, arguments = listOf(outcome)) } - - private fun testEvent(): GenericEvent.Instance { - return mockEvent(testModule.name, "test") - } - private fun proxyCall(real: AccountId, innerCall: GenericCall.Instance): GenericCall.Instance { return mockCall( moduleName = "Proxy", @@ -299,32 +223,4 @@ internal class ProxyWalkTest { ) ) } - - private fun mockEvent(moduleName: String, eventName: String, arguments: List = emptyList()): GenericEvent.Instance { - val module = Mockito.mock(Module::class.java) - whenever(module.name).thenReturn(moduleName) - - val event = Mockito.mock(Event::class.java) - whenever(event.name).thenReturn(eventName) - - return GenericEvent.Instance( - module = module, - event = event, - arguments = arguments - ) - } - - private fun mockCall(moduleName: String, callName: String, arguments: Map = emptyMap()): GenericCall.Instance { - val module = Mockito.mock(Module::class.java) - whenever(module.name).thenReturn(moduleName) - - val function = Mockito.mock(MetadataFunction::class.java) - whenever(function.name).thenReturn(callName) - - return GenericCall.Instance( - module = module, - function = function, - arguments = arguments - ) - } } diff --git a/settings.gradle b/settings.gradle index dbe2e34e9b..b22f2976bf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,5 +36,6 @@ include ':feature-swap-api' include ':feature-swap-impl' include ':feature-buy-api' include ':feature-buy-impl' +include ':hydra-dx-math' include ':feature-proxy-impl' include ':feature-proxy-api' diff --git a/test-shared/src/main/java/io/novafoundation/nova/test_shared/Assertions.kt b/test-shared/src/main/java/io/novafoundation/nova/test_shared/Assertions.kt index 4d1a22c7ff..04ef900732 100644 --- a/test-shared/src/main/java/io/novafoundation/nova/test_shared/Assertions.kt +++ b/test-shared/src/main/java/io/novafoundation/nova/test_shared/Assertions.kt @@ -16,3 +16,9 @@ fun assertSetEquals(expected: Set, actual: Set) { throw AssertionError("Sets are not equal. Expected: $expected, actual: $actual") } } + +fun assertMapEquals(expected: Map, actual: Map) { + if (expected != actual) { + throw AssertionError("Maps are not equal. Expected: $expected, actual: $actual") + } +}