diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml index 70d75c41fc..bb807afa61 100644 --- a/.github/workflows/android_build.yml +++ b/.github/workflows/android_build.yml @@ -10,7 +10,7 @@ on: gradlew-command: required: false type: string - default: false + default: "false" run-tests: required: false type: boolean @@ -18,10 +18,11 @@ on: keystore-file-name: required: false type: string - default: false + default: "false" keystore-file-base64: required: false type: string + default: "false" secrets: ACALA_PROD_AUTH_TOKEN: required: true @@ -94,6 +95,8 @@ env: CI_GITHUB_KEYSTORE_PASS: ${{ secrets.CI_GITHUB_KEYSTORE_PASS }} CI_GITHUB_KEYSTORE_KEY_ALIAS: ${{ secrets.CI_GITHUB_KEYSTORE_KEY_ALIAS }} CI_GITHUB_KEYSTORE_KEY_PASS: ${{ secrets.CI_GITHUB_KEYSTORE_KEY_PASS }} + CI_GITHUB_KEYSTORE_KEY_FILE: ${{ secrets.BASE64_GITHUB_KEYSTORE_FILE }} + jobs: build-app: @@ -113,14 +116,21 @@ jobs: if: ${{ inputs.run-tests }} == "true" run: ./gradlew runTest - - name: πŸ” Getting sign key - if: ${{ !startsWith(inputs.keystore-file-name, 'false') }} - id: write_file + - name: πŸ” Getting github sign key + if: ${{ startsWith(inputs.keystore-file-name, 'github_key.jks') }} + uses: timheuer/base64-to-file@v1.1 + with: + fileName: ${{ inputs.keystore-file-name }} + fileDir: './app/' + encodedString: ${{ env.CI_GITHUB_KEYSTORE_KEY_FILE }} + + - name: πŸ” Getting market sign key + if: ${{ startsWith(inputs.keystore-file-name, 'market_key.jks') }} uses: timheuer/base64-to-file@v1.1 with: fileName: ${{ inputs.keystore-file-name }} fileDir: './app/' - encodedString: ${{ inputs.keystore-file-base64 }} + encodedString: ${{ env.CI_MARKET_KEY_FILE }} - name: πŸ— Build app if: ${{ !startsWith(inputs.gradlew-command, 'false') }} diff --git a/.github/workflows/publish_github_release.yml b/.github/workflows/publish_github_release.yml new file mode 100644 index 0000000000..60643342c9 --- /dev/null +++ b/.github/workflows/publish_github_release.yml @@ -0,0 +1,41 @@ +name: Publish GitHub release + +on: + push: + tags: + - '*' + +jobs: + build: + uses: novasamatech/nova-wallet-android/.github/workflows/android_build.yml@develop + with: + branch: master + gradlew-command: assembleReleaseGithub + keystore-file-name: github_key.jks + secrets: inherit + + create-release: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v2 + + - name: Download built artifact + uses: actions/download-artifact@v2 + with: + name: apk + path: app + + - name: Rename artifacts + run: mv app/releaseGithub/app-releaseGithub.apk app/releaseGithub/nova-wallet-android-${{ github.ref_name }}-github.apk + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + with: + name: Release ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + generate_release_notes: true + draft: true + files: app/releaseGithub/nova-wallet-android-${{ github.ref_name }}-github.apk diff --git a/.github/workflows/update_tag.yml b/.github/workflows/update_tag.yml new file mode 100644 index 0000000000..508a3d60ca --- /dev/null +++ b/.github/workflows/update_tag.yml @@ -0,0 +1,44 @@ +name: Bump app version + +on: + push: + branches: + ['master'] + +jobs: + update-tag: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Version in build.gradle + run: | + versionName=$(grep "versionName" build.gradle | grep -o "'.*'") + versionName=${versionName//\'/} + echo Version in gradle file: $versionName + echo "GRADLE_APP_VERSION=$versionName" >> "$GITHUB_ENV" + + - name: Was version changed? + id: version + run: | + if [[ ${{ env.GRADLE_APP_VERSION }} == ${{ secrets.ANDROID_APP_VERSION }} ]]; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - uses: rickstaa/action-create-tag@v1 + if: steps.version.outputs.changed == 'true' + with: + tag: 'v${{ env.GRADLE_APP_VERSION }}' + message: Release v${{ env.GRADLE_APP_VERSION }} + + - name: Write app version to secrets + if: steps.version.outputs.changed == 'true' + uses: hmanzur/actions-set-secret@v2.0.0 + with: + name: 'ANDROID_APP_VERSION' + value: ${{ env.GRADLE_APP_VERSION }} + repository: novasamatech/nova-wallet-android + token: ${{ secrets.WRITE_SECRET_PAT }} diff --git a/NOTICE b/NOTICE index 7c9b58449c..1978a6eef0 100644 --- a/NOTICE +++ b/NOTICE @@ -1,6 +1,9 @@ Nova - Polkadot, Kusama wallet -Copyright 2022 Novasama Technologies PTE. LTD. +Copyright 2022-2023 Novasama Technologies PTE. LTD. This product includes software developed at Novasama Technologies PTE. LTD. + Some parts of this product are derived from https://github.com/soramitsu/fearless-Android, which belongs to Soramitsu K.K. and was mostly developed by our team of developers from May 1, 2020, to October 5, 2021. Copyright 2021, Soramitsu Helvetia AG, all rights reserved. + +License Rights transferred from Novasama Technologies PTE. LTD to Novasama Technologies GmbH starting from 1st of April 2023 diff --git a/README.md b/README.md index 9c194bd9b8..4df6e8660b 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,4 @@ Developed by former Fearless Wallet team & based on open source work under Apach ## License Nova Wallet Android is available under the Apache 2.0 license. See the LICENSE file for more info. +Β© Novasama Technologies GmbH 2023 \ No newline at end of file diff --git a/app/src/androidTest/java/io/novafoundation/nova/GasPriceProviderIntegrationTest.kt b/app/src/androidTest/java/io/novafoundation/nova/GasPriceProviderIntegrationTest.kt new file mode 100644 index 0000000000..db274710d4 --- /dev/null +++ b/app/src/androidTest/java/io/novafoundation/nova/GasPriceProviderIntegrationTest.kt @@ -0,0 +1,52 @@ +package io.novafoundation.nova + +import android.util.Log +import io.novafoundation.nova.common.utils.average +import io.novafoundation.nova.common.utils.divideToDecimal +import io.novafoundation.nova.runtime.ethereum.gas.LegacyGasPriceProvider +import io.novafoundation.nova.runtime.ethereum.gas.MaxPriorityFeeGasProvider +import io.novafoundation.nova.runtime.ext.Ids +import io.novafoundation.nova.runtime.multiNetwork.awaitCallEthereumApiOrThrow +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import org.junit.Test +import java.math.BigInteger + +class GasPriceProviderIntegrationTest : BaseIntegrationTest() { + + @Test + fun compareLegacyAndImprovedGasPriceEstimations() = runTest { + val api = chainRegistry.awaitCallEthereumApiOrThrow(Chain.Ids.MOONBEAM) + + val legacy = LegacyGasPriceProvider(api) + val improved = MaxPriorityFeeGasProvider(api) + + val legacyStats = mutableSetOf() + val improvedStats = mutableSetOf() + + api.newHeadsFlow().map { + legacyStats.add(legacy.getGasPrice()) + improvedStats.add(improved.getGasPrice()) + } + .take(1000) + .collect() + + legacyStats.printStats("Legacy") + improvedStats.printStats("Improved") + } + + private fun Set.printStats(name: String) { + val min = min() + val max = max() + + Log.d("GasPriceProviderIntegrationTest", """ + Stats for $name source + Min: $min + Max: $max + Avg: ${average()} + Max/Min ratio: ${max.divideToDecimal(min)} + """) + } +} diff --git a/build.gradle b/build.gradle index eccb9eea26..62b062b446 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { // App version - versionName = '6.7.2' - versionCode = 85 + versionName = '6.7.3' + versionCode = 86 applicationId = "io.novafoundation.nova" releaseApplicationSuffix = "market" diff --git a/common/src/main/java/io/novafoundation/nova/common/base/BaseViewModel.kt b/common/src/main/java/io/novafoundation/nova/common/base/BaseViewModel.kt index 63b2a7dd9e..be49b3d052 100644 --- a/common/src/main/java/io/novafoundation/nova/common/base/BaseViewModel.kt +++ b/common/src/main/java/io/novafoundation/nova/common/base/BaseViewModel.kt @@ -75,7 +75,8 @@ open class BaseViewModel : ViewModel(), CoroutineScope, WithCoroutineScopeExtens suspend fun ValidationExecutor.requireValid( validationSystem: ValidationSystem, payload: P, - validationFailureTransformerCustom: (ValidationStatus.NotValid, ValidationFlowActions) -> TransformedFailure, + validationFailureTransformerCustom: (ValidationStatus.NotValid, ValidationFlowActions) -> TransformedFailure?, + autoFixPayload: (original: P, failureStatus: S) -> P = { original, _ -> original }, progressConsumer: ProgressConsumer? = null, block: (P) -> Unit, ) = requireValid( @@ -84,6 +85,7 @@ open class BaseViewModel : ViewModel(), CoroutineScope, WithCoroutineScopeExtens errorDisplayer = ::showError, validationFailureTransformerCustom = validationFailureTransformerCustom, progressConsumer = progressConsumer, + autoFixPayload = autoFixPayload, block = block ) } 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 fa51734c6d..75a3fa16ea 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 @@ -12,14 +12,11 @@ import java.io.InputStream import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext +import java.util.Calendar +import java.util.Collections import java.util.Date import java.util.UUID -import java.util.Collections -import java.util.Calendar import java.util.concurrent.TimeUnit -import kotlin.Comparator -import kotlin.collections.HashMap -import kotlin.collections.LinkedHashMap import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.sqrt @@ -49,6 +46,16 @@ inline fun List.associateByMultiple(keysExtractor: (V) -> Iterable) return destination } +fun ByteArray.startsWith(prefix: ByteArray): Boolean { + if (prefix.size > size) return false + + prefix.forEachIndexed { index, byte -> + if (get(index) != byte) return false + } + + return true +} + /** * Compares two BigDecimals taking into account only values but not scale unlike `==` operator */ @@ -74,10 +81,6 @@ val BigDecimal.isNonNegative: Boolean val BigInteger.isZero: Boolean get() = signum() == 0 -inline fun , R : Comparable> ClosedRange.map(mapper: (T) -> R): ClosedRange { - return mapper(start)..mapper(endInclusive) -} - fun BigInteger?.orZero(): BigInteger = this ?: BigInteger.ZERO fun BigDecimal?.orZero(): BigDecimal = this ?: 0.toBigDecimal() @@ -178,6 +181,12 @@ fun List.median(): Double = sorted().let { (middleLeft + middleRight) / 2 } +fun Collection.average(): BigInteger { + if (isEmpty()) throw NoSuchFieldException("Collection is empty") + + return sum() / size.toBigInteger() +} + fun generateLinearSequence(initial: Int, step: Int) = generateSequence(initial) { it + step } fun Set.toggle(item: T): Set = if (item in this) { diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/SemiUnboundedRange.kt b/common/src/main/java/io/novafoundation/nova/common/utils/SemiUnboundedRange.kt new file mode 100644 index 0000000000..d71a048302 --- /dev/null +++ b/common/src/main/java/io/novafoundation/nova/common/utils/SemiUnboundedRange.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.common.utils + +interface SemiUnboundedRange> { + + val start: T + + val endInclusive: T? +} + +infix operator fun > T.rangeTo(another: T?): SemiUnboundedRange { + return ComparableSemiUnboundedRange(this, another) +} + +inline fun , R : Comparable> SemiUnboundedRange.map(mapper: (T) -> R): SemiUnboundedRange { + return mapper(start)..endInclusive?.let(mapper) +} + +class ComparableSemiUnboundedRange>(override val start: T, override val endInclusive: T?) : SemiUnboundedRange diff --git a/common/src/main/java/io/novafoundation/nova/common/validation/ValidationExecutor.kt b/common/src/main/java/io/novafoundation/nova/common/validation/ValidationExecutor.kt index 84aa83f8ec..e43837ffff 100644 --- a/common/src/main/java/io/novafoundation/nova/common/validation/ValidationExecutor.kt +++ b/common/src/main/java/io/novafoundation/nova/common/validation/ValidationExecutor.kt @@ -32,7 +32,7 @@ class ValidationExecutor : Validatable { validationSystem: ValidationSystem, payload: P, errorDisplayer: (Throwable) -> Unit, - validationFailureTransformerCustom: (ValidationStatus.NotValid, ValidationFlowActions) -> TransformedFailure, + validationFailureTransformerCustom: (ValidationStatus.NotValid, ValidationFlowActions) -> TransformedFailure?, progressConsumer: ProgressConsumer? = null, autoFixPayload: (original: P, failureStatus: S) -> P = { original, _ -> original }, block: (P) -> Unit, @@ -65,9 +65,13 @@ class ValidationExecutor : Validatable { confirmWarning = validationFlowActions::resumeFlow ) } + + null -> null } - validationFailureEvent.value = Event(eventPayload) + eventPayload?.let { + validationFailureEvent.value = Event(eventPayload) + } } ) } diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index d400d871f6..6b0ca155cc 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -134,6 +134,7 @@ Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ Π’Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ %s Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ Π² кошСлСк, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΠΌΠ΅Ρ‚ΡŒ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π΄Π΅Π»Π΅Π³ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π’Π²Π΅Π΄Π΅Π½Π½Ρ‹ΠΉ адрСс ΠΊΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚Π° ΡƒΠΆΠ΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ Π² Nova ΠΊΠ°ΠΊ Ρ‚ΠΎΠΊΠ΅Π½ %s. + Π’Π²Π΅Π΄Π΅Π½Π½Ρ‹ΠΉ адрСс ΠΊΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚Π° присутствуСт Π² Nova ΠΊΠ°ΠΊ Ρ‚ΠΎΠΊΠ΅Π½ %s. Π’Ρ‹ ΡƒΠ²Π΅Ρ€Π΅Π½Ρ‹, Ρ‡Ρ‚ΠΎ Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Π΅Π³ΠΎ? Π­Ρ‚ΠΎΡ‚ Ρ‚ΠΎΠΊΠ΅Π½ ΡƒΠΆΠ΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΡƒΠ±Π΅Π΄ΠΈΡ‚Π΅ΡΡŒ, Ρ‡Ρ‚ΠΎ Π²Π²Π΅Π΄Π΅Π½Π½Ρ‹ΠΉ URL-адрСс ΠΈΠΌΠ΅Π΅Ρ‚ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ Ρ„ΠΎΡ€ΠΌΡƒ: www.coingecko.com/en/coins/tether. ΠΠ΅Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ ссылка Π½Π° CoinGecko @@ -163,7 +164,7 @@ НС ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΈΡ‚Π΅ %s Π½Π° Ledger Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Ledger Π½Π΅ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Ρ‹ %s, Ρ‚Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ ассСт Π±ΡƒΠ΄Π΅Ρ‚ нСдоступСн для ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° Π½Π° этом Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚Π΅ Ledger Π½Π΅ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ этот Ρ‚ΠΎΠΊΠ΅Π½ Поиск ΠΏΠΎ названию сСти ΠΈΠ»ΠΈ Ρ‚ΠΎΠΊΠ΅Π½Π° - Π’ΠΎΠΊΠ΅Π½Ρ‹ с ΡƒΠΊΠ°Π·Π°Π½Π½Ρ‹ΠΌ ΠΈΠΌΠ΅Π½Π΅ΠΌ\nΠ½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹ + Π’ΠΎΠΊΠ΅Π½Ρ‹ ΠΈ сСти с ΡƒΠΊΠ°Π·Π°Π½Π½Ρ‹ΠΌ ΠΈΠΌΠ΅Π½Π΅ΠΌ\nΠ½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹ Π’Π°ΡˆΠΈ кошСльки БиомСтрия ΠŸΠΎΠΊΡƒΠΏΠΊΠ° ΡΠΎΠ²Π΅Ρ€ΡˆΠ΅Π½Π°! ΠžΠΆΠΈΠ΄Π°ΠΉΡ‚Π΅ Π΄ΠΎ 60 ΠΌΠΈΠ½ΡƒΡ‚. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Ρ‚ΡŒ статус ΠΏΠΎ элСктронной ΠΏΠΎΡ‡Ρ‚Π΅. @@ -234,6 +235,8 @@ ΠžΠΏΠ΅Ρ€Π°Ρ†ΠΈΡ ΡƒΠ΄Π°Π»ΠΈΡ‚ Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ Π˜ΡΡ‚Π΅ΠΊΠ»Π° Π˜ΡΡΠ»Π΅Π΄ΡƒΠΉ + ΠŸΡ€Π΅Π΄ΠΏΠΎΠ»Π°Π³Π°Π΅ΠΌΠ°Ρ комиссия %s Π½Π°ΠΌΠ½ΠΎΠ³ΠΎ Π²Ρ‹ΡˆΠ΅, Ρ‡Π΅ΠΌ комиссия ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ (%s). Π­Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ связано с Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ ΠΏΠ΅Ρ€Π΅Π³Ρ€ΡƒΠ·ΠΊΠΎΠΉ сСти. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ комиссию, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π΄ΠΎΠΆΠ΄Π°Ρ‚ΡŒΡΡ Π±ΠΎΠ»Π΅Π΅ Π½ΠΈΠ·ΠΊΠΎΠΉ суммы. + Комиссия сСти слишком высокая Π‘ΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠΎ: Π€ΠΈΠ»ΡŒΡ‚Ρ€Ρ‹ Π£Π·Π½Π°Ρ‚ΡŒ большС @@ -305,6 +308,7 @@ Π¦Π΅Π½Π° ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅ + ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ ΠžΡ‚ΠΊΠ»ΠΎΠ½ΠΈΡ‚ΡŒ Π£Π΄Π°Π»ΠΈΡ‚ΡŒ ΠžΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ @@ -739,6 +743,8 @@ Nova нуТдаСтся Π² Π²ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ мСстополоТСния, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΠΌΠ΅Ρ‚ΡŒ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡ‚ΡŒ сканированиС Bluetooth для поиска вашСго устройства Ledger ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π²ΠΊΠ»ΡŽΡ‡ΠΈΡ‚Π΅ Π³Π΅ΠΎΠ»ΠΎΠΊΠ°Ρ†ΠΈΡŽ Π² настройках устройства АдрСс ΠΈΠ»ΠΈ w3n + ΠŸΠΎΠ»ΡƒΡ‡Π°Ρ‚Π΅Π»ΡŒ являСтся систСмным Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ΠΎΠΌ. Π­Ρ‚ΠΎΡ‚ Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ Π½Π΅ контролируСтся ΠΊΠ°ΠΊΠΎΠΉ-Π»ΠΈΠ±ΠΎ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠ΅ΠΉ ΠΈΠ»ΠΈ частным Π»ΠΈΡ†ΠΎΠΌ. \nΠ’Ρ‹ ΡƒΠ²Π΅Ρ€Π΅Π½Ρ‹, Ρ‡Ρ‚ΠΎ всС Π΅Ρ‰Π΅ Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹ΠΉ ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄? + Π’ΠΎΠΊΠ΅Π½Ρ‹ Π±ΡƒΠ΄ΡƒΡ‚ потСряны ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΡƒΠ±Π΅Π΄ΠΈΡ‚Π΅ΡΡŒ, Ρ‡Ρ‚ΠΎ биомСтрия Π²ΠΊΠ»ΡŽΡ‡Π΅Π½Π° Π² настройках БиомСтрия ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½Π° Π² настройках БообщСство @@ -798,8 +804,8 @@ ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ свой список Π‘Ρ‚Π΅ΠΉΠΊΠΈΠ½Π³ Ρ‡Π΅Ρ€Π΅Π· DApp Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ Nova Π‘ΠΎΠ»ΡŒΡˆΠ΅ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚ΠΎΠ² стСйкинга - Π‘Ρ‚Π΅ΠΉΠΊΠ°ΠΉΡ‚Π΅ ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°ΠΉΡ‚Π΅ вознаграТдСния - ΠŸΡ€ΠΈΠΌΠ΅Ρ€Π½Π°Ρ Π΄ΠΎΡ…ΠΎΠ΄Π½ΠΎΡΡ‚ΡŒ + Π‘Ρ‚Π΅ΠΉΠΊΠ°ΠΉΡ‚Π΅ ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°ΠΉΡ‚Π΅ Π½Π°Π³Ρ€Π°Π΄Ρ‹ + ΠŸΡ€ΠΈΠΌΠ΅Ρ€Π½Ρ‹ΠΉ Π΄ΠΎΡ…ΠΎΠ΄ эра %s Расчёт доходности Расчёт доходности %s diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 8fb1513e6c..926d518648 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,5 +1,15 @@ + Network fee is too high + The estimated network fee %s is much higher than the default network fee (%s). This might be due to temporary network congestion. You can refresh to wait for a lower network fee. + Refresh fee + + Tokens will be lost + Recipient is a system account. It is not controlled by any company or individual.\nAre you sure you still want to perform this transfer? + + This token already exists + The entered contract address is present in Nova as a %s token. Are you sure you want to modify it? + For security reasons generated operations valid for only %s.\nPlease generate new QR code and sign it with %s Invalid QR code, please make sure you are scanning QR code from %s I have an error in %s @@ -356,7 +366,6 @@ Add token - This token already exist The entered contract address is present in Nova as a %s token. Invalid contract address diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt index 947c735d79..5186b21890 100644 --- a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/ethereum/transaction/EvmTransactionService.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_account_api.data.ethereum.transaction +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId import org.web3j.tx.gas.DefaultGasProvider @@ -14,10 +15,11 @@ interface EvmTransactionService { origin: TransactionOrigin = TransactionOrigin.SelectedWallet, fallbackGasLimit: BigInteger = DefaultGasProvider.GAS_LIMIT, building: EvmTransactionBuilding, - ): BigInteger + ): Fee suspend fun transact( chainId: ChainId, + presetFee: Fee?, origin: TransactionOrigin = TransactionOrigin.SelectedWallet, fallbackGasLimit: BigInteger = DefaultGasProvider.GAS_LIMIT, building: EvmTransactionBuilding, 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 f644b591e2..0d74a1f13e 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 @@ -2,6 +2,7 @@ package io.novafoundation.nova.feature_account_api.data.extrinsic import io.novafoundation.nova.common.data.network.runtime.model.FeeResponse import io.novafoundation.nova.common.utils.multiResult.RetriableMultiResult +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.runtime.extrinsic.ExtrinsicStatus import io.novafoundation.nova.runtime.extrinsic.multi.CallBuilder import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -54,10 +55,15 @@ interface ExtrinsicService { formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, ): BigInteger + suspend fun estimateFeeV2( + chain: Chain, + formExtrinsic: suspend ExtrinsicBuilder.() -> Unit, + ): Fee + suspend fun estimateMultiFee( chain: Chain, formExtrinsic: FormMultiExtrinsic, ): BigInteger - suspend fun estimateFee(chainId: ChainId, extrinsic: String): BigInteger + suspend fun estimateFee(chainId: ChainId, extrinsic: String): Fee } diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt new file mode 100644 index 0000000000..7e014a4a89 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/data/model/Fee.kt @@ -0,0 +1,19 @@ +package io.novafoundation.nova.feature_account_api.data.model + +import java.math.BigInteger + +interface Fee { + + companion object + + val amount: BigInteger +} + +data class EvmFee(val gasLimit: BigInteger, val gasPrice: BigInteger) : Fee { + override val amount = gasLimit * gasPrice +} + +@JvmInline +value class InlineFee(override val amount: BigInteger) : Fee + +fun Fee.Companion.zero(): Fee = InlineFee(BigInteger.ZERO) diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/CompoundSystemAccountMatcher.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/CompoundSystemAccountMatcher.kt new file mode 100644 index 0000000000..e015081536 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/CompoundSystemAccountMatcher.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_account_api.domain.account.system + +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +class CompoundSystemAccountMatcher( + private val delegates: List +) : SystemAccountMatcher { + + constructor(vararg delegates: SystemAccountMatcher) : this(delegates.toList()) + + override fun isSystemAccount(accountId: AccountId): Boolean { + return delegates.any { it.isSystemAccount(accountId) } + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/PrefixSystemAccountMatcher.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/PrefixSystemAccountMatcher.kt new file mode 100644 index 0000000000..399723e39e --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/PrefixSystemAccountMatcher.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.feature_account_api.domain.account.system + +import io.novafoundation.nova.common.utils.startsWith +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +class PrefixSystemAccountMatcher(prefix: String) : SystemAccountMatcher { + + private val prefixBytes = prefix.encodeToByteArray() + + override fun isSystemAccount(accountId: AccountId): Boolean { + return accountId.startsWith(prefixBytes) + } +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/SystemAccountMatcher.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/SystemAccountMatcher.kt new file mode 100644 index 0000000000..43ffe660c9 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/account/system/SystemAccountMatcher.kt @@ -0,0 +1,23 @@ +package io.novafoundation.nova.feature_account_api.domain.account.system + +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +interface SystemAccountMatcher { + + companion object + + fun isSystemAccount(accountId: AccountId): Boolean +} + +fun SystemAccountMatcher.Companion.default(): SystemAccountMatcher { + return CompoundSystemAccountMatcher( + // Pallet-specific technical accounts, e.g. crowdloan-fund, nomination pool, + PrefixSystemAccountMatcher("modl"), + // Parachain sovereign accounts on relaychain + PrefixSystemAccountMatcher("para"), + // Relaychain sovereign account on parachains + PrefixSystemAccountMatcher("Parent"), + // Sibling parachain soveregin accounts + PrefixSystemAccountMatcher("sibl") + ) +} diff --git a/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/validation/SystemAccountRecipientValidation.kt b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/validation/SystemAccountRecipientValidation.kt new file mode 100644 index 0000000000..08c82729e1 --- /dev/null +++ b/feature-account-api/src/main/java/io/novafoundation/nova/feature_account_api/domain/validation/SystemAccountRecipientValidation.kt @@ -0,0 +1,40 @@ +package io.novafoundation.nova.feature_account_api.domain.validation + +import io.novafoundation.nova.common.base.TitleAndMessage +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.validation.Validation +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.common.validation.isFalseOrWarning +import io.novafoundation.nova.common.validation.valid +import io.novafoundation.nova.feature_account_api.R +import io.novafoundation.nova.feature_account_api.domain.account.system.SystemAccountMatcher +import io.novafoundation.nova.feature_account_api.domain.account.system.default +import jp.co.soramitsu.fearless_utils.runtime.AccountId + +class SystemAccountRecipientValidation( + private val accountId: (P) -> AccountId?, + private val error: (AccountId) -> E, + private val matcher: SystemAccountMatcher = SystemAccountMatcher.default(), +) : Validation { + + override suspend fun validate(value: P): ValidationStatus { + val accountId = accountId(value) ?: return valid() + + return matcher.isSystemAccount(accountId) isFalseOrWarning { + error(accountId) + } + } +} + +fun ValidationSystemBuilder.notSystemAccount( + accountId: (P) -> AccountId?, + error: (AccountId) -> E, +) { + validate(SystemAccountRecipientValidation(accountId, error)) +} + +fun handleSystemAccountValidationFailure(resourceManager: ResourceManager): TitleAndMessage { + return resourceManager.getString(R.string.send_recipient_system_account_title) to + resourceManager.getString(R.string.send_recipient_system_account_message) +} diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt index e2e2614b90..fcfc89cdce 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/ethereum/transaction/RealEvmTransactionService.kt @@ -1,16 +1,20 @@ package io.novafoundation.nova.feature_account_impl.data.ethereum.transaction +import io.novafoundation.nova.common.utils.castOrNull import io.novafoundation.nova.core.ethereum.Web3Api import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionBuilding import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionHash import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin +import io.novafoundation.nova.feature_account_api.data.model.EvmFee +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn import io.novafoundation.nova.runtime.ethereum.EvmRpcException +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.ethereum.sendSuspend import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -33,6 +37,7 @@ internal class RealEvmTransactionService( private val accountRepository: AccountRepository, private val chainRegistry: ChainRegistry, private val signerProvider: SignerProvider, + private val gasPriceProviderFactory: GasPriceProviderFactory, ) : EvmTransactionService { override suspend fun calculateFee( @@ -40,7 +45,7 @@ internal class RealEvmTransactionService( origin: TransactionOrigin, fallbackGasLimit: BigInteger, building: EvmTransactionBuilding - ): BigInteger { + ): Fee { val web3Api = chainRegistry.awaitCallEthereumApiOrThrow(chainId) val chain = chainRegistry.getChain(chainId) @@ -50,13 +55,15 @@ internal class RealEvmTransactionService( val txBuilder = EvmTransactionBuilder().apply(building) val txForFee = txBuilder.buildForFee(submittingAddress) - val gasPrice = web3Api.gasPrice() + val gasPrice = gasPriceProviderFactory.createKnown(chainId).getGasPrice() + val gasLimit = web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit) - return gasPrice * web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit) + return EvmFee(gasLimit, gasPrice) } override suspend fun transact( chainId: ChainId, + presetFee: Fee?, origin: TransactionOrigin, fallbackGasLimit: BigInteger, building: EvmTransactionBuilding @@ -67,13 +74,18 @@ internal class RealEvmTransactionService( val web3Api = chainRegistry.awaitCallEthereumApiOrThrow(chainId) val txBuilder = EvmTransactionBuilder().apply(building) - val txForFee = txBuilder.buildForFee(submittingAddress) - val gasPrice = web3Api.gasPrice() - val gasLimit = web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit) + val evmFee = presetFee?.castOrNull() ?: run { + val txForFee = txBuilder.buildForFee(submittingAddress) + val gasPrice = gasPriceProviderFactory.createKnown(chainId).getGasPrice() + val gasLimit = web3Api.gasLimitOrDefault(txForFee, fallbackGasLimit) + + EvmFee(gasLimit, gasPrice) + } + val nonce = web3Api.getNonce(submittingAddress) - val txForSign = txBuilder.buildForSign(nonce = nonce, gasPrice = gasPrice, gasLimit = gasLimit) + val txForSign = txBuilder.buildForSign(nonce = nonce, gasPrice = evmFee.gasPrice, gasLimit = evmFee.gasLimit) val toSubmit = signTransaction(txForSign, submittingMetaAccount, chain) web3Api.sendTransaction(toSubmit) @@ -106,8 +118,6 @@ internal class RealEvmTransactionService( .transactionCount } - private suspend fun Web3Api.gasPrice(): BigInteger = ethGasPrice().sendSuspend().gasPrice - private suspend fun Web3Api.gasLimitOrDefault(tx: Transaction, default: BigInteger): BigInteger = try { ethEstimateGas(tx).sendSuspend().amountUsed } catch (rpcException: EvmRpcException) { 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 74bef365a1..16be228613 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 @@ -11,6 +11,8 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic import io.novafoundation.nova.feature_account_api.data.extrinsic.FormExtrinsicWithOrigin import io.novafoundation.nova.feature_account_api.data.extrinsic.FormMultiExtrinsic import io.novafoundation.nova.feature_account_api.data.extrinsic.FormMultiExtrinsicWithOrigin +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn @@ -118,10 +120,14 @@ class RealExtrinsicService( extrinsicBuilder.formExtrinsic() val extrinsic = extrinsicBuilder.build() - return estimateFee(chain.id, extrinsic) + return estimateFee(chain.id, extrinsic).amount } - override suspend fun estimateFee(chainId: ChainId, extrinsic: String): BigInteger { + override suspend fun estimateFeeV2(chain: Chain, formExtrinsic: suspend ExtrinsicBuilder.() -> Unit): Fee { + return InlineFee(estimateFee(chain, formExtrinsic)) + } + + override suspend fun estimateFee(chainId: ChainId, extrinsic: String): Fee { val baseFee = rpcCalls.getExtrinsicFee(chainId, extrinsic).partialFee val runtime = chainRegistry.getRuntime(chainId) @@ -131,7 +137,7 @@ class RealExtrinsicService( val tip = decodedExtrinsic.tip().orZero() - return tip + baseFee + return InlineFee(tip + baseFee) } override suspend fun estimateMultiFee(chain: Chain, formExtrinsic: FormMultiExtrinsic): BigInteger { @@ -139,7 +145,7 @@ class RealExtrinsicService( val extrinsics = constructSplitExtrinsics(chain, formExtrinsic, feeExtrinsicBuilderSequence) - val separateFees = extrinsics.map { estimateFee(chain.id, it) } + val separateFees = extrinsics.map { estimateFee(chain.id, it).amount } return separateFees.sum() } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt index e6f49e10c6..a2522fba96 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureDependencies.kt @@ -33,6 +33,7 @@ import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository import io.novafoundation.nova.feature_versions_api.domain.UpdateNotificationsInteractor import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase import io.novafoundation.nova.runtime.extrinsic.MortalityConstructor @@ -135,4 +136,6 @@ interface AccountFeatureDependencies { fun remoteStorageSource(): StorageDataSource val extrinsicSplitter: ExtrinsicSplitter + + val gasPriceProviderFactory: GasPriceProviderFactory } diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt index 78681d6c0f..bb75005254 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/di/AccountFeatureModule.kt @@ -19,6 +19,7 @@ import io.novafoundation.nova.common.utils.systemCall.SystemCallExecutor import io.novafoundation.nova.core_db.dao.AccountDao import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.NodeDao +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.repository.OnChainIdentityRepository @@ -361,11 +362,13 @@ class AccountFeatureModule { fun provideEvmTransactionService( accountRepository: AccountRepository, chainRegistry: ChainRegistry, - signerProvider: SignerProvider + signerProvider: SignerProvider, + gasPriceProviderFactory: GasPriceProviderFactory, ): EvmTransactionService = RealEvmTransactionService( accountRepository = accountRepository, chainRegistry = chainRegistry, - signerProvider = signerProvider + signerProvider = signerProvider, + gasPriceProviderFactory = gasPriceProviderFactory ) @Provides diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt index eec373c20d..6baa085790 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/send/SendInteractor.kt @@ -1,8 +1,11 @@ package io.novafoundation.nova.feature_assets.domain.send import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.isCrossChain import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransactor import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainTransfersRepository @@ -25,7 +28,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import java.math.BigDecimal -import java.math.BigInteger class SendInteractor( private val chainRegistry: ChainRegistry, @@ -66,7 +68,7 @@ class SendInteractor( crossChainTransfersRepository.syncConfiguration() } - suspend fun getOriginFee(transfer: AssetTransfer): BigInteger = withContext(Dispatchers.Default) { + suspend fun getOriginFee(transfer: AssetTransfer): Fee = withContext(Dispatchers.Default) { if (transfer.isCrossChain) { val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! @@ -76,19 +78,21 @@ class SendInteractor( } } - suspend fun getCrossChainFee(transfer: AssetTransfer): BigInteger? = if (transfer.isCrossChain) { - withContext(Dispatchers.Default) { + suspend fun getCrossChainFee(transfer: AssetTransfer): Fee? = if (transfer.isCrossChain) { + val feePlanks = withContext(Dispatchers.Default) { val config = crossChainTransfersRepository.getConfiguration().configurationFor(transfer)!! val crossChainFee = crossChainWeigher.estimateFee(config) crossChainFee.reserve.orZero() + crossChainFee.destination.orZero() } + + InlineFee(feePlanks) } else { null } suspend fun performTransfer( - transfer: AssetTransfer, + transfer: WeightedAssetTransfer, originFee: BigDecimal, crossChainFee: BigDecimal?, ): Result<*> = withContext(Dispatchers.Default) { diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt index d1f1941e04..1d28de365d 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/AddTokensInteractor.kt @@ -4,7 +4,7 @@ import io.novafoundation.nova.common.address.format.EthereumAddressFormat import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.AddEvmTokenValidationSystem import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory -import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.evmAssetNotExist +import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.evmAssetNotExists import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.validCoinGeckoLink import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.validErc20Contract import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.validTokenDecimals @@ -94,7 +94,7 @@ class RealAddTokensInteractor( override fun getValidationSystem(): AddEvmTokenValidationSystem { return ValidationSystem { - evmAssetNotExist(chainAssetRepository) + evmAssetNotExists(chainRegistry) validErc20Contract(ethereumAddressFormat, erc20Standard, chainRegistry) validTokenDecimals() validCoinGeckoLink(coinGeckoLinkValidationFactory) diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/validations/AddEvmTokensValidatoins.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/validations/AddEvmTokensValidatoins.kt index 8d5e4ee610..b571e94b12 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/validations/AddEvmTokensValidatoins.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/tokens/add/validations/AddEvmTokensValidatoins.kt @@ -4,8 +4,7 @@ import io.novafoundation.nova.common.address.format.EthereumAddressFormat import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_assets.domain.tokens.add.CustomErc20Token -import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository -import io.novafoundation.nova.feature_wallet_api.domain.validation.evmAssetNotExist +import io.novafoundation.nova.feature_wallet_api.domain.validation.evmAssetNotExists import io.novafoundation.nova.feature_wallet_api.domain.validation.validErc20Contract import io.novafoundation.nova.feature_wallet_api.domain.validation.validTokenDecimals import io.novafoundation.nova.runtime.ethereum.contract.erc20.Erc20Standard @@ -18,7 +17,7 @@ typealias AddEvmTokenValidationSystemBuilder = ValidationSystemBuilder { + status: ValidationStatus.NotValid, + actions: ValidationFlowActions, + feeLoaderMixin: FeeLoaderMixin.Presentation, +): TransformedFailure? { + return when (val reason = status.reason) { + is AssetTransferValidationFailure.DeadRecipient.InCommissionAsset -> Default( resourceManager.getString(R.string.wallet_send_dead_recipient_commission_asset_title) to - resourceManager.getString(R.string.wallet_send_dead_recipient_commission_asset_message, failure.commissionAsset.symbol) - } - AssetTransferValidationFailure.DeadRecipient.InUsedAsset -> { + resourceManager.getString(R.string.wallet_send_dead_recipient_commission_asset_message, reason.commissionAsset.symbol) + ) + + AssetTransferValidationFailure.DeadRecipient.InUsedAsset -> Default( resourceManager.getString(R.string.common_amount_low) to resourceManager.getString(R.string.wallet_send_dead_recipient_message) - } - AssetTransferValidationFailure.NotEnoughFunds.InUsedAsset -> { + ) + + AssetTransferValidationFailure.NotEnoughFunds.InUsedAsset -> Default( resourceManager.getString(R.string.common_not_enough_funds_title) to resourceManager.getString(R.string.choose_amount_error_too_big) - } - is AssetTransferValidationFailure.NotEnoughFunds.InCommissionAsset -> { - handleNotEnoughFeeError(failure, resourceManager) - } + ) - AssetTransferValidationFailure.WillRemoveAccount.WillBurnDust -> { + is AssetTransferValidationFailure.NotEnoughFunds.InCommissionAsset -> Default( + handleNotEnoughFeeError(reason, resourceManager) + ) + + AssetTransferValidationFailure.WillRemoveAccount.WillBurnDust -> Default( resourceManager.getString(R.string.wallet_send_existential_warning_title) to resourceManager.getString(R.string.wallet_send_existential_warning_message_v2_2_0) - } - is AssetTransferValidationFailure.WillRemoveAccount.WillTransferDust -> { + ) + + is AssetTransferValidationFailure.WillRemoveAccount.WillTransferDust -> Default( resourceManager.getString(R.string.wallet_send_existential_warning_title) to resourceManager.getString(R.string.wallet_send_existential_warnining_transfer_dust) - } - is AssetTransferValidationFailure.InvalidRecipientAddress -> { + ) + + is AssetTransferValidationFailure.InvalidRecipientAddress -> Default( resourceManager.getString(R.string.common_validation_invalid_address_title) to - resourceManager.getString(R.string.common_validation_invalid_address_message, failure.chain.name) - } - is AssetTransferValidationFailure.PhishingRecipient -> { + resourceManager.getString(R.string.common_validation_invalid_address_message, reason.chain.name) + ) + + is AssetTransferValidationFailure.PhishingRecipient -> Default( resourceManager.getString(R.string.wallet_send_phishing_warning_title) to - resourceManager.getString(R.string.wallet_send_phishing_warning_text, failure.address) - } - AssetTransferValidationFailure.NonPositiveAmount -> { + resourceManager.getString(R.string.wallet_send_phishing_warning_text, reason.address) + ) + + AssetTransferValidationFailure.NonPositiveAmount -> Default( resourceManager.getString(R.string.common_error_general_title) to resourceManager.getString(R.string.common_zero_amount_error) - } - is AssetTransferValidationFailure.NotEnoughFunds.ToPayCrossChainFee -> { + ) + + is AssetTransferValidationFailure.NotEnoughFunds.ToPayCrossChainFee -> Default( resourceManager.getString(R.string.common_not_enough_funds_title) to resourceManager.getString( R.string.wallet_send_cannot_pay_cross_chain_fee, - failure.fee.formatTokenAmount(failure.usedAsset), - failure.remainingBalanceAfterTransfer.formatTokenAmount(failure.usedAsset) + reason.fee.formatTokenAmount(reason.usedAsset), + reason.remainingBalanceAfterTransfer.formatTokenAmount(reason.usedAsset) ) - } - is AssetTransferValidationFailure.NotEnoughFunds.ToStayAboveED -> { + ) + + is AssetTransferValidationFailure.NotEnoughFunds.ToStayAboveED -> Default( resourceManager.getString(R.string.common_not_enough_funds_title) to - resourceManager.getString(R.string.wallet_send_insufficient_balance_commission, failure.commissionAsset.symbol) - } - AssetTransferValidationFailure.RecipientCannotAcceptTransfer -> { + resourceManager.getString(R.string.wallet_send_insufficient_balance_commission, reason.commissionAsset.symbol) + ) + + AssetTransferValidationFailure.RecipientCannotAcceptTransfer -> Default( resourceManager.getString(R.string.wallet_send_recipient_blocked_title) to resourceManager.getString(R.string.wallet_send_recipient_blocked_message) - } + ) + + is AssetTransferValidationFailure.FeeChangeDetected -> handleFeeSpikeDetected( + error = reason, + resourceManager = resourceManager, + feeLoaderMixin = feeLoaderMixin, + actions = actions, + ) + + AssetTransferValidationFailure.RecipientIsSystemAccount -> Default( + handleSystemAccountValidationFailure(resourceManager) + ) } } + +fun autoFixSendValidationPayload( + payload: AssetTransferPayload, + failureReason: AssetTransferValidationFailure +) = when (failureReason) { + is AssetTransferValidationFailure.WillRemoveAccount.WillTransferDust -> payload.copy( + transfer = payload.transfer.copy( + amount = payload.transfer.amount + failureReason.dust + ) + ) + is AssetTransferValidationFailure.FeeChangeDetected -> payload.copy( + transfer = payload.transfer.copy( + decimalFee = failureReason.payload.newFee + ) + ) + else -> payload +} diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt index f37d389a1e..0a52f762c3 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/TransferDraft.kt @@ -2,13 +2,14 @@ package io.novafoundation.nova.feature_assets.presentation.send import android.os.Parcelable import io.novafoundation.nova.feature_assets.presentation.AssetPayload +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeParcelModel import kotlinx.android.parcel.Parcelize import java.math.BigDecimal @Parcelize class TransferDraft( val amount: BigDecimal, - val originFee: BigDecimal, + val originFee: FeeParcelModel, val crossChainFee: BigDecimal?, val origin: AssetPayload, val destination: AssetPayload, diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt index 96cac1b808..8f919de4bf 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/amount/SelectSendViewModel.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.progressConsumer import io.novafoundation.nova.common.view.ButtonState import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.interfaces.MetaAccountGroupingInteractor import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.list.SelectAddressForTransactionRequester @@ -28,19 +29,21 @@ import io.novafoundation.nova.feature_assets.presentation.send.TransferDirection import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft import io.novafoundation.nova.feature_assets.presentation.send.amount.view.CrossChainDestinationModel import io.novafoundation.nova.feature_assets.presentation.send.amount.view.SelectCrossChainDestinationBottomSheet +import io.novafoundation.nova.feature_assets.presentation.send.autoFixSendValidationPayload import io.novafoundation.nova.feature_assets.presentation.send.mapAssetTransferValidationFailureToUI import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure.WillRemoveAccount +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.BaseAssetTransfer +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.connectWith import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireFee -import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.requireOptionalFee +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeToParcel import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset import io.novafoundation.nova.runtime.multiNetwork.asset @@ -57,7 +60,6 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal -import java.math.BigInteger class SelectSendViewModel( private val interactor: WalletInteractor, @@ -171,33 +173,46 @@ class SelectSendViewModel( syncCrossChainConfig() } - fun nextClicked() = originFeeMixin.requireFee(this) { originFee -> - crossChainFeeMixin.requireOptionalFee(this) { crossChainFee -> - launch { - val payload = AssetTransferPayload( - transfer = buildTransfer( - destination = destinationChainWithAsset.first(), - amount = amountChooserMixin.amount.first(), - address = addressInputMixin.getAddress() - ), - originFee = originFee, - crossChainFee = crossChainFee, - originCommissionAsset = commissionAssetFlow.first(), - originUsedAsset = assetFlow.first() + fun nextClicked() = launch { + sendInProgressFlow.value = true + + val originFee = originFeeMixin.awaitDecimalFee() + val crossChainFee = crossChainFeeMixin.awaitOptionalDecimalFee() + + val transfer = buildTransfer( + destination = destinationChainWithAsset.first(), + amount = amountChooserMixin.amount.first(), + address = addressInputMixin.getAddress(), + ) + + val payload = AssetTransferPayload( + transfer = WeightedAssetTransfer( + assetTransfer = transfer, + fee = originFee, + ), + crossChainFee = crossChainFee?.decimalAmount, + originFee = originFee.decimalAmount, + originCommissionAsset = commissionAssetFlow.first(), + originUsedAsset = assetFlow.first() + ) + + validationExecutor.requireValid( + validationSystem = sendInteractor.validationSystemFor(payload.transfer), + payload = payload, + progressConsumer = sendInProgressFlow.progressConsumer(), + autoFixPayload = ::autoFixSendValidationPayload, + validationFailureTransformerCustom = { status, actions -> + viewModelScope.mapAssetTransferValidationFailureToUI( + resourceManager = resourceManager, + status = status, + actions = actions, + feeLoaderMixin = originFeeMixin ) + }, + ) { + sendInProgressFlow.value = false - validationExecutor.requireValid( - validationSystem = sendInteractor.validationSystemFor(payload.transfer), - payload = payload, - progressConsumer = sendInProgressFlow.progressConsumer(), - autoFixPayload = ::autoFixValidationPayload, - validationFailureTransformer = { mapAssetTransferValidationFailureToUI(resourceManager, it) } - ) { - sendInProgressFlow.value = false - - openConfirmScreen(it) - } - } + openConfirmScreen(it) } } @@ -267,7 +282,7 @@ class SelectSendViewModel( } private fun FeeLoaderMixin.Presentation.setupFee( - feeConstructor: suspend Token.(transfer: AssetTransfer) -> BigInteger? + feeConstructor: suspend Token.(transfer: AssetTransfer) -> Fee? ) { connectWith( inputSource1 = amountChooserMixin.backPressuredAmount, @@ -285,7 +300,7 @@ class SelectSendViewModel( private fun openConfirmScreen(validPayload: AssetTransferPayload) = launch { val transferDraft = TransferDraft( amount = validPayload.transfer.amount, - originFee = validPayload.originFee, + originFee = mapFeeToParcel(validPayload.transfer.decimalFee), origin = assetPayload, destination = AssetPayload( chainId = validPayload.transfer.destinationChain.id, @@ -298,20 +313,12 @@ class SelectSendViewModel( router.openConfirmTransfer(transferDraft) } - private fun autoFixValidationPayload( - payload: AssetTransferPayload, - failureReason: AssetTransferValidationFailure - ) = when (failureReason) { - is WillRemoveAccount.WillTransferDust -> payload.copy( - transfer = payload.transfer.copy( - amount = payload.transfer.amount + failureReason.dust - ) - ) - else -> payload - } - - private suspend fun buildTransfer(destination: ChainWithAsset, amount: BigDecimal, address: String): AssetTransfer { - return AssetTransfer( + private suspend fun buildTransfer( + destination: ChainWithAsset, + amount: BigDecimal, + address: String, + ): AssetTransfer { + return BaseAssetTransfer( sender = selectedAccount.first(), recipient = address, originChain = originChain(), @@ -319,7 +326,7 @@ class SelectSendViewModel( destinationChain = destination.chain, destinationChainAsset = destination.asset, amount = amount, - commissionAssetToken = commissionAssetFlow.first().token + commissionAssetToken = commissionAssetFlow.first().token, ) } @@ -344,7 +351,6 @@ class SelectSendViewModel( } } - @OptIn(ExperimentalStdlibApi::class) private suspend fun buildDestinationsMap(crossChainDestinations: List): Map> { val crossChainDestinationModels = crossChainDestinations.map { CrossChainDestinationModel( diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt index b8dbc17118..1e4c9c3fef 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/send/confirm/ConfirmSendViewModel.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_assets.presentation.send.confirm +import androidx.lifecycle.viewModelScope import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.api.Validatable @@ -25,13 +26,16 @@ import io.novafoundation.nova.feature_assets.presentation.AssetPayload import io.novafoundation.nova.feature_assets.presentation.AssetsRouter import io.novafoundation.nova.feature_assets.presentation.send.TransferDirectionModel import io.novafoundation.nova.feature_assets.presentation.send.TransferDraft +import io.novafoundation.nova.feature_assets.presentation.send.autoFixSendValidationPayload import io.novafoundation.nova.feature_assets.presentation.send.confirm.hints.ConfirmSendHintsMixinFactory import io.novafoundation.nova.feature_assets.presentation.send.isCrossChain import io.novafoundation.nova.feature_assets.presentation.send.mapAssetTransferValidationFailureToUI -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitDecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.create +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.mapFeeFromParcel import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountSign import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry @@ -64,6 +68,8 @@ class ConfirmSendViewModel( ExternalActions by externalActions, Validatable by validationExecutor { + private val originFee = mapFeeFromParcel(transferDraft.originFee) + private val originChain by lazyAsync { chainRegistry.getChain(transferDraft.origin.chainId) } private val originAsset by lazyAsync { chainRegistry.asset(transferDraft.origin.chainId, transferDraft.origin.chainAssetId) } @@ -158,14 +164,22 @@ class ConfirmSendViewModel( validationSystem = sendInteractor.validationSystemFor(payload.transfer), payload = payload, progressConsumer = _transferSubmittingLiveData.progressConsumer(), - validationFailureTransformer = { mapAssetTransferValidationFailureToUI(resourceManager, it) } + autoFixPayload = ::autoFixSendValidationPayload, + validationFailureTransformerCustom = { status, actions -> + viewModelScope.mapAssetTransferValidationFailureToUI( + resourceManager = resourceManager, + status = status, + actions = actions, + feeLoaderMixin = originFeeMixin + ) + }, ) { validPayload -> performTransfer(validPayload.transfer, validPayload.originFee, validPayload.crossChainFee) } } private fun setInitialState() = launch { - originFeeMixin.setFee(transferDraft.originFee) + originFeeMixin.setFee(originFee.fee) crossChainFeeMixin.setFee(transferDraft.crossChainFee) } @@ -183,7 +197,7 @@ class ConfirmSendViewModel( ) private fun performTransfer( - transfer: AssetTransfer, + transfer: WeightedAssetTransfer, originFee: BigDecimal, crossChainFee: BigDecimal? ) = launch { @@ -208,8 +222,10 @@ class ConfirmSendViewModel( val chain = originChain() val chainAsset = originAsset() + val originFee = originFeeMixin.awaitDecimalFee() + return AssetTransferPayload( - transfer = AssetTransfer( + transfer = WeightedAssetTransfer( sender = currentAccount.first(), recipient = transferDraft.recipientAddress, originChain = chain, @@ -218,8 +234,9 @@ class ConfirmSendViewModel( originChainAsset = chainAsset, amount = transferDraft.amount, commissionAssetToken = commissionAssetFlow.first().token, + decimalFee = originFee, ), - originFee = transferDraft.originFee, + originFee = originFee.decimalAmount, originCommissionAsset = commissionAssetFlow.first(), originUsedAsset = assetFlow.first(), crossChainFee = transferDraft.crossChainFee diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/add/enterInfo/AddEvmTokenValidationFailureUi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/add/enterInfo/AddEvmTokenValidationFailureUi.kt index 27e22914f8..4f895043d5 100644 --- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/add/enterInfo/AddEvmTokenValidationFailureUi.kt +++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/tokens/add/enterInfo/AddEvmTokenValidationFailureUi.kt @@ -11,8 +11,14 @@ fun mapAddEvmTokensValidationFailureToUI( ): TitleAndMessage { return when (failure) { is AddEvmTokensValidationFailure.AssetExist -> { - resourceManager.getString(R.string.asset_add_evm_token_already_exist_title) to + val title = resourceManager.getString(R.string.asset_add_evm_token_already_exist_title) + val message = if (failure.canModify) { + resourceManager.getString(R.string.asset_add_evm_token_already_exist_modifiable_message, failure.alreadyExistingSymbol) + } else { resourceManager.getString(R.string.asset_add_evm_token_already_exist_message, failure.alreadyExistingSymbol) + } + + title to message } is AddEvmTokensValidationFailure.InvalidTokenContractAddress -> { resourceManager.getString(R.string.asset_add_evm_token_invalid_contract_address_title) to diff --git a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt index eb3bf18dc6..6eb7ea2ba3 100644 --- a/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt +++ b/feature-crowdloan-impl/src/main/java/io/novafoundation/nova/feature_crowdloan_impl/presentation/contribute/select/CrowdloanContributeViewModel.kt @@ -16,6 +16,7 @@ import io.novafoundation.nova.common.validation.CompositeValidation import io.novafoundation.nova.common.validation.ValidationExecutor import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.progressConsumer +import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_crowdloan_impl.R import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.CustomContributeManager import io.novafoundation.nova.feature_crowdloan_impl.di.customCrowdloan.hasExtraBonusFlow @@ -267,12 +268,14 @@ class CrowdloanContributeViewModel( feeConstructor = { val crowdloan = crowdloanFlow.first() - contributionInteractor.estimateFee( + val fee = contributionInteractor.estimateFee( crowdloan, amount, bonusActiveState?.payload, customizationPayload, ) + + InlineFee(fee) }, onRetryCancelled = ::backClicked ) diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt index 063009b4cc..0c4109c18f 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/data/evmApi/EvmApi.kt @@ -1,5 +1,7 @@ package io.novafoundation.nova.feature_external_sign_impl.data.evmApi +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProvider +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.EvmChainSource.UnknownChainOptions import io.novafoundation.nova.runtime.ethereum.sendSuspend @@ -63,6 +65,7 @@ interface EvmApi { class EvmApiFactory( private val okHttpClient: OkHttpClient, private val chainRegistry: ChainRegistry, + private val gasPriceProviderFactory: GasPriceProviderFactory, ) { suspend fun create(chainSource: EvmChainSource): EvmApi? { @@ -70,12 +73,21 @@ class EvmApiFactory( val unknownChainOptions = chainSource.unknownChainOptions return when { - knownWeb3jApi != null -> Web3JEvmApi(knownWeb3jApi, shouldShutdown = false) + knownWeb3jApi != null -> { + Web3JEvmApi( + web3 = knownWeb3jApi, + shouldShutdown = false, + gasPriceProvider = gasPriceProviderFactory.create(knownWeb3jApi) + ) + } unknownChainOptions is UnknownChainOptions.WithFallBack -> { + val web3Api = createWeb3j(unknownChainOptions.evmChain.rpcUrl) + Web3JEvmApi( - web3 = createWeb3j(unknownChainOptions.evmChain.rpcUrl), - shouldShutdown = true + web3 = web3Api, + shouldShutdown = true, + gasPriceProvider = gasPriceProviderFactory.create(web3Api) ) } @@ -91,6 +103,7 @@ class EvmApiFactory( private class Web3JEvmApi( private val web3: Web3j, private val shouldShutdown: Boolean, + private val gasPriceProvider: GasPriceProvider, ) : EvmApi { override suspend fun formTransaction( @@ -103,7 +116,7 @@ private class Web3JEvmApi( gasPrice: BigInteger?, ): RawTransaction { val finalNonce = nonce ?: getNonce(fromAddress) - val finalGasPrice = gasPrice ?: getGasPrice() + val finalGasPrice = gasPrice ?: gasPriceProvider.getGasPrice() val dataOrDefault = data.orEmpty() @@ -172,10 +185,6 @@ private class Web3JEvmApi( return web3.ethSendRawTransaction(transactionData).sendSuspend().transactionHash } - private suspend fun getGasPrice(): BigInteger { - return web3.ethGasPrice().sendSuspend().gasPrice - } - private suspend fun getNonce(address: String): BigInteger { return web3.ethGetTransactionCount(address, DefaultBlockParameterName.PENDING) .sendSuspend() diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt index a633736818..cc6c9bea3a 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/ExternalSignFeatureDependencies.kt @@ -17,6 +17,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenReposito import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.runtime.di.ExtrinsicSerialization +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import okhttp3.OkHttpClient @@ -58,4 +59,6 @@ interface ExternalSignFeatureDependencies { val validationExecutor: ValidationExecutor val signerProvider: SignerProvider + + val gasPriceProviderFactory: GasPriceProviderFactory } diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/EvmSignModule.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/EvmSignModule.kt index b6d597dea5..07f76cc24e 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/EvmSignModule.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/di/modules/sign/EvmSignModule.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.Provides import io.novafoundation.nova.common.address.AddressIconGenerator import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository @@ -22,9 +23,10 @@ class EvmSignModule { @FeatureScope fun provideEthereumApiFactory( okHttpClient: OkHttpClient, - chainRegistry: ChainRegistry + chainRegistry: ChainRegistry, + gasPriceProviderFactory: GasPriceProviderFactory, ): EvmApiFactory { - return EvmApiFactory(okHttpClient, chainRegistry) + return EvmApiFactory(okHttpClient, chainRegistry, gasPriceProviderFactory) } @Provides diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/ExternalSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/ExternalSignInteractor.kt index df3cc46ecd..9df0379c70 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/ExternalSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/ExternalSignInteractor.kt @@ -1,9 +1,9 @@ package io.novafoundation.nova.feature_external_sign_impl.domain.sign import io.novafoundation.nova.common.address.AddressModel +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.Token import kotlinx.coroutines.flow.Flow @@ -21,9 +21,9 @@ interface ExternalSignInteractor { fun commissionTokenFlow(): Flow? - suspend fun calculateFee(): Balance? + suspend fun calculateFee(): Fee? - suspend fun performOperation(): ExternalSignCommunicator.Response? + suspend fun performOperation(upToDateFee: Fee?): ExternalSignCommunicator.Response? suspend fun readableOperationContent(): String diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt index 7df1db64fe..d206a821b1 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/Validations.kt @@ -3,16 +3,19 @@ package io.novafoundation.nova.feature_external_sign_impl.domain.sign import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_wallet_api.domain.model.Token import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeChangeDetectedFailure +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import java.math.BigDecimal import java.math.BigInteger sealed class ConfirmDAppOperationValidationFailure { - object NotEnoughBalanceToPayFees : ConfirmDAppOperationValidationFailure() + class FeeSpikeDetected(override val payload: FeeChangeDetectedFailure.Payload) : ConfirmDAppOperationValidationFailure(), FeeChangeDetectedFailure } -class ConfirmDAppOperationValidationPayload( - val token: Token? +data class ConfirmDAppOperationValidationPayload( + val token: Token?, + val decimalFee: DecimalFee? ) inline fun ConfirmDAppOperationValidationPayload.convertingToAmount(planks: () -> BigInteger): BigDecimal { diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt index 73b717d02a..d0f712d106 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/evm/EvmSignInteractor.kt @@ -12,8 +12,11 @@ import io.novafoundation.nova.common.utils.invoke import io.novafoundation.nova.common.utils.lazyAsync import io.novafoundation.nova.common.utils.parseArbitraryObject import io.novafoundation.nova.common.utils.singleReplaySharedFlow -import io.novafoundation.nova.common.validation.EmptyValidationSystem +import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_account_api.data.model.EvmFee +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.zero import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel @@ -34,11 +37,12 @@ import io.novafoundation.nova.feature_external_sign_api.model.signPayload.evm.Ev import io.novafoundation.nova.feature_external_sign_impl.data.evmApi.EvmApi import io.novafoundation.nova.feature_external_sign_impl.data.evmApi.EvmApiFactory import io.novafoundation.nova.feature_external_sign_impl.domain.sign.BaseExternalSignInteractor +import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationFailure import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationSystem import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ExternalSignInteractor -import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForFeeChanges import io.novafoundation.nova.runtime.ext.utilityAsset import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -110,7 +114,16 @@ class EvmSignInteractor( } } - override val validationSystem: ConfirmDAppOperationValidationSystem = EmptyValidationSystem() + override val validationSystem: ConfirmDAppOperationValidationSystem = ValidationSystem { + if (payload is ConfirmTx) { + checkForFeeChanges( + calculateFee = { calculateFee() }, + currentFee = { it.decimalFee!!.decimalAmount }, + chainAsset = { it.token!!.configuration }, + error = ConfirmDAppOperationValidationFailure::FeeSpikeDetected + ) + } + } override suspend fun createAccountAddressModel(): AddressModel = withContext(Dispatchers.Default) { val address = request.payload.originAddress @@ -145,21 +158,21 @@ class EvmSignInteractor( } } - override suspend fun calculateFee(): Balance = withContext(Dispatchers.Default) { - if (payload !is ConfirmTx) return@withContext Balance.ZERO + override suspend fun calculateFee(): Fee = withContext(Dispatchers.Default) { + if (payload !is ConfirmTx) return@withContext Fee.zero() - val api = ethereumApi() ?: return@withContext Balance.ZERO + val api = ethereumApi() ?: return@withContext Fee.zero() - val tx = api.formTransaction(payload.transaction) + val tx = api.formTransaction(payload.transaction, feeOverride = null) mostRecentFormedTx.emit(tx) tx.fee() } - override suspend fun performOperation(): ExternalSignCommunicator.Response? = withContext(Dispatchers.Default) { + override suspend fun performOperation(upToDateFee: Fee?): ExternalSignCommunicator.Response? = withContext(Dispatchers.Default) { runCatching { when (payload) { - is ConfirmTx -> confirmTx(payload.transaction, payload.chainSource.evmChainId.toLong(), payload.action) + is ConfirmTx -> confirmTx(payload.transaction, upToDateFee, payload.chainSource.evmChainId.toLong(), payload.action) is SignTypedMessage -> signTypedMessage(payload.message) is PersonalSign -> personalSign(payload.message) } @@ -182,10 +195,10 @@ class EvmSignInteractor( ethereumApi()?.shutdown() } - private suspend fun confirmTx(basedOn: EvmTransaction, evmChainId: Long, action: ConfirmTx.Action): ExternalSignCommunicator.Response { + private suspend fun confirmTx(basedOn: EvmTransaction, upToDateFee: Fee?, evmChainId: Long, action: ConfirmTx.Action): ExternalSignCommunicator.Response { val api = requireNotNull(ethereumApi()) - val tx = api.formTransaction(basedOn) + val tx = api.formTransaction(basedOn, upToDateFee) val originAccountId = originAccountId() val signer = resolveWalletSigner() @@ -253,19 +266,23 @@ class EvmSignInteractor( ) } - private suspend fun EvmApi.formTransaction(basedOn: EvmTransaction): RawTransaction { + private suspend fun EvmApi.formTransaction(basedOn: EvmTransaction, feeOverride: Fee?): RawTransaction { return when (basedOn) { is EvmTransaction.Raw -> TransactionDecoder.decode(basedOn.rawContent) - is EvmTransaction.Struct -> formTransaction( - fromAddress = basedOn.from, - toAddress = basedOn.to, - data = basedOn.data, - value = basedOn.value?.decodeEvmQuantity(), - nonce = basedOn.nonce?.decodeEvmQuantity(), - gasLimit = basedOn.gas?.decodeEvmQuantity(), - gasPrice = basedOn.gasPrice?.decodeEvmQuantity() - ) + is EvmTransaction.Struct -> { + val evmFee = feeOverride.castOrNull() + + formTransaction( + fromAddress = basedOn.from, + toAddress = basedOn.to, + data = basedOn.data, + value = basedOn.value?.decodeEvmQuantity(), + nonce = basedOn.nonce?.decodeEvmQuantity(), + gasLimit = evmFee?.gasLimit ?: basedOn.gas?.decodeEvmQuantity(), + gasPrice = evmFee?.gasPrice ?: basedOn.gasPrice?.decodeEvmQuantity() + ) + } } } @@ -297,7 +314,7 @@ class EvmSignInteractor( ) } - private fun RawTransaction.fee() = gasLimit * gasPrice + private fun RawTransaction.fee(): Fee = EvmFee(gasLimit = gasLimit, gasPrice = gasPrice) private fun originAccountId() = payload.originAddress.asEthereumAddress().toAccountId().value diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt index e778efe625..cd7f19df13 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/domain/sign/polkadot/PolkadotExternalSignInteractor.kt @@ -10,6 +10,7 @@ import io.novafoundation.nova.common.utils.intFromHex import io.novafoundation.nova.common.validation.EmptyValidationSystem import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi @@ -48,7 +49,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext -import java.math.BigInteger class PolkadotSignInteractorFactory( private val extrinsicService: ExtrinsicService, @@ -116,7 +116,7 @@ class PolkadotExternalSignInteractor( } } - override suspend fun performOperation(): ExternalSignCommunicator.Response? = withContext(Dispatchers.Default) { + override suspend fun performOperation(upToDateFee: Fee?): ExternalSignCommunicator.Response? = withContext(Dispatchers.Default) { runCatching { when (signPayload) { is PolkadotSignPayload.Json -> signExtrinsic(signPayload) @@ -139,7 +139,7 @@ class PolkadotExternalSignInteractor( } } - override suspend fun calculateFee(): BigInteger? = withContext(Dispatchers.Default) { + override suspend fun calculateFee(): Fee? = withContext(Dispatchers.Default) { require(signPayload is PolkadotSignPayload.Json) val chain = signPayload.chainOrNull() ?: return@withContext null diff --git a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt index e75fd51200..759be3697e 100644 --- a/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt +++ b/feature-external-sign-impl/src/main/java/io/novafoundation/nova/feature_external_sign_impl/presentation/signExtrinsic/ExternaSignViewModel.kt @@ -1,14 +1,17 @@ package io.novafoundation.nova.feature_external_sign_impl.presentation.signExtrinsic import io.novafoundation.nova.common.base.BaseViewModel -import io.novafoundation.nova.common.base.TitleAndMessage import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction import io.novafoundation.nova.common.mixin.api.Validatable import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.flowOf +import io.novafoundation.nova.common.validation.TransformedFailure import io.novafoundation.nova.common.validation.ValidationExecutor +import io.novafoundation.nova.common.validation.ValidationFlowActions +import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.progressConsumer +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletModel import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator.Response @@ -20,8 +23,10 @@ import io.novafoundation.nova.feature_external_sign_impl.R import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationFailure import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ConfirmDAppOperationValidationPayload import io.novafoundation.nova.feature_external_sign_impl.domain.sign.ExternalSignInteractor +import io.novafoundation.nova.feature_wallet_api.domain.validation.handleFeeSpikeDetected import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.WithFeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.awaitOptionalDecimalFee import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -93,21 +98,23 @@ class ExternaSignViewModel( fun acceptClicked() = launch { val validationPayload = ConfirmDAppOperationValidationPayload( - token = commissionTokenFlow?.first() + token = commissionTokenFlow?.first(), + decimalFee = originFeeMixin?.awaitOptionalDecimalFee() ) validationExecutor.requireValid( validationSystem = interactor.validationSystem, payload = validationPayload, - validationFailureTransformer = ::validationFailureToUi, + validationFailureTransformerCustom = ::validationFailureToUi, + autoFixPayload = ::autoFixPayload, progressConsumer = _performingOperationInProgress.progressConsumer() ) { - performOperation() + performOperation(it.decimalFee?.fee) } } - private fun performOperation() = launch { - interactor.performOperation()?.let { response -> + private fun performOperation(upToDateFee: Fee?) = launch { + interactor.performOperation(upToDateFee)?.let { response -> responder.respond(response) exit() @@ -117,7 +124,7 @@ class ExternaSignViewModel( } private fun maybeLoadFee() { - originFeeMixin?.loadFee( + originFeeMixin?.loadFeeV2( coroutineScope = this, feeConstructor = { interactor.calculateFee() }, onRetryCancelled = {} @@ -155,21 +162,37 @@ class ExternaSignViewModel( } } - fun exit() = launch { + private fun exit() = launch { interactor.shutdown() router.back() } - private fun validationFailureToUi(failure: ConfirmDAppOperationValidationFailure): TitleAndMessage { - return when (failure) { - ConfirmDAppOperationValidationFailure.NotEnoughBalanceToPayFees -> { - resourceManager.getString(R.string.common_not_enough_funds_title) to - resourceManager.getString(R.string.common_not_enough_funds_message) + private fun validationFailureToUi( + failure: ValidationStatus.NotValid, + actions: ValidationFlowActions + ): TransformedFailure? { + return when (val reason = failure.reason) { + is ConfirmDAppOperationValidationFailure.FeeSpikeDetected -> originFeeMixin?.let { + handleFeeSpikeDetected( + error = reason, + resourceManager = resourceManager, + feeLoaderMixin = originFeeMixin, + actions = actions + ) } } } + private fun autoFixPayload( + payload: ConfirmDAppOperationValidationPayload, + failure: ConfirmDAppOperationValidationFailure + ): ConfirmDAppOperationValidationPayload { + return when (failure) { + is ConfirmDAppOperationValidationFailure.FeeSpikeDetected -> payload.copy(decimalFee = failure.payload.newFee) + } + } + private fun Flow>.finishOnFailure(): Flow { return onEach { result -> result.onFailure { respondError(it) } } .map { it.getOrNull() } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/updaters/chain/StakingDashboardRelayStakingUpdater.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/updaters/chain/StakingDashboardRelayStakingUpdater.kt index 34b29e57ad..6e02ffac80 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/updaters/chain/StakingDashboardRelayStakingUpdater.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/dashboard/network/updaters/chain/StakingDashboardRelayStakingUpdater.kt @@ -147,8 +147,9 @@ class StakingDashboardRelayStakingUpdater( ): Status? { return when { baseInfo == null -> null + baseInfo.nominations == null -> Status.INACTIVE chainStakingStats.accountPresentInActiveStakers -> Status.ACTIVE - baseInfo.nominations != null && baseInfo.nominations.isWaiting(activeEra) -> Status.WAITING + baseInfo.nominations.isWaiting(activeEra) -> Status.WAITING else -> Status.INACTIVE } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/StakeActionsValidationModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/StakeActionsValidationModule.kt index 827f8f7982..65bc197a0e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/StakeActionsValidationModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/validations/StakeActionsValidationModule.kt @@ -7,6 +7,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import io.novafoundation.nova.common.di.scope.FeatureScope import io.novafoundation.nova.common.validation.CompositeValidation +import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState @@ -16,6 +17,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.validations.main.MainS import io.novafoundation.nova.feature_staking_impl.domain.validations.main.MainStakingUnlockingLimitValidation import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_REWARD_DESTINATION import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_BOND_MORE +import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_REBAG import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_REBOND import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_REDEEM import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_UNBOND @@ -128,6 +130,16 @@ class StakeActionsValidationsModule { ) ) + @FeatureScope + @Named(SYSTEM_MANAGE_STAKING_REBAG) + @Provides + fun provideRebagValidationSystem( + @Named(BALANCE_REQUIRED_STASH) + stashRequiredValidation: MainStakingAccountRequiredValidation + ): StakeActionsValidationSystem = ValidationSystem { + validate(stashRequiredValidation) + } + @FeatureScope @Named(SYSTEM_MANAGE_REWARD_DESTINATION) @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt index 9837835039..56fe63f422 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/StakingInteractorExt.kt @@ -86,8 +86,10 @@ fun minimumStake( val lastElectedBag = bagListLocator.bagBoundaries(bagListScoreConverter.scoreOf(minElectedStake)) - val nextBagThreshold = bagListScoreConverter.balanceOf(lastElectedBag.endInclusive) + val nextBagThreshold = bagListScoreConverter.balanceOf(lastElectedBag.endInclusive ?: lastElectedBag.start) val epsilon = Balance.ONE - return nextBagThreshold + epsilon + val nextBagRequiredAmount = nextBagThreshold + epsilon + + return nextBagRequiredAmount.coerceAtLeast(minElectedStake) } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/BagListLocator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/BagListLocator.kt index 189ca295b4..76cc783602 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/BagListLocator.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/BagListLocator.kt @@ -1,16 +1,13 @@ package io.novafoundation.nova.feature_staking_impl.domain.bagList +import io.novafoundation.nova.common.utils.rangeTo import io.novafoundation.nova.feature_staking_impl.domain.model.BagListNode.Score interface BagListLocator { fun bagBoundaries(userScore: Score): BagScoreBoundaries - - fun nextBagBoundaries(previous: BagScoreBoundaries): BagScoreBoundaries } -typealias BagScoreBoundaries = ClosedRange - fun BagListLocator(thresholds: List): BagListLocator = RealBagListLocator(thresholds) private class RealBagListLocator(private val thresholds: List) : BagListLocator { @@ -21,15 +18,8 @@ private class RealBagListLocator(private val thresholds: List) : BagListL return bagBoundariesAt(bagIndex) } - override fun nextBagBoundaries(previous: BagScoreBoundaries): BagScoreBoundaries { - val previousBagIndex = notionalBagIndexFor(previous.endInclusive) - val nextBagIndex = (previousBagIndex + 1).coerceAtMost(thresholds.size - 1) - - return bagBoundariesAt(nextBagIndex) - } - private fun bagBoundariesAt(index: Int): BagScoreBoundaries { - val bagUpper = thresholds[index] + val bagUpper = thresholds.getOrNull(index) val bagLower = if (index > 0) thresholds[index - 1] else Score.zero() return bagLower..bagUpper diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/BagScoreBoundaries.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/BagScoreBoundaries.kt new file mode 100644 index 0000000000..7e578e43ec --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/BagScoreBoundaries.kt @@ -0,0 +1,6 @@ +package io.novafoundation.nova.feature_staking_impl.domain.bagList + +import io.novafoundation.nova.common.utils.SemiUnboundedRange +import io.novafoundation.nova.feature_staking_impl.domain.model.BagListNode.Score + +typealias BagScoreBoundaries = SemiUnboundedRange diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagMovement.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagMovement.kt index c2bd074a64..99e8ff71df 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagMovement.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/bagList/rebag/RebagMovement.kt @@ -1,8 +1,9 @@ package io.novafoundation.nova.feature_staking_impl.domain.bagList.rebag +import io.novafoundation.nova.common.utils.SemiUnboundedRange import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance -typealias BagAmountBoundaries = ClosedRange +typealias BagAmountBoundaries = SemiUnboundedRange class RebagMovement( val from: BagAmountBoundaries, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/main/ValidationSystems.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/main/ValidationSystems.kt index 58f1df8741..a5ce0048e5 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/main/ValidationSystems.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validations/main/ValidationSystems.kt @@ -10,5 +10,6 @@ const val SYSTEM_MANAGE_REWARD_DESTINATION = "ManageStakingRewardDestination" const val SYSTEM_MANAGE_PAYOUTS = "ManageStakingPayouts" const val SYSTEM_MANAGE_VALIDATORS = "ManageStakingValidators" const val SYSTEM_MANAGE_CONTROLLER = "ManageStakingController" +const val SYSTEM_MANAGE_STAKING_REBAG = "ManageStakingRebag" typealias StakeActionsValidationSystem = ValidationSystem diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/alerts/relaychain/RelaychainAlertsComponent.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/alerts/relaychain/RelaychainAlertsComponent.kt index c0f6857020..c2071c2a8f 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/alerts/relaychain/RelaychainAlertsComponent.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/components/alerts/relaychain/RelaychainAlertsComponent.kt @@ -40,6 +40,7 @@ class RelaychainAlertsComponentFactory( private val resourceManager: ResourceManager, private val redeemValidationSystem: StakeActionsValidationSystem, private val bondMoreValidationSystem: StakeActionsValidationSystem, + private val rebagValidationSystem: StakeActionsValidationSystem, private val router: StakingRouter, private val stakingSharedComputation: StakingSharedComputation, ) { @@ -53,6 +54,7 @@ class RelaychainAlertsComponentFactory( alertsInteractor = alertsInteractor, redeemValidationSystem = redeemValidationSystem, bondMoreValidationSystem = bondMoreValidationSystem, + rebagValidationSystem = rebagValidationSystem, router = router, assetWithChain = assetWithChain, @@ -70,6 +72,7 @@ private class RelaychainAlertsComponent( private val redeemValidationSystem: StakeActionsValidationSystem, private val bondMoreValidationSystem: StakeActionsValidationSystem, + private val rebagValidationSystem: StakeActionsValidationSystem, private val router: StakingRouter, ) : AlertsComponent, CoroutineScope by hostContext.scope, @@ -138,7 +141,7 @@ private class RelaychainAlertsComponent( Alert.Rebag -> AlertModel( resourceManager.getString(R.string.staking_alert_rebag_title), resourceManager.getString(R.string.staking_alert_rebag_message), - AlertModel.Type.CallToAction { router.openRebag() } + AlertModel.Type.CallToAction(::rebagClicked) ) } } @@ -162,6 +165,10 @@ private class RelaychainAlertsComponent( router.openRedeem() } + private fun rebagClicked() = requireValidManageStakingAction(rebagValidationSystem) { + router.openRebag() + } + private fun requireValidManageStakingAction( validationSystem: StakeActionsValidationSystem, action: () -> Unit, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/di/components/RelaychainModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/di/components/RelaychainModule.kt index ecb82af09a..597dc9fbaa 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/di/components/RelaychainModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/presentation/staking/main/di/components/RelaychainModule.kt @@ -11,6 +11,7 @@ import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedCo import io.novafoundation.nova.feature_staking_impl.domain.period.StakingRewardPeriodInteractor import io.novafoundation.nova.feature_staking_impl.domain.staking.unbond.UnbondInteractor import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_BOND_MORE +import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_REBAG import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_REBOND import io.novafoundation.nova.feature_staking_impl.domain.validations.main.SYSTEM_MANAGE_STAKING_REDEEM import io.novafoundation.nova.feature_staking_impl.domain.validations.main.StakeActionsValidationSystem @@ -37,6 +38,7 @@ class RelaychainModule { resourceManager: ResourceManager, @Named(SYSTEM_MANAGE_STAKING_REDEEM) redeemValidationSystem: StakeActionsValidationSystem, @Named(SYSTEM_MANAGE_STAKING_BOND_MORE) bondMoreValidationSystem: StakeActionsValidationSystem, + @Named(SYSTEM_MANAGE_STAKING_REBAG) rebagValidationSystem: StakeActionsValidationSystem, router: StakingRouter, ) = RelaychainAlertsComponentFactory( stakingSharedComputation = stakingSharedComputation, @@ -44,6 +46,7 @@ class RelaychainModule { resourceManager = resourceManager, redeemValidationSystem = redeemValidationSystem, bondMoreValidationSystem = bondMoreValidationSystem, + rebagValidationSystem = rebagValidationSystem, router = router ) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt index bc598c8bd9..16aa419fe0 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/mappers/Fee.kt @@ -1,15 +1,37 @@ package io.novafoundation.nova.feature_wallet_api.data.mappers +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.FeeModel import io.novafoundation.nova.feature_wallet_api.presentation.model.mapAmountToAmountModel import java.math.BigDecimal fun mapFeeToFeeModel( - fee: BigDecimal, + fee: Fee, token: Token, includeZeroFiat: Boolean = true ) = FeeModel( - fee = fee, - display = mapAmountToAmountModel(fee, token, includeZeroFiat) + decimalFee = DecimalFee( + fee = fee, + decimalAmount = token.amountFromPlanks(fee.amount) + ), + display = mapAmountToAmountModel( + amountInPlanks = fee.amount, + token = token, + includeZeroFiat = includeZeroFiat + ) ) + +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated("Backward-compatible adapter") +fun mapFeeToFeeModel( + feeAmount: BigDecimal, + token: Token, + includeZeroFiat: Boolean = true +): FeeModel { + return mapFeeToFeeModel(InlineFee(token.planksFromAmount(feeAmount)), token, includeZeroFiat) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt index d23dae89ae..a57e130302 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransferValidations.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount +import io.novafoundation.nova.feature_wallet_api.domain.validation.FeeChangeDetectedFailure import io.novafoundation.nova.feature_wallet_api.domain.validation.NotEnoughToPayFeesError import io.novafoundation.nova.runtime.ext.commissionAsset import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -55,10 +56,14 @@ sealed class AssetTransferValidationFailure { object NonPositiveAmount : AssetTransferValidationFailure() object RecipientCannotAcceptTransfer : AssetTransferValidationFailure() + + class FeeChangeDetected(override val payload: FeeChangeDetectedFailure.Payload) : AssetTransferValidationFailure(), FeeChangeDetectedFailure + + object RecipientIsSystemAccount : AssetTransferValidationFailure() } data class AssetTransferPayload( - val transfer: AssetTransfer, + val transfer: WeightedAssetTransfer, val originFee: BigDecimal, val crossChainFee: BigDecimal?, val originCommissionAsset: Asset, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt index 137e15440f..363f88a1eb 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/blockhain/assets/tranfers/AssetTransfers.kt @@ -1,23 +1,60 @@ package io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers +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_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.runtime.ext.accountIdOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.fearless_utils.runtime.AccountId import java.math.BigDecimal -import java.math.BigInteger - -data class AssetTransfer( - val sender: MetaAccount, - val recipient: String, - val originChain: Chain, - val originChainAsset: Chain.Asset, - val destinationChain: Chain, - val destinationChainAsset: Chain.Asset, - val commissionAssetToken: Token, - val amount: BigDecimal, -) + +interface AssetTransfer { + val sender: MetaAccount + val recipient: String + val originChain: Chain + val originChainAsset: Chain.Asset + val destinationChain: Chain + val destinationChainAsset: Chain.Asset + val commissionAssetToken: Token + val amount: BigDecimal +} + +class BaseAssetTransfer( + override val sender: MetaAccount, + override val recipient: String, + override val originChain: Chain, + override val originChainAsset: Chain.Asset, + override val destinationChain: Chain, + override val destinationChainAsset: Chain.Asset, + override val commissionAssetToken: Token, + override val amount: BigDecimal, +) : AssetTransfer + +data class WeightedAssetTransfer( + override val sender: MetaAccount, + override val recipient: String, + override val originChain: Chain, + override val originChainAsset: Chain.Asset, + override val destinationChain: Chain, + override val destinationChainAsset: Chain.Asset, + override val commissionAssetToken: Token, + override val amount: BigDecimal, + val decimalFee: DecimalFee, +) : AssetTransfer { + + constructor(assetTransfer: AssetTransfer, fee: DecimalFee) : this( + sender = assetTransfer.sender, + recipient = assetTransfer.recipient, + originChain = assetTransfer.originChain, + originChainAsset = assetTransfer.originChainAsset, + destinationChain = assetTransfer.destinationChain, + destinationChainAsset = assetTransfer.destinationChainAsset, + commissionAssetToken = assetTransfer.commissionAssetToken, + amount = assetTransfer.amount, + decimalFee = fee + ) +} val AssetTransfer.isCrossChain get() = originChain.id != destinationChain.id @@ -30,9 +67,9 @@ interface AssetTransfers { val validationSystem: AssetTransfersValidationSystem - suspend fun calculateFee(transfer: AssetTransfer): BigInteger + suspend fun calculateFee(transfer: AssetTransfer): Fee - suspend fun performTransfer(transfer: AssetTransfer): Result + suspend fun performTransfer(transfer: WeightedAssetTransfer): Result suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt index 0a4c4ddc7c..7ade311ae8 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/data/network/crosschain/CrossChainTransactor.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.data.network.crosschain +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem import io.novafoundation.nova.feature_wallet_api.domain.model.CrossChainTransferConfiguration @@ -12,7 +13,7 @@ interface CrossChainTransactor { suspend fun estimateOriginFee( configuration: CrossChainTransferConfiguration, transfer: AssetTransfer - ): BigInteger + ): Fee suspend fun performTransfer( configuration: CrossChainTransferConfiguration, diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/ChainAssetRepository.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/ChainAssetRepository.kt index d14a929283..09f2768517 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/ChainAssetRepository.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/interfaces/ChainAssetRepository.kt @@ -9,7 +9,5 @@ interface ChainAssetRepository { suspend fun insertCustomAsset(chainAsset: Chain.Asset) - suspend fun getAssetSymbol(id: FullChainAssetId): String? - suspend fun getEnabledAssets(): List } diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Fee.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Fee.kt deleted file mode 100644 index a896273dbf..0000000000 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/model/Fee.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.novafoundation.nova.feature_wallet_api.domain.model - -import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigDecimal - -class Fee( - val transferAmount: BigDecimal, - val feeAmount: BigDecimal, - val type: Chain.Asset -) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EvmAssetExistenceValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EvmAssetExistenceValidation.kt index d2905160bc..a99a51f5fe 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EvmAssetExistenceValidation.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/EvmAssetExistenceValidation.kt @@ -5,18 +5,21 @@ import io.novafoundation.nova.common.validation.ValidationStatus import io.novafoundation.nova.common.validation.ValidationSystemBuilder import io.novafoundation.nova.common.validation.valid import io.novafoundation.nova.common.validation.validationError -import io.novafoundation.nova.feature_wallet_api.domain.interfaces.ChainAssetRepository +import io.novafoundation.nova.common.validation.validationWarning +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.assetOrNull import io.novafoundation.nova.runtime.multiNetwork.chain.mappers.chainAssetIdOfErc20Token import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.Source import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId -typealias AssetNotExistError = (existingSymbol: String) -> E +typealias AssetNotExistError = (existingSymbol: String, canModify: Boolean) -> E class EvmAssetExistenceValidation( - private val assetRepository: ChainAssetRepository, + private val chainRegistry: ChainRegistry, private val chain: (P) -> Chain, private val contractAddress: (P) -> String, - private val assetNotExistError: AssetNotExistError, + private val assetAlreadyExists: AssetNotExistError, private val addressMappingError: (P) -> E, ) : Validation { @@ -24,12 +27,13 @@ class EvmAssetExistenceValidation( return try { val assetId = chainAssetIdOfErc20Token(contractAddress(value)) val fullAssetId = FullChainAssetId(chain(value).id, assetId) - val alreadyExistingSymbol = assetRepository.getAssetSymbol(fullAssetId) + val alreadyExistingAsset = chainRegistry.assetOrNull(fullAssetId) - if (alreadyExistingSymbol != null) { - validationError(assetNotExistError(alreadyExistingSymbol)) - } else { - valid() + when { + alreadyExistingAsset == null -> valid() + // we only allow to modify manually added tokens. Default tokens should remain unchanged + alreadyExistingAsset.source == Source.MANUAL -> assetAlreadyExists(alreadyExistingAsset.symbol, true).validationWarning() + else -> assetAlreadyExists(alreadyExistingAsset.symbol, false).validationError() } } catch (e: Exception) { validationError(addressMappingError(value)) @@ -37,18 +41,18 @@ class EvmAssetExistenceValidation( } } -fun ValidationSystemBuilder.evmAssetNotExist( - assetRepository: ChainAssetRepository, +fun ValidationSystemBuilder.evmAssetNotExists( + chainRegistry: ChainRegistry, chain: (P) -> Chain, address: (P) -> String, assetNotExistError: AssetNotExistError, addressMappingError: (P) -> E ) = validate( EvmAssetExistenceValidation( - assetRepository = assetRepository, + chainRegistry = chainRegistry, chain = chain, contractAddress = address, - assetNotExistError = assetNotExistError, + assetAlreadyExists = assetNotExistError, addressMappingError = addressMappingError ) ) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt new file mode 100644 index 0000000000..5659718d4d --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/domain/validation/FeeChangeValidation.kt @@ -0,0 +1,125 @@ +package io.novafoundation.nova.feature_wallet_api.domain.validation + +import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer.Payload +import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer.Payload.DialogAction +import io.novafoundation.nova.common.resources.ResourceManager +import io.novafoundation.nova.common.utils.hasTheSaveValueAs +import io.novafoundation.nova.common.validation.TransformedFailure +import io.novafoundation.nova.common.validation.Validation +import io.novafoundation.nova.common.validation.ValidationFlowActions +import io.novafoundation.nova.common.validation.ValidationStatus +import io.novafoundation.nova.common.validation.ValidationSystemBuilder +import io.novafoundation.nova.common.validation.isTrueOrError +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_wallet_api.R +import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.presentation.formatters.formatTokenAmount +import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.math.BigDecimal + +private val FEE_RATIO_THRESHOLD = 1.5.toBigDecimal() + +class FeeChangeValidation( + private val calculateFee: suspend (P) -> DecimalFee, + private val currentFee: (P) -> BigDecimal, + private val chainAsset: (P) -> Chain.Asset, + private val error: (FeeChangeDetectedFailure.Payload) -> E +) : Validation { + + override suspend fun validate(value: P): ValidationStatus { + val oldFee = currentFee(value) + val newFee = calculateFee(value) + + val areFeesSame = oldFee hasTheSaveValueAs newFee.decimalAmount + + return areFeesSame isTrueOrError { + val payload = FeeChangeDetectedFailure.Payload( + needsUserAttention = newFee.decimalAmount / oldFee > FEE_RATIO_THRESHOLD, + oldFee = oldFee, + newFee = newFee, + chainAsset = chainAsset(value) + ) + + error(payload) + } + } +} + +interface FeeChangeDetectedFailure { + + class Payload( + val needsUserAttention: Boolean, + val oldFee: BigDecimal, + val newFee: DecimalFee, + val chainAsset: Chain.Asset, + ) + + val payload: Payload +} + +fun ValidationSystemBuilder.checkForFeeChanges( + calculateFee: suspend (P) -> Fee, + currentFee: (P) -> BigDecimal, + chainAsset: (P) -> Chain.Asset, + error: (FeeChangeDetectedFailure.Payload) -> E +) = validate( + FeeChangeValidation( + calculateFee = { payload -> + val newFee = calculateFee(payload) + + DecimalFee( + fee = newFee, + decimalAmount = chainAsset(payload).amountFromPlanks(newFee.amount) + ) + }, + currentFee = currentFee, + error = error, + chainAsset = chainAsset + ) +) + +fun CoroutineScope.handleFeeSpikeDetected( + error: FeeChangeDetectedFailure, + resourceManager: ResourceManager, + feeLoaderMixin: FeeLoaderMixin.Presentation, + actions: ValidationFlowActions +): TransformedFailure? { + if (!error.payload.needsUserAttention) { + actions.resumeFlow() + return null + } + + val chainAsset = error.payload.chainAsset + val oldFee = error.payload.oldFee.formatTokenAmount(chainAsset) + val newFee = error.payload.newFee.decimalAmount.formatTokenAmount(chainAsset) + + return TransformedFailure.Custom( + Payload( + title = resourceManager.getString(R.string.common_fee_changed_title), + message = resourceManager.getString(R.string.common_fee_changed_message, newFee, oldFee), + customStyle = R.style.AccentNegativeAlertDialogTheme_Reversed, + okAction = DialogAction( + title = resourceManager.getString(R.string.common_proceed), + action = { + launch { + feeLoaderMixin.setFee(error.payload.newFee.fee) + + actions.resumeFlow() + } + } + ), + cancelAction = DialogAction( + title = resourceManager.getString(R.string.common_refresh_fee), + action = { + launch { + feeLoaderMixin.setFee(error.payload.newFee.fee) + } + } + ) + ) + ) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/Formatters.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/Formatters.kt index 9612d7d767..405125c77b 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/Formatters.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/formatters/Formatters.kt @@ -1,5 +1,6 @@ package io.novafoundation.nova.feature_wallet_api.presentation.formatters +import io.novafoundation.nova.common.utils.SemiUnboundedRange import io.novafoundation.nova.common.utils.formatting.format import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks @@ -12,11 +13,17 @@ fun BigInteger.formatPlanks(chainAsset: Chain.Asset): String { return chainAsset.amountFromPlanks(this).formatTokenAmount(chainAsset) } -fun ClosedRange.formatPlanksRange(chainAsset: Chain.Asset): String { +fun SemiUnboundedRange.formatPlanksRange(chainAsset: Chain.Asset): String { + val end = endInclusive val startFormatted = chainAsset.amountFromPlanks(start).format() - val endFormatted = endInclusive.formatPlanks(chainAsset) - return "$startFormatted β€” $endFormatted" + return if (end != null) { + val endFormatted = end.formatPlanks(chainAsset) + + "$startFormatted β€” $endFormatted" + } else { + "$startFormatted+".withTokenSymbol(chainAsset.symbol) + } } fun BigDecimal.formatTokenAmount(chainAsset: Chain.Asset, roundingMode: RoundingMode = RoundingMode.FLOOR): String { @@ -24,7 +31,11 @@ fun BigDecimal.formatTokenAmount(chainAsset: Chain.Asset, roundingMode: Rounding } fun BigDecimal.formatTokenAmount(tokenSymbol: String, roundingMode: RoundingMode = RoundingMode.FLOOR): String { - return "${format(roundingMode)} $tokenSymbol" + return format(roundingMode).withTokenSymbol(tokenSymbol) +} + +fun String.withTokenSymbol(tokenSymbol: String): String { + return "$this $tokenSymbol" } fun BigDecimal.formatTokenChange(chainAsset: Chain.Asset, isIncome: Boolean): String { diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt index 3bfc95209f..bba5595cae 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderMixin.kt @@ -1,19 +1,25 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee import androidx.lifecycle.LiveData +import androidx.lifecycle.asFlow import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.mixin.api.Retriable import io.novafoundation.nova.common.utils.inBackground +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.domain.TokenUseCase import io.novafoundation.nova.feature_wallet_api.domain.model.Asset import io.novafoundation.nova.feature_wallet_api.domain.model.Token +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee import io.novafoundation.nova.feature_wallet_api.presentation.model.FeeModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transform import java.math.BigDecimal import java.math.BigInteger @@ -39,7 +45,7 @@ interface FeeLoaderMixin : Retriable { suspend fun loadFeeSuspending( retryScope: CoroutineScope, - feeConstructor: suspend (Token) -> BigInteger?, + feeConstructor: suspend (Token) -> Fee?, onRetryCancelled: () -> Unit, ) @@ -49,14 +55,26 @@ interface FeeLoaderMixin : Retriable { onRetryCancelled: () -> Unit, ) - suspend fun setFee(fee: BigDecimal?) + fun loadFeeV2( + coroutineScope: CoroutineScope, + feeConstructor: suspend (Token) -> Fee?, + onRetryCancelled: () -> Unit, + ) + + suspend fun setFee(fee: Fee?) + + suspend fun setFee(feeAmount: BigDecimal?) fun requireFee( block: (BigDecimal) -> Unit, onError: (title: String, message: String) -> Unit, ) - suspend fun awaitFee(): BigDecimal + @Deprecated( + message = "Use `awaitDecimalFee` instead since it holds more information about fee", + replaceWith = ReplaceWith("awaitDecimalFee().decimalAmount") + ) + suspend fun awaitFee(): BigDecimal = awaitDecimalFee().decimalAmount fun requireOptionalFee( block: (BigDecimal?) -> Unit, @@ -73,6 +91,19 @@ interface FeeLoaderMixin : Retriable { } } +suspend fun FeeLoaderMixin.awaitDecimalFee(): DecimalFee = feeLiveData.asFlow() + .filterIsInstance() + .first().feeModel.decimalFee + +suspend fun FeeLoaderMixin.awaitOptionalDecimalFee(): DecimalFee? = feeLiveData.asFlow() + .transform { feeStatus -> + when (feeStatus) { + is FeeStatus.Loaded -> emit(feeStatus.feeModel.decimalFee) + FeeStatus.NoFee -> emit(null) + else -> {} // skip + } + }.first() + fun FeeLoaderMixin.Factory.create(assetFlow: Flow) = create(assetFlow.map { it.token }) fun FeeLoaderMixin.Factory.create(tokenUseCase: TokenUseCase) = create(tokenUseCase.currentTokenFlow()) @@ -137,7 +168,7 @@ fun FeeLoaderMixin.Presentation.connectWith( inputSource2: Flow, inputSource3: Flow, scope: CoroutineScope, - feeConstructor: suspend Token.(input1: I1, input2: I2, input3: I3) -> BigInteger?, + feeConstructor: suspend Token.(input1: I1, input2: I2, input3: I3) -> Fee?, onRetryCancelled: () -> Unit = {} ) { combine( @@ -145,7 +176,7 @@ fun FeeLoaderMixin.Presentation.connectWith( inputSource2, inputSource3, ) { input1, input2, input3 -> - loadFee( + loadFeeV2( coroutineScope = scope, feeConstructor = { feeConstructor(it, input1, input2, input3) }, onRetryCancelled = onRetryCancelled diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderProvider.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderProvider.kt index a53892812e..29f0ab4ee7 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderProvider.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeLoaderProvider.kt @@ -1,19 +1,19 @@ package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asFlow import io.novafoundation.nova.common.mixin.api.RetryPayload import io.novafoundation.nova.common.resources.ResourceManager import io.novafoundation.nova.common.utils.Event +import io.novafoundation.nova.feature_account_api.data.model.Fee +import io.novafoundation.nova.feature_account_api.data.model.InlineFee import io.novafoundation.nova.feature_wallet_api.R import io.novafoundation.nova.feature_wallet_api.data.mappers.mapFeeToFeeModel import io.novafoundation.nova.feature_wallet_api.domain.model.Token -import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks +import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -41,7 +41,7 @@ class FeeLoaderProvider( override suspend fun loadFeeSuspending( retryScope: CoroutineScope, - feeConstructor: suspend (Token) -> BigInteger?, + feeConstructor: suspend (Token) -> Fee?, onRetryCancelled: () -> Unit, ): Unit = withContext(Dispatchers.IO) { feeLiveData.postValue(FeeStatus.Loading) @@ -64,11 +64,29 @@ class FeeLoaderProvider( onRetryCancelled: () -> Unit, ) { coroutineScope.launch { - loadFeeSuspending(coroutineScope, feeConstructor, onRetryCancelled) + loadFeeSuspending( + retryScope = coroutineScope, + feeConstructor = { feeConstructor(it)?.let(::InlineFee) }, + onRetryCancelled = onRetryCancelled + ) + } + } + + override fun loadFeeV2( + coroutineScope: CoroutineScope, + feeConstructor: suspend (Token) -> Fee?, + onRetryCancelled: () -> Unit + ) { + coroutineScope.launch { + loadFeeSuspending( + retryScope = coroutineScope, + feeConstructor = feeConstructor, + onRetryCancelled = onRetryCancelled + ) } } - override suspend fun setFee(fee: BigDecimal?) { + override suspend fun setFee(fee: Fee?) { if (fee != null) { val token = tokenFlow.first() val feeModel = mapFeeToFeeModel(fee, token, includeZeroFiat = configuration.showZeroFiat) @@ -79,6 +97,15 @@ class FeeLoaderProvider( } } + override suspend fun setFee(feeAmount: BigDecimal?) { + val fee = feeAmount?.let { + val token = tokenFlow.first() + InlineFee(token.planksFromAmount(feeAmount)) + } + + setFee(fee) + } + override fun requireFee( block: (BigDecimal) -> Unit, onError: (title: String, message: String) -> Unit, @@ -86,7 +113,7 @@ class FeeLoaderProvider( val feeStatus = feeLiveData.value if (feeStatus is FeeStatus.Loaded) { - block(feeStatus.feeModel.fee) + block(feeStatus.feeModel.decimalFee.decimalAmount) } else { onError( resourceManager.getString(R.string.fee_not_yet_loaded_title), @@ -95,18 +122,12 @@ class FeeLoaderProvider( } } - override suspend fun awaitFee(): BigDecimal { - return feeLiveData.asFlow() - .filterIsInstance() - .first().feeModel.fee - } - override fun requireOptionalFee( block: (BigDecimal?) -> Unit, onError: (title: String, message: String) -> Unit ) { when (val status = feeLiveData.value) { - is FeeStatus.Loaded -> block(status.feeModel.fee) + is FeeStatus.Loaded -> block(status.feeModel.decimalFee.decimalAmount) is FeeStatus.NoFee -> block(null) else -> onError( resourceManager.getString(R.string.fee_not_yet_loaded_title), @@ -117,9 +138,8 @@ class FeeLoaderProvider( private fun onFeeLoaded( token: Token, - feeInPlanks: BigInteger? - ): FeeStatus = if (feeInPlanks != null) { - val fee = token.amountFromPlanks(feeInPlanks) + fee: Fee? + ): FeeStatus = if (fee != null) { val feeModel = mapFeeToFeeModel(fee, token, includeZeroFiat = configuration.showZeroFiat) FeeStatus.Loaded(feeModel) @@ -127,10 +147,10 @@ class FeeLoaderProvider( FeeStatus.NoFee } - fun onError( + private fun onError( exception: Throwable, retryScope: CoroutineScope, - feeConstructor: suspend (Token) -> BigInteger?, + feeConstructor: suspend (Token) -> Fee?, onRetryCancelled: () -> Unit, ) = if (exception !is CancellationException) { retryEvent.postValue( @@ -138,7 +158,7 @@ class FeeLoaderProvider( RetryPayload( title = resourceManager.getString(R.string.choose_amount_network_error), message = resourceManager.getString(R.string.choose_amount_error_fee), - onRetry = { loadFee(retryScope, feeConstructor, onRetryCancelled) }, + onRetry = { loadFeeV2(retryScope, feeConstructor, onRetryCancelled) }, onCancel = onRetryCancelled ) ) diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt new file mode 100644 index 0000000000..4e9e4c3f91 --- /dev/null +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/mixin/fee/FeeParcelModel.kt @@ -0,0 +1,43 @@ +package io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee + +import android.os.Parcelable +import io.novafoundation.nova.feature_account_api.data.model.EvmFee +import io.novafoundation.nova.feature_account_api.data.model.InlineFee +import io.novafoundation.nova.feature_wallet_api.presentation.model.DecimalFee +import kotlinx.android.parcel.Parcelize +import java.math.BigDecimal +import java.math.BigInteger + +sealed interface FeeParcelModel : Parcelable { + + val amount: BigDecimal +} + +@Parcelize +class EvmFeeParcelModel( + val gasLimit: BigInteger, + val gasPrice: BigInteger, + override val amount: BigDecimal +) : FeeParcelModel + +@Parcelize +class SimpleFeeParcelModel( + val planks: BigInteger, + override val amount: BigDecimal +) : FeeParcelModel + +fun mapFeeToParcel(decimalFee: DecimalFee): FeeParcelModel { + return when (val fee = decimalFee.fee) { + is EvmFee -> EvmFeeParcelModel(gasLimit = fee.gasLimit, gasPrice = fee.gasPrice, amount = decimalFee.decimalAmount) + else -> SimpleFeeParcelModel(decimalFee.fee.amount, decimalFee.decimalAmount) + } +} + +fun mapFeeFromParcel(parcelFee: FeeParcelModel): DecimalFee { + val fee = when (parcelFee) { + is EvmFeeParcelModel -> EvmFee(gasLimit = parcelFee.gasLimit, gasPrice = parcelFee.gasPrice) + is SimpleFeeParcelModel -> InlineFee(parcelFee.planks) + } + + return DecimalFee(fee, parcelFee.amount) +} diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt index 74f9352034..0df55d89b9 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/AmountModel.kt @@ -33,10 +33,12 @@ fun mapAmountToAmountModel( fun mapAmountToAmountModel( amountInPlanks: BigInteger, - token: Token + token: Token, + includeZeroFiat: Boolean = true, ): AmountModel = mapAmountToAmountModel( amount = token.amountFromPlanks(amountInPlanks), - token = token + token = token, + includeZeroFiat = includeZeroFiat ) fun mapAmountToAmountModel( diff --git a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt index 4288ca6acf..b10f9b692f 100644 --- a/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt +++ b/feature-wallet-api/src/main/java/io/novafoundation/nova/feature_wallet_api/presentation/model/FeeModel.kt @@ -1,8 +1,14 @@ package io.novafoundation.nova.feature_wallet_api.presentation.model +import io.novafoundation.nova.feature_account_api.data.model.Fee import java.math.BigDecimal class FeeModel( - val fee: BigDecimal, + val decimalFee: DecimalFee, val display: AmountModel, ) + +class DecimalFee( + val fee: Fee, + val decimalAmount: BigDecimal +) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt index c1c049a7e7..9a51a37e80 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/BaseAssetTransfers.kt @@ -2,12 +2,14 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_account_api.domain.model.accountIdIn import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.originFeeInUsedAsset import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.doNotCrossExistentialDeposit @@ -15,6 +17,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notPhishingRecipient import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientCommissionBalanceToStayAboveED import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee @@ -26,7 +29,6 @@ import io.novafoundation.nova.runtime.multiNetwork.getRuntime import jp.co.soramitsu.fearless_utils.runtime.extrinsic.ExtrinsicBuilder import jp.co.soramitsu.fearless_utils.runtime.metadata.callOrNull import jp.co.soramitsu.fearless_utils.runtime.metadata.moduleOrNull -import java.math.BigInteger abstract class BaseAssetTransfers( private val chainRegistry: ChainRegistry, @@ -43,7 +45,7 @@ abstract class BaseAssetTransfers( */ protected abstract suspend fun transferFunctions(chainAsset: Chain.Asset): List> - override suspend fun performTransfer(transfer: AssetTransfer): Result { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { val senderAccountId = transfer.sender.accountIdIn(transfer.originChain)!! return extrinsicService.submitExtrinsicWithAnySuitableWallet(transfer.originChain, senderAccountId) { @@ -51,8 +53,8 @@ abstract class BaseAssetTransfers( } } - override suspend fun calculateFee(transfer: AssetTransfer): BigInteger { - return extrinsicService.estimateFee(transfer.originChain) { + override suspend fun calculateFee(transfer: AssetTransfer): Fee { + return extrinsicService.estimateFeeV2(transfer.originChain) { transfer(transfer) } } @@ -67,6 +69,7 @@ abstract class BaseAssetTransfers( protected fun defaultValidationSystem(): AssetTransfersValidationSystem = ValidationSystem { validAddress() + recipientIsNotSystemAccount() notPhishingRecipient(phishingValidationFactory) diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt index 3a7d7fdbdb..989a07a99b 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/UnsupportedAssetTransfers.kt @@ -1,21 +1,22 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigInteger open class UnsupportedAssetTransfers : AssetTransfers { override val validationSystem: AssetTransfersValidationSystem get() = throw UnsupportedOperationException("Unsupported") - override suspend fun calculateFee(transfer: AssetTransfer): BigInteger { + override suspend fun calculateFee(transfer: AssetTransfer): Fee { throw UnsupportedOperationException("Unsupported") } - override suspend fun performTransfer(transfer: AssetTransfer): Result { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { return Result.failure(UnsupportedOperationException("Unsupported")) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/equilibrium/EquilibriumAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/equilibrium/EquilibriumAssetTransfers.kt index a81371f412..b04596a12e 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/equilibrium/EquilibriumAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/equilibrium/EquilibriumAssetTransfers.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValid import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.BaseAssetTransfers import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress @@ -42,6 +43,8 @@ class EquilibriumAssetTransfers( override val validationSystem: AssetTransfersValidationSystem = ValidationSystem { validAddress() + recipientIsNotSystemAccount() + positiveAmount() sufficientBalanceInUsedAsset() sufficientTransferableBalanceToPayOriginFee() diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt index 944e965777..532b5143f0 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmErc20/EvmErc20AssetTransfers.kt @@ -2,12 +2,16 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.checkForFeeChanges import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress @@ -18,7 +22,6 @@ import io.novafoundation.nova.runtime.ethereum.transaction.builder.contractCall import io.novafoundation.nova.runtime.ext.accountIdOrDefault import io.novafoundation.nova.runtime.ext.requireErc20 import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigInteger // a conservative upper limit. Usually transfer takes around 30-50k private val ERC_20_UPPER_GAS_LIMIT = 200_000.toBigInteger() @@ -31,6 +34,7 @@ class EvmErc20AssetTransfers( override val validationSystem: AssetTransfersValidationSystem = ValidationSystem { validAddress() + recipientIsNotSystemAccount() positiveAmount() @@ -38,16 +42,22 @@ class EvmErc20AssetTransfers( sufficientTransferableBalanceToPayOriginFee() recipientCanAcceptTransfer(assetSourceRegistry) + + checkForFeeChanges(assetSourceRegistry) } - override suspend fun calculateFee(transfer: AssetTransfer): BigInteger { + override suspend fun calculateFee(transfer: AssetTransfer): Fee { return evmTransactionService.calculateFee(transfer.originChain.id, fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT) { transfer(transfer) } } - override suspend fun performTransfer(transfer: AssetTransfer): Result { - return evmTransactionService.transact(transfer.originChain.id, fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT) { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { + return evmTransactionService.transact( + chainId = transfer.originChain.id, + presetFee = transfer.decimalFee.fee, + fallbackGasLimit = ERC_20_UPPER_GAS_LIMIT + ) { transfer(transfer) } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt index 22dfc01a83..52d98388a5 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/evmNative/EvmNativeAssetTransfers.kt @@ -2,12 +2,16 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.asset import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.checkForFeeChanges import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress @@ -15,7 +19,6 @@ import io.novafoundation.nova.feature_wallet_impl.domain.validaiton.recipientCan import io.novafoundation.nova.runtime.ethereum.transaction.builder.EvmTransactionBuilder import io.novafoundation.nova.runtime.ext.accountIdOrDefault import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain -import java.math.BigInteger // native coin transfer has a fixed fee private val NATIVE_COIN_TRANSFER_GAS_LIMIT = 21_000.toBigInteger() @@ -27,6 +30,7 @@ class EvmNativeAssetTransfers( override val validationSystem: AssetTransfersValidationSystem = ValidationSystem { validAddress() + recipientIsNotSystemAccount() positiveAmount() @@ -34,16 +38,22 @@ class EvmNativeAssetTransfers( sufficientTransferableBalanceToPayOriginFee() recipientCanAcceptTransfer(assetSourceRegistry) + + checkForFeeChanges(assetSourceRegistry) } - override suspend fun calculateFee(transfer: AssetTransfer): BigInteger { + override suspend fun calculateFee(transfer: AssetTransfer): Fee { return evmTransactionService.calculateFee(transfer.originChain.id, fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT) { nativeTransfer(transfer) } } - override suspend fun performTransfer(transfer: AssetTransfer): Result { - return evmTransactionService.transact(transfer.originChain.id, fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT) { + override suspend fun performTransfer(transfer: WeightedAssetTransfer): Result { + return evmTransactionService.transact( + chainId = transfer.originChain.id, + fallbackGasLimit = NATIVE_COIN_TRANSFER_GAS_LIMIT, + presetFee = transfer.decimalFee.fee, + ) { nativeTransfer(transfer) } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt index 63e6d94fe8..535ad9f3c1 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/blockchain/assets/transfers/validations/Common.kt @@ -1,15 +1,18 @@ package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations +import io.novafoundation.nova.feature_account_api.domain.validation.notSystemAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferValidationFailure.WillRemoveAccount import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystemBuilder +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.recipientOrNull import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.sendingAmountInCommissionAsset import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks import io.novafoundation.nova.feature_wallet_api.domain.validation.AmountProducer import io.novafoundation.nova.feature_wallet_api.domain.validation.PhishingValidationFactory +import io.novafoundation.nova.feature_wallet_api.domain.validation.checkForFeeChanges import io.novafoundation.nova.feature_wallet_api.domain.validation.doNotCrossExistentialDeposit import io.novafoundation.nova.feature_wallet_api.domain.validation.enoughTotalToStayAboveED import io.novafoundation.nova.feature_wallet_api.domain.validation.notPhishingAccount @@ -50,6 +53,18 @@ fun AssetTransfersValidationSystemBuilder.sufficientCommissionBalanceToStayAbove error = { AssetTransferValidationFailure.NotEnoughFunds.ToStayAboveED(it.transfer.originChain.commissionAsset) } ) +fun AssetTransfersValidationSystemBuilder.checkForFeeChanges( + assetSourceRegistry: AssetSourceRegistry +) = checkForFeeChanges( + calculateFee = { + val transfers = assetSourceRegistry.sourceFor(it.transfer.originChainAsset).transfers + transfers.calculateFee(it.transfer) + }, + currentFee = { it.originFee }, + chainAsset = { it.transfer.commissionAssetToken.configuration }, + error = AssetTransferValidationFailure::FeeChangeDetected +) + fun AssetTransfersValidationSystemBuilder.doNotCrossExistentialDeposit( assetSourceRegistry: AssetSourceRegistry, fee: AmountProducer, @@ -82,6 +97,11 @@ fun AssetTransfersValidationSystemBuilder.sufficientBalanceInUsedAsset() = suffi error = { _, _ -> AssetTransferValidationFailure.NotEnoughFunds.InUsedAsset } ) +fun AssetTransfersValidationSystemBuilder.recipientIsNotSystemAccount() = notSystemAccount( + accountId = { it.transfer.recipientOrNull() }, + error = { AssetTransferValidationFailure.RecipientIsSystemAccount } +) + private suspend fun AssetSourceRegistry.existentialDepositForUsedAsset(transfer: AssetTransfer): BigDecimal { return existentialDeposit(transfer.originChain, transfer.originChainAsset) } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt index 4452357ea6..71ba43495f 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/network/crosschain/RealCrossChainTransactor.kt @@ -6,6 +6,7 @@ import io.novafoundation.nova.common.utils.orZero import io.novafoundation.nova.common.utils.xcmPalletName import io.novafoundation.nova.common.validation.ValidationSystem import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService +import io.novafoundation.nova.feature_account_api.data.model.Fee import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem @@ -25,6 +26,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notDeadRecipientInUsedAsset import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.notPhishingRecipient import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount +import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientCommissionBalanceToStayAboveED import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress @@ -43,6 +45,7 @@ class RealCrossChainTransactor( override val validationSystem: AssetTransfersValidationSystem = ValidationSystem { positiveAmount() + recipientIsNotSystemAccount() validAddress() notPhishingRecipient(phishingValidationFactory) @@ -62,8 +65,8 @@ class RealCrossChainTransactor( ) } - override suspend fun estimateOriginFee(configuration: CrossChainTransferConfiguration, transfer: AssetTransfer): BigInteger { - return extrinsicService.estimateFee(transfer.originChain) { + override suspend fun estimateOriginFee(configuration: CrossChainTransferConfiguration, transfer: AssetTransfer): Fee { + return extrinsicService.estimateFeeV2(transfer.originChain) { crossChainTransfer(configuration, transfer, crossChainFee = Balance.ZERO) } } diff --git a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealChainAssetRepository.kt b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealChainAssetRepository.kt index b1f370c6b2..7a3b59d868 100644 --- a/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealChainAssetRepository.kt +++ b/feature-wallet-impl/src/main/java/io/novafoundation/nova/feature_wallet_impl/data/repository/RealChainAssetRepository.kt @@ -24,12 +24,6 @@ class RealChainAssetRepository( chainAssetDao.insertAsset(localAsset) } - override suspend fun getAssetSymbol(id: FullChainAssetId): String? { - val existingAsset = chainAssetDao.getAsset(id.assetId, id.chainId) - - return existingAsset?.symbol - } - override suspend fun getEnabledAssets(): List { return chainAssetDao.getEnabledAssets().map { mapChainAssetLocalToAsset(it, gson) } } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt index 98ee896ea1..949cf9889b 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeApi.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.runtime.di import com.google.gson.Gson import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase import io.novafoundation.nova.runtime.extrinsic.MortalityConstructor @@ -74,4 +75,6 @@ interface RuntimeApi { val extrinsicSplitter: ExtrinsicSplitter val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory + + val gasPriceProviderFactory: GasPriceProviderFactory } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt index e419eb7dfa..80fbec42aa 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeModule.kt @@ -10,6 +10,8 @@ import io.novafoundation.nova.core.storage.StorageCache import io.novafoundation.nova.core_db.dao.ChainDao import io.novafoundation.nova.core_db.dao.StorageDao import io.novafoundation.nova.runtime.ethereum.StorageSharedRequestsBuilderFactory +import io.novafoundation.nova.runtime.ethereum.gas.GasPriceProviderFactory +import io.novafoundation.nova.runtime.ethereum.gas.RealGasPriceProviderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicBuilderFactory import io.novafoundation.nova.runtime.extrinsic.ExtrinsicSerializers import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase @@ -180,4 +182,10 @@ class RuntimeModule { @Provides @ApplicationScope fun provideStorageSharedRequestBuilderFactory(chainRegistry: ChainRegistry) = StorageSharedRequestsBuilderFactory(chainRegistry) + + @Provides + @ApplicationScope + fun provideGasPriceProviderFactory( + chainRegistry: ChainRegistry + ): GasPriceProviderFactory = RealGasPriceProviderFactory(chainRegistry) } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/CompoundGasPriceProvider.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/CompoundGasPriceProvider.kt new file mode 100644 index 0000000000..0ea288128c --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/CompoundGasPriceProvider.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.runtime.ethereum.gas + +import io.novafoundation.nova.common.utils.orZero +import io.novafoundation.nova.common.utils.tryFindNonNull +import java.math.BigInteger + +class CompoundGasPriceProvider(vararg val delegates: GasPriceProvider) : GasPriceProvider { + + override suspend fun getGasPrice(): BigInteger { + return delegates.tryFindNonNull { delegate -> + runCatching { delegate.getGasPrice() }.getOrNull() + }.orZero() + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/GasPriceProvider.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/GasPriceProvider.kt new file mode 100644 index 0000000000..1b31c61904 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/GasPriceProvider.kt @@ -0,0 +1,8 @@ +package io.novafoundation.nova.runtime.ethereum.gas + +import java.math.BigInteger + +interface GasPriceProvider { + + suspend fun getGasPrice(): BigInteger +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/GasPriceProviderFactory.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/GasPriceProviderFactory.kt new file mode 100644 index 0000000000..487c5b8ec7 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/GasPriceProviderFactory.kt @@ -0,0 +1,37 @@ +package io.novafoundation.nova.runtime.ethereum.gas + +import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry +import io.novafoundation.nova.runtime.multiNetwork.awaitCallEthereumApiOrThrow +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import org.web3j.protocol.Web3j + +interface GasPriceProviderFactory { + + /** + * Creates gas provider for a [chainId] that is known to the app + */ + suspend fun createKnown(chainId: ChainId): GasPriceProvider + + /** + * Creates gas provider for arbitrary EVM chain given instance of [Web3j] + */ + suspend fun create(web3j: Web3j): GasPriceProvider +} + +class RealGasPriceProviderFactory( + private val chainRegistry: ChainRegistry +) : GasPriceProviderFactory { + + override suspend fun createKnown(chainId: ChainId): GasPriceProvider { + val api = chainRegistry.awaitCallEthereumApiOrThrow(chainId) + + return create(api) + } + + override suspend fun create(web3j: Web3j): GasPriceProvider { + return CompoundGasPriceProvider( + MaxPriorityFeeGasProvider(web3j), + LegacyGasPriceProvider(web3j) + ) + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/LegacyGasPriceProvider.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/LegacyGasPriceProvider.kt new file mode 100644 index 0000000000..1f3423b06e --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/LegacyGasPriceProvider.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.runtime.ethereum.gas + +import io.novafoundation.nova.runtime.ethereum.sendSuspend +import org.web3j.protocol.Web3j +import java.math.BigInteger + +class LegacyGasPriceProvider(private val api: Web3j) : GasPriceProvider { + + override suspend fun getGasPrice(): BigInteger { + return api.ethGasPrice().sendSuspend().gasPrice + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/MaxPriorityFeeGasProvider.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/MaxPriorityFeeGasProvider.kt new file mode 100644 index 0000000000..ff02344ad7 --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ethereum/gas/MaxPriorityFeeGasProvider.kt @@ -0,0 +1,22 @@ +package io.novafoundation.nova.runtime.ethereum.gas + +import io.novafoundation.nova.runtime.ethereum.sendSuspend +import org.web3j.protocol.Web3j +import org.web3j.protocol.core.DefaultBlockParameterName +import java.math.BigInteger + +class MaxPriorityFeeGasProvider(private val api: Web3j) : GasPriceProvider { + + override suspend fun getGasPrice(): BigInteger { + val baseFeePerGas = api.getLatestBaseFeePerGas() + val maxPriorityFee = api.ethMaxPriorityFeePerGas().sendSuspend().maxPriorityFeePerGas + + return baseFeePerGas + maxPriorityFee + } + + private suspend fun Web3j.getLatestBaseFeePerGas(): BigInteger { + val block = ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).sendSuspend() + + return block.block.baseFeePerGas + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt index c4038a97b2..1a95f53a47 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/ChainRegistry.kt @@ -141,6 +141,12 @@ suspend fun ChainRegistry.chainWithAssetOrNull(chainId: String, assetId: Int): C return ChainWithAsset(chain, chainAsset) } +suspend fun ChainRegistry.assetOrNull(fullChainAssetId: FullChainAssetId): Chain.Asset? { + val chain = getChainOrNull(fullChainAssetId.chainId) ?: return null + + return chain.assetsById[fullChainAssetId.assetId] +} + suspend fun ChainRegistry.chainWithAsset(chainId: String, assetId: Int): ChainWithAsset { val chain = chainsById.first().getValue(chainId)