From c3875675922963a40bd8a5d3c030d49d02f31e54 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 26 Jul 2023 17:08:11 +0500 Subject: [PATCH 01/31] fix azero coundown timer --- novawallet.xcodeproj/project.pbxproj | 4 +++ .../Substrate/Types/ConstantCodingPath.swift | 4 +++ .../Modules/Staking/Model/ConsesusType.swift | 3 +- .../StakingSharedState+Duration.swift | 31 ++++++++++++++++--- .../AuraEraOperationFactory.swift | 20 ++++++------ .../AuraSessionLengthOperationFactory.swift | 28 +++++++++++++++++ .../AuraStakingDurationFactory.swift | 12 +++---- .../Services/StakingServiceFactory.swift | 2 +- .../StakingMainPresenterFactory.swift | 4 +-- 9 files changed, 82 insertions(+), 26 deletions(-) create mode 100644 novawallet/Modules/Staking/Operations/StakingDuration/AuraSessionLengthOperationFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index a9834d424d..96777de65b 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 0C1FE4F62A52F137003769E7 /* AssetSearchBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1FE4F52A52F137003769E7 /* AssetSearchBuilderResult.swift */; }; 0C29B5382A4C68A500E35C6D /* AnimationUpdatibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */; }; 0C2AA829B5CB89B39E0FA95E /* CrowdloanContributionConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF01941105BCD02536538362 /* CrowdloanContributionConfirmProtocols.swift */; }; + 0C2F86802A7119D400593C01 /* AuraSessionLengthOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */; }; 0C40520C2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */; }; 0C463FC82A58126A003E71C9 /* UIView+MotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */; }; 0C463FD02A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */; }; @@ -3702,6 +3703,7 @@ 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationUpdatibleView.swift; sourceTree = ""; }; 0C2B3C9875FDA7EE8D168900 /* ParaStkYieldBoostSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupWireframe.swift; sourceTree = ""; }; 0C2B583DB30C6C818B0F952D /* ParaStkRebondWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondWireframe.swift; sourceTree = ""; }; + 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuraSessionLengthOperationFactory.swift; sourceTree = ""; }; 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmInteractor.swift; sourceTree = ""; }; 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBlurBackgroundView.swift; sourceTree = ""; }; 0C432D57ACFA53F42E574CBD /* TokensManageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewController.swift; sourceTree = ""; }; @@ -10642,6 +10644,7 @@ 846952A32852A1640083E0B4 /* StakingDuration.swift */, 846C372D26B199D10098F303 /* BabeStakingDurationFactory.swift */, 846952A52852A1E60083E0B4 /* AuraStakingDurationFactory.swift */, + 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */, ); path = StakingDuration; sourceTree = ""; @@ -18575,6 +18578,7 @@ 84466B4028B77B4500FA1E0D /* SignatureVerificationWrapper.swift in Sources */, 844DBC62274D1E29009F8351 /* SecretTypeTableViewCell.swift in Sources */, 847297A2260B3146009B86D0 /* ChangeTargetsSelectValidatorsStartWireframe.swift in Sources */, + 0C2F86802A7119D400593C01 /* AuraSessionLengthOperationFactory.swift in Sources */, 849D755B2756910A007726C3 /* RoundedView+Styles.swift in Sources */, 88AC5ADE2948CB540056DD40 /* TransactionHistoryViewModelFactory.swift in Sources */, 84038FEC26FFBA4D00C73F3F /* PriceLocalStorageSubscriber.swift in Sources */, diff --git a/novawallet/Common/Substrate/Types/ConstantCodingPath.swift b/novawallet/Common/Substrate/Types/ConstantCodingPath.swift index 394719895c..3e77f832d6 100644 --- a/novawallet/Common/Substrate/Types/ConstantCodingPath.swift +++ b/novawallet/Common/Substrate/Types/ConstantCodingPath.swift @@ -65,4 +65,8 @@ extension ConstantCodingPath { static var electionsSessionPeriod: ConstantCodingPath { ConstantCodingPath(moduleName: "Elections", constantName: "SessionPeriod") } + + static var azeroSessionPeriod: ConstantCodingPath { + ConstantCodingPath(moduleName: "CommitteeManagement", constantName: "SessionPeriod") + } } diff --git a/novawallet/Modules/Staking/Model/ConsesusType.swift b/novawallet/Modules/Staking/Model/ConsesusType.swift index 24105e78a2..e46ff508c3 100644 --- a/novawallet/Modules/Staking/Model/ConsesusType.swift +++ b/novawallet/Modules/Staking/Model/ConsesusType.swift @@ -2,5 +2,6 @@ import Foundation enum ConsensusType { case babe - case aura + case auraGeneral + case auraAzero } diff --git a/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState+Duration.swift b/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState+Duration.swift index 04164dae9d..e9346b986b 100644 --- a/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState+Duration.swift +++ b/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState+Duration.swift @@ -9,7 +9,7 @@ extension StakingSharedState { switch consensus { case .babe: return BabeEraOperationFactory(storageRequestFactory: storageRequestFactory) - case .aura: + case .auraGeneral: guard let blockTimeService = blockTimeService else { throw StakingSharedStateError.missingBlockTimeService } @@ -17,7 +17,19 @@ extension StakingSharedState { return AuraEraOperationFactory( storageRequestFactory: storageRequestFactory, blockTimeService: blockTimeService, - blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain) + blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain), + sessionPeriodOperationFactory: PathStakingSessionPeriodOperationFactory(path: .electionsSessionPeriod) + ) + case .auraAzero: + guard let blockTimeService = blockTimeService else { + throw StakingSharedStateError.missingBlockTimeService + } + + return AuraEraOperationFactory( + storageRequestFactory: storageRequestFactory, + blockTimeService: blockTimeService, + blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain), + sessionPeriodOperationFactory: PathStakingSessionPeriodOperationFactory(path: .azeroSessionPeriod) ) } } @@ -28,14 +40,25 @@ extension StakingSharedState { switch consensus { case .babe: return BabeStakingDurationFactory() - case .aura: + case .auraGeneral: + guard let blockTimeService = blockTimeService else { + throw StakingSharedStateError.missingBlockTimeService + } + + return AuraStakingDurationFactory( + blockTimeService: blockTimeService, + blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain), + sessionPeriodOperationFactory: PathStakingSessionPeriodOperationFactory(path: .electionsSessionPeriod) + ) + case .auraAzero: guard let blockTimeService = blockTimeService else { throw StakingSharedStateError.missingBlockTimeService } return AuraStakingDurationFactory( blockTimeService: blockTimeService, - blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain) + blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain), + sessionPeriodOperationFactory: PathStakingSessionPeriodOperationFactory(path: .azeroSessionPeriod) ) } } diff --git a/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift b/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift index c4cfac2073..5feda9a539 100644 --- a/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift +++ b/novawallet/Modules/Staking/Operations/EraCountdownOperationFactory/AuraEraOperationFactory.swift @@ -6,18 +6,18 @@ final class AuraEraOperationFactory: EraCountdownOperationFactoryProtocol { let storageRequestFactory: StorageRequestFactoryProtocol let blockTimeService: BlockTimeEstimationServiceProtocol let blockTimeOperationFactory: BlockTimeOperationFactoryProtocol - let defaultSessionPeriod: SessionIndex + let sessionPeriodOperationFactory: StakingSessionPeriodOperationFactoryProtocol init( storageRequestFactory: StorageRequestFactoryProtocol, blockTimeService: BlockTimeEstimationServiceProtocol, blockTimeOperationFactory: BlockTimeOperationFactoryProtocol, - defaultSessionPeriod: SessionIndex = 50 + sessionPeriodOperationFactory: StakingSessionPeriodOperationFactoryProtocol ) { self.storageRequestFactory = storageRequestFactory self.blockTimeService = blockTimeService self.blockTimeOperationFactory = blockTimeOperationFactory - self.defaultSessionPeriod = defaultSessionPeriod + self.sessionPeriodOperationFactory = sessionPeriodOperationFactory } // swiftlint:disable function_body_length @@ -33,10 +33,8 @@ final class AuraEraOperationFactory: EraCountdownOperationFactoryProtocol { codingFactoryOperation: codingFactoryOperation ) - let sessionLengthWrapper: CompoundOperationWrapper = createFetchConstantWrapper( - for: .electionsSessionPeriod, - codingFactoryOperation: codingFactoryOperation, - fallbackValue: defaultSessionPeriod + let sessionLengthOperation: BaseOperation = sessionPeriodOperationFactory.createOperation( + dependingOn: codingFactoryOperation ) let blockTimeWrapper = blockTimeOperationFactory.createBlockTimeOperation( @@ -82,8 +80,10 @@ final class AuraEraOperationFactory: EraCountdownOperationFactoryProtocol { engine: connection ) - let dependencies = eraLengthWrapper.allOperations - + sessionLengthWrapper.allOperations + let singleOperations: [Operation] = [sessionLengthOperation] + + let dependencies = singleOperations + + eraLengthWrapper.allOperations + blockTimeWrapper.allOperations + sessionIndexWrapper.allOperations + blockNumberWrapper.allOperations @@ -99,7 +99,7 @@ final class AuraEraOperationFactory: EraCountdownOperationFactoryProtocol { let currentEra = try? currentEraWrapper.targetOperation.extractNoCancellableResultData() .first?.value?.value, let eraLength = try? eraLengthWrapper.targetOperation.extractNoCancellableResultData(), - let sessionLength = try? sessionLengthWrapper.targetOperation.extractNoCancellableResultData(), + let sessionLength = try? sessionLengthOperation.extractNoCancellableResultData(), let blockTime = try? blockTimeWrapper.targetOperation.extractNoCancellableResultData(), let currentSessionIndex = try? sessionIndexWrapper.targetOperation .extractNoCancellableResultData().first?.value?.value, diff --git a/novawallet/Modules/Staking/Operations/StakingDuration/AuraSessionLengthOperationFactory.swift b/novawallet/Modules/Staking/Operations/StakingDuration/AuraSessionLengthOperationFactory.swift new file mode 100644 index 0000000000..3c19a48437 --- /dev/null +++ b/novawallet/Modules/Staking/Operations/StakingDuration/AuraSessionLengthOperationFactory.swift @@ -0,0 +1,28 @@ +import Foundation +import RobinHood + +protocol StakingSessionPeriodOperationFactoryProtocol { + func createOperation( + dependingOn codingFactoryOperation: BaseOperation + ) -> BaseOperation +} + +final class PathStakingSessionPeriodOperationFactory: StakingSessionPeriodOperationFactoryProtocol { + let defaultSessionPeriod: SessionIndex + let path: ConstantCodingPath + + init(path: ConstantCodingPath, defaultSessionPeriod: SessionIndex = 50) { + self.path = path + self.defaultSessionPeriod = defaultSessionPeriod + } + + func createOperation( + dependingOn codingFactoryOperation: BaseOperation + ) -> BaseOperation { + PrimitiveConstantOperation.operation( + for: path, + dependingOn: codingFactoryOperation, + fallbackValue: defaultSessionPeriod + ) + } +} diff --git a/novawallet/Modules/Staking/Operations/StakingDuration/AuraStakingDurationFactory.swift b/novawallet/Modules/Staking/Operations/StakingDuration/AuraStakingDurationFactory.swift index 45da8e025c..5a2d6e0f23 100644 --- a/novawallet/Modules/Staking/Operations/StakingDuration/AuraStakingDurationFactory.swift +++ b/novawallet/Modules/Staking/Operations/StakingDuration/AuraStakingDurationFactory.swift @@ -4,16 +4,16 @@ import RobinHood final class AuraStakingDurationFactory: StakingDurationOperationFactoryProtocol { let blockTimeService: BlockTimeEstimationServiceProtocol let blockTimeOperationFactory: BlockTimeOperationFactoryProtocol - let defaultSessionPeriod: SessionIndex + let sessionPeriodOperationFactory: StakingSessionPeriodOperationFactoryProtocol init( blockTimeService: BlockTimeEstimationServiceProtocol, blockTimeOperationFactory: BlockTimeOperationFactoryProtocol, - defaultSessionPeriod: SessionIndex = 50 + sessionPeriodOperationFactory: StakingSessionPeriodOperationFactoryProtocol ) { self.blockTimeService = blockTimeService self.blockTimeOperationFactory = blockTimeOperationFactory - self.defaultSessionPeriod = defaultSessionPeriod + self.sessionPeriodOperationFactory = sessionPeriodOperationFactory } func createDurationOperation( @@ -31,11 +31,7 @@ final class AuraStakingDurationFactory: StakingDurationOperationFactoryProtocol dependingOn: runtimeFactoryOperation ) - let sessionLengthOperation: BaseOperation = PrimitiveConstantOperation.operation( - for: .electionsSessionPeriod, - dependingOn: runtimeFactoryOperation, - fallbackValue: defaultSessionPeriod - ) + let sessionLengthOperation = sessionPeriodOperationFactory.createOperation(dependingOn: runtimeFactoryOperation) let blockTimeWrapper = blockTimeOperationFactory.createBlockTimeOperation( from: runtimeService, diff --git a/novawallet/Modules/Staking/Services/StakingServiceFactory.swift b/novawallet/Modules/Staking/Services/StakingServiceFactory.swift index 5d1f2b507c..4baebda4c0 100644 --- a/novawallet/Modules/Staking/Services/StakingServiceFactory.swift +++ b/novawallet/Modules/Staking/Services/StakingServiceFactory.swift @@ -114,7 +114,7 @@ final class StakingServiceFactory: StakingServiceFactoryProtocol { switch consensus { case .babe: return nil - case .aura: + case .auraGeneral, .auraAzero: guard let runtimeService = chainRegisty.getRuntimeProvider(for: chainId) else { throw ChainRegistryError.runtimeMetadaUnavailable } diff --git a/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift b/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift index 1000ae27b7..c0ee9d69ff 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift @@ -26,11 +26,11 @@ extension StakingMainPresenterFactory: StakingMainPresenterFactoryProtocol { case .relaychain: return createRelaychainPresenter(for: stakingOption, view: view, consensus: .babe) case .auraRelaychain: - return createRelaychainPresenter(for: stakingOption, view: view, consensus: .aura) + return createRelaychainPresenter(for: stakingOption, view: view, consensus: .auraGeneral) case .parachain, .turing: return createParachainPresenter(for: stakingOption, view: view) case .azero: - return createRelaychainPresenter(for: stakingOption, view: view, consensus: .aura) + return createRelaychainPresenter(for: stakingOption, view: view, consensus: .auraAzero) case .unsupported: return nil } From cb35135eadfd742117136098c954b3957cbdeb13 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 26 Jul 2023 20:50:12 +0500 Subject: [PATCH 02/31] add validation for stash on rebag alert tap --- .../StakingRelaychainPresenter.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift index 073c41b0f2..23c9f96ea2 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift @@ -330,7 +330,23 @@ extension StakingRelaychainPresenter: StakingMainChildPresenterProtocol { } func performRebag() { - wireframe.showRebagConfirm(from: view) + let locale = view?.localizationManager?.selectedLocale ?? Locale.current + + let stashItem: StashItem? = stateMachine.viewState { (state: BaseStashNextState) in + state.stashItem + } + + let stashAccount = stashItem.flatMap { accountForAddress($0.stash) } + + DataValidationRunner(validators: [ + dataValidatingFactory.has( + stash: stashAccount?.chainAccount, + for: stashItem?.stash ?? "", + locale: locale + ) + ]).runValidation { [weak self] in + self?.wireframe.showRebagConfirm(from: self?.view) + } } // swiftlint:disable:next cyclomatic_complexity From 01c3fcf19320a8805731cc406cfe05026bc4ba98 Mon Sep 17 00:00:00 2001 From: Stepan Lavrentev Date: Thu, 27 Jul 2023 09:40:29 +0300 Subject: [PATCH 03/31] change trigger for pull-request runner --- .github/workflows/pull_request.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 69d31dc8bb..7f1f41e9c0 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,10 +1,7 @@ name: Build and Test on PR on: - pull_request_target: - types: [labeled] - # pull_request: - # branches: [ develop ] + pull_request: jobs: build: From 73d07664fda2b72fb0b7f0f3fe921b4140dffac5 Mon Sep 17 00:00:00 2001 From: leohar Date: Thu, 10 Aug 2023 13:36:46 +0500 Subject: [PATCH 04/31] update NOTICE --- NOTICE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NOTICE b/NOTICE index 48585f4b07..e2c2b53cc2 100644 --- a/NOTICE +++ b/NOTICE @@ -6,6 +6,8 @@ This product includes software developed at Novasama Technologies PTE. LTD. Some parts of this product are derived from https://github.com/soramitsu/fearless-iOS, 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 + Notwithstanding the above, some works related to this product are licensed under the GPL 3.0 License which can be found in the respective folders available via the following links: 1) SoraKeystore: https://github.com/soramitsu/keystore-iOS; From 39483a57ab8096e7d58e0b27e4c68e1e48b24109 Mon Sep 17 00:00:00 2001 From: leohar Date: Thu, 10 Aug 2023 14:07:09 +0500 Subject: [PATCH 05/31] fix --- NOTICE | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NOTICE b/NOTICE index e2c2b53cc2..c67b65915a 100644 --- a/NOTICE +++ b/NOTICE @@ -6,12 +6,12 @@ This product includes software developed at Novasama Technologies PTE. LTD. Some parts of this product are derived from https://github.com/soramitsu/fearless-iOS, 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 - Notwithstanding the above, some works related to this product are licensed under the GPL 3.0 License which can be found in the respective folders available via the following links: 1) SoraKeystore: https://github.com/soramitsu/keystore-iOS; 2) SoraUI: https://github.com/ERussel/UIkit-iOS.git; 3) RobinHood: https://github.com/soramitsu/robinhood-ios; 4) CommonWallet: https://github.com/ERussel/Capital-iOS.git; -5) SoraFoundation: https://github.com/soramitsu/Foundation-iOS. \ No newline at end of file +5) SoraFoundation: https://github.com/soramitsu/Foundation-iOS. + +License Rights transferred from Novasama Technologies PTE. LTD to Novasama Technologies GmbH starting from 1st of April 2023 From b98ec8d3faf96097a50403cc6ce38721bf1ce3cb Mon Sep 17 00:00:00 2001 From: leohar Date: Fri, 11 Aug 2023 11:24:02 +0500 Subject: [PATCH 06/31] add copyright --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f95bdf085e..128fe9d786 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 iOS 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 From 8590ea941e62993728b77f6ffd5eac160556d75f Mon Sep 17 00:00:00 2001 From: leohar Date: Fri, 11 Aug 2023 12:04:34 +0500 Subject: [PATCH 07/31] update NOTICE --- NOTICE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE b/NOTICE index c67b65915a..5f33139386 100644 --- a/NOTICE +++ b/NOTICE @@ -1,6 +1,6 @@ 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-iOS, which belongs to Soramitsu K.K. and was mostly developed by our team of developers from May 1, 2020, to October 5, 2021. From 74c305403e0397488d04169fa37ff7ef933a2afd Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 12 Aug 2023 12:01:29 +0300 Subject: [PATCH 08/31] improve gas price calculation --- novawallet.xcodeproj/project.pbxproj | 36 +++++++++ .../Ethereum/EthereumBlockObject.swift | 1 - .../Ethereum/EthereumOperationFactory.swift | 7 ++ .../Ethereum/EthereumReducedBlockObject.swift | 6 ++ .../Evm/EvmTransactionService.swift | 27 +++---- .../Evm/EvmWebSocketOperationFactory.swift | 27 +++++++ .../EvmGasPriceProvider.swift | 12 +++ .../EvmGasPriceProviderFactory.swift | 20 +++++ .../EvmGasPriceWithFallbackProvider.swift | 79 +++++++++++++++++++ .../EvmLegacyGasPriceProvider.swift | 32 ++++++++ .../EvmMaxPriorityGasPriceProvider.swift | 37 +++++++++ .../TransferConfirmOnChainViewFactory.swift | 10 ++- ...ransferSetupPresenterFactory+OnChain.swift | 10 ++- .../EvmGasPriceIntegrationTests.swift | 60 ++++++++++++++ 14 files changed, 345 insertions(+), 19 deletions(-) create mode 100644 novawallet/Common/Network/Ethereum/EthereumReducedBlockObject.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProviderFactory.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceWithFallbackProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmLegacyGasPriceProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmMaxPriorityGasPriceProvider.swift create mode 100644 novawalletIntegrationTests/EvmGasPriceIntegrationTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 96777de65b..a341b33035 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -49,6 +49,13 @@ 0C29B5382A4C68A500E35C6D /* AnimationUpdatibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */; }; 0C2AA829B5CB89B39E0FA95E /* CrowdloanContributionConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF01941105BCD02536538362 /* CrowdloanContributionConfirmProtocols.swift */; }; 0C2F86802A7119D400593C01 /* AuraSessionLengthOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */; }; + 0C3205BB2A8679F0002EB914 /* EvmGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */; }; + 0C3205BE2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BD2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift */; }; + 0C3205C02A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */; }; + 0C3205C22A868236002EB914 /* EvmGasPriceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C12A868236002EB914 /* EvmGasPriceIntegrationTests.swift */; }; + 0C3205C42A877172002EB914 /* EthereumReducedBlockObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C32A877172002EB914 /* EthereumReducedBlockObject.swift */; }; + 0C3205C62A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C52A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift */; }; + 0C3205C82A877E5A002EB914 /* EvmGasPriceProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C72A877E5A002EB914 /* EvmGasPriceProviderFactory.swift */; }; 0C40520C2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */; }; 0C463FC82A58126A003E71C9 /* UIView+MotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */; }; 0C463FD02A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */; }; @@ -3704,6 +3711,13 @@ 0C2B3C9875FDA7EE8D168900 /* ParaStkYieldBoostSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupWireframe.swift; sourceTree = ""; }; 0C2B583DB30C6C818B0F952D /* ParaStkRebondWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondWireframe.swift; sourceTree = ""; }; 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuraSessionLengthOperationFactory.swift; sourceTree = ""; }; + 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceProvider.swift; sourceTree = ""; }; + 0C3205BD2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmLegacyGasPriceProvider.swift; sourceTree = ""; }; + 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmMaxPriorityGasPriceProvider.swift; sourceTree = ""; }; + 0C3205C12A868236002EB914 /* EvmGasPriceIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceIntegrationTests.swift; sourceTree = ""; }; + 0C3205C32A877172002EB914 /* EthereumReducedBlockObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumReducedBlockObject.swift; sourceTree = ""; }; + 0C3205C52A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceWithFallbackProvider.swift; sourceTree = ""; }; + 0C3205C72A877E5A002EB914 /* EvmGasPriceProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceProviderFactory.swift; sourceTree = ""; }; 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmInteractor.swift; sourceTree = ""; }; 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBlurBackgroundView.swift; sourceTree = ""; }; 0C432D57ACFA53F42E574CBD /* TokensManageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewController.swift; sourceTree = ""; }; @@ -7521,6 +7535,18 @@ path = Model; sourceTree = ""; }; + 0C3205BC2A867A46002EB914 /* GasPriceProvider */ = { + isa = PBXGroup; + children = ( + 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */, + 0C3205BD2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift */, + 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */, + 0C3205C52A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift */, + 0C3205C72A877E5A002EB914 /* EvmGasPriceProviderFactory.swift */, + ); + path = GasPriceProvider; + sourceTree = ""; + }; 0C463FCE2A592ACD003E71C9 /* Effects */ = { isa = PBXGroup; children = ( @@ -9545,6 +9571,7 @@ 8813702B29C13E7900829458 /* Web3NamesOperationFactoryTests.swift */, 8455F1992A1DEEAF003F072D /* SubqueryMultistakingTests.swift */, 844304662A28F7A500DE36DE /* MultistakingSyncTests.swift */, + 0C3205C12A868236002EB914 /* EvmGasPriceIntegrationTests.swift */, ); path = novawalletIntegrationTests; sourceTree = ""; @@ -12775,6 +12802,7 @@ 84A59157292AA64E00BCCF8F /* Evm */ = { isa = PBXGroup; children = ( + 0C3205BC2A867A46002EB914 /* GasPriceProvider */, 84A59159292AA83500BCCF8F /* EvmWebSocketOperationFactory.swift */, 84A5915B292B390800BCCF8F /* EvmTransactionBuilder.swift */, 84A5915D292B3C3D00BCCF8F /* EvmTransactionBuilder+Transfer.swift */, @@ -14542,6 +14570,7 @@ 84E8BA1429FFB38600FD9F40 /* EthereumResultParser.swift */, 84E8BA1629FFB38600FD9F40 /* EthereumOperationFactory.swift */, 84E8BA1929FFB38600FD9F40 /* EthereumTransaction.swift */, + 0C3205C32A877172002EB914 /* EthereumReducedBlockObject.swift */, ); path = Ethereum; sourceTree = ""; @@ -17681,6 +17710,7 @@ 84BFE8A228C2420A00140F1F /* AutocompounDelegateStakeTests.swift in Sources */, F49F49A326A5C45600A25931 /* BabeEraOperationFactoryTests.swift in Sources */, 84532D5F28E4210E00EF4ADC /* ConvictionVotesFetchTests.swift in Sources */, + 0C3205C22A868236002EB914 /* EvmGasPriceIntegrationTests.swift in Sources */, 84C3420F2831A67200156569 /* BlockTimeEstimationServiceTests.swift in Sources */, 84FACB6A25F5759C00F32ED4 /* AccountCreationHelper.swift in Sources */, 843E9B3827C8CAC1009C143A /* NftDownloadIntegrationTests.swift in Sources */, @@ -18024,6 +18054,7 @@ 8849AD6229C3532600F4F7FF /* String+Split.swift in Sources */, 8466781C27ED9644007935D3 /* AsyncWarningConditionViolation.swift in Sources */, 849067CA299BCCB700B2983E /* GovernanceRevokeDelegationConfirmPresenter+Update.swift in Sources */, + 0C3205BE2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift in Sources */, 84452F9325D5EE7300F47EC5 /* DataOperationFactory.swift in Sources */, 841E5538282CF3F400C8438F /* StakingMainViewModelFactory.swift in Sources */, 8442003C28EAA2E400C49C4A /* ReferendumsWireframe.swift in Sources */, @@ -18651,6 +18682,7 @@ 882808C829009CA500AE8089 /* DotsView.swift in Sources */, 84C355BD29892885005072CF /* SubqueryBaseOperationFactory.swift in Sources */, 8860F3E8289D7CF400C0BF86 /* Atomic.swift in Sources */, + 0C3205C02A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift in Sources */, 8499FECF27BFA25100712589 /* NftType.swift in Sources */, 84EF8D40288FDA7700265346 /* WalletListLocalStorageSubscriptionHandler.swift in Sources */, AEA2C1B62681E9B20069492E /* ValidatorSearchViewFactory.swift in Sources */, @@ -18901,6 +18933,7 @@ 849ABE7226280F3800011A2A /* ControllersReducer.swift in Sources */, 84F30EE425FFAC0800039D09 /* StreamableProviderOptions+Substrate.swift in Sources */, 845B811228F429BB0040CE84 /* SupportPallet.swift in Sources */, + 0C3205C62A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift in Sources */, 8470D6D4253E35F0009E9A5D /* StorageUpdate.swift in Sources */, 8460E715284AC0AA002896E9 /* ParaStkBaseUnstakeInteractor.swift in Sources */, 84C2F27D25E297350050A4AD /* CalculatedReward.swift in Sources */, @@ -19672,6 +19705,7 @@ 5DDD2206DF795CF205610455 /* AccountExportPasswordPresenter.swift in Sources */, 800FCAF66DC8A24020D16A9C /* AccountExportPasswordInteractor.swift in Sources */, 841E553A282D23AE00C8438F /* StakingRelaychainWireframe.swift in Sources */, + 0C3205BB2A8679F0002EB914 /* EvmGasPriceProvider.swift in Sources */, 8846F71E29D5675E00B8B776 /* Web3NameRecipientListViewModel.swift in Sources */, 84FBDBDF28C8811D00CC1037 /* ParaStkYieldBoostProviderFactory.swift in Sources */, 7C93FA82996A426E7B8CA06E /* AccountExportPasswordViewController.swift in Sources */, @@ -20238,6 +20272,7 @@ 8425D0EE28FE9BF1003B782A /* ReferendumVoteAction.swift in Sources */, 84E8BA1A29FFB38600FD9F40 /* EthereumBlockObject.swift in Sources */, 6857DAF09C8D7D5F9C5A5000 /* CrowdloanYourContributionsViewController.swift in Sources */, + 0C3205C82A877E5A002EB914 /* EvmGasPriceProviderFactory.swift in Sources */, 487A912B697604FE3367FAEC /* CrowdloanYourContributionsViewLayout.swift in Sources */, 3F7F10D0E1BDE09CBE64BD2D /* CrowdloanYourContributionsViewFactory.swift in Sources */, E37BB7A393FFEFC350B4EA3D /* AdvancedWalletProtocols.swift in Sources */, @@ -20965,6 +21000,7 @@ 4C4142B4CB2DBCA1F06DC046 /* GovernanceDelegateSetupViewLayout.swift in Sources */, 6789ED94EFF73E5B47956462 /* GovernanceDelegateSetupViewFactory.swift in Sources */, 4F406ED92AAE2C77E358F49C /* GovernanceDelegateConfirmProtocols.swift in Sources */, + 0C3205C42A877172002EB914 /* EthereumReducedBlockObject.swift in Sources */, 1C9EA26D4E4BA6BAE147B374 /* GovernanceDelegateConfirmWireframe.swift in Sources */, 6FE660C98518CEB28AD9CDA3 /* GovernanceDelegateConfirmPresenter.swift in Sources */, 9358E048B1AA0F71F519101E /* GovernanceDelegateConfirmInteractor.swift in Sources */, diff --git a/novawallet/Common/Network/Ethereum/EthereumBlockObject.swift b/novawallet/Common/Network/Ethereum/EthereumBlockObject.swift index 283f8e767b..8ed82e4a1e 100644 --- a/novawallet/Common/Network/Ethereum/EthereumBlockObject.swift +++ b/novawallet/Common/Network/Ethereum/EthereumBlockObject.swift @@ -4,7 +4,6 @@ import Core struct EthereumBlockObject: Codable { struct Transaction: Codable { - // swiftlint:disable:next nesting enum CodingKeys: String, CodingKey { case hash case sender = "from" diff --git a/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift b/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift index a7cef3c7fd..b10b0e8fa6 100644 --- a/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift +++ b/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift @@ -20,6 +20,12 @@ protocol EthereumOperationFactoryProtocol { func createTransactionReceiptOperation(for transactionHash: String) -> BaseOperation func createBlockOperation(for blockNumber: BigUInt) -> RobinHood.BaseOperation + + func createReducedBlockOperation( + for blockOption: EthereumBlock + ) -> RobinHood.BaseOperation + + func createMaxPriorityPerGasOperation() -> BaseOperation> } enum EthereumBlock: String { @@ -35,4 +41,5 @@ enum EthereumMethod: String { case sendRawTransaction = "eth_sendRawTransaction" case transactionReceipt = "eth_getTransactionReceipt" case blockByNumber = "eth_getBlockByNumber" + case maxPriorityFeePerGas = "eth_maxPriorityFeePerGas" } diff --git a/novawallet/Common/Network/Ethereum/EthereumReducedBlockObject.swift b/novawallet/Common/Network/Ethereum/EthereumReducedBlockObject.swift new file mode 100644 index 0000000000..e0ed31a59d --- /dev/null +++ b/novawallet/Common/Network/Ethereum/EthereumReducedBlockObject.swift @@ -0,0 +1,6 @@ +import Foundation +import BigInt + +struct EthereumReducedBlockObject: Decodable { + @OptionHexCodable var baseFeePerGas: BigUInt? +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift index 6376b039e7..44e8353200 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift @@ -34,17 +34,20 @@ enum EvmTransactionServiceError: Error { final class EvmTransactionService { let accountId: AccountId let operationFactory: EthereumOperationFactoryProtocol + let gasPriceProvider: EvmGasPriceProviderProtocol let chain: ChainModel let operationQueue: OperationQueue init( accountId: AccountId, operationFactory: EthereumOperationFactoryProtocol, + gasPriceProvider: EvmGasPriceProviderProtocol, chain: ChainModel, operationQueue: OperationQueue ) { self.accountId = accountId self.operationFactory = operationFactory + self.gasPriceProvider = gasPriceProvider self.chain = chain self.operationQueue = operationQueue } @@ -92,21 +95,17 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { fallbackGasLimit: fallbackGasLimit ) - let gasPriceOperation = operationFactory.createGasPriceOperation() + let gasPriceWrapper = gasPriceProvider.getGasPriceWrapper() let mapOperation = ClosureOperation { let gasLimit = try gasEstimationWrapper.targetOperation.extractNoCancellableResultData() - let gasPriceString = try gasPriceOperation.extractNoCancellableResultData() - - guard let gasPrice = BigUInt.fromHexString(gasPriceString) else { - throw EvmTransactionServiceError.invalidGasPrice(gasPriceString) - } + let gasPrice = try gasPriceWrapper.targetOperation.extractNoCancellableResultData() return gasLimit * gasPrice } mapOperation.addDependency(gasEstimationWrapper.targetOperation) - mapOperation.addDependency(gasPriceOperation) + mapOperation.addDependency(gasPriceWrapper.targetOperation) mapOperation.completionBlock = { queue.async { @@ -119,7 +118,7 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { } } - let operations = gasEstimationWrapper.allOperations + [gasPriceOperation, mapOperation] + let operations = gasEstimationWrapper.allOperations + gasPriceWrapper.allOperations + [mapOperation] operationQueue.addOperations(operations, waitUntilFinished: false) } catch { @@ -141,22 +140,18 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { let builder = try closure(initBuilder) let gasEstimationOperation = operationFactory.createGasLimitOperation(for: builder.buildTransaction()) - let gasPriceOperation = operationFactory.createGasPriceOperation() + let gasPriceWrapper = gasPriceProvider.getGasPriceWrapper() let nonceOperation = operationFactory.createTransactionsCountOperation(for: accountId, block: .pending) let sendOperation = operationFactory.createSendTransactionOperation { let gasLimitString = try gasEstimationOperation.extractNoCancellableResultData() - let gasPriceString = try gasPriceOperation.extractNoCancellableResultData() + let gasPrice = try gasPriceWrapper.targetOperation.extractNoCancellableResultData() let nonceString = try nonceOperation.extractNoCancellableResultData() guard let gasLimit = BigUInt.fromHexString(gasLimitString) else { throw EvmTransactionServiceError.invalidGasLimit(gasLimitString) } - guard let gasPrice = BigUInt.fromHexString(gasPriceString) else { - throw EvmTransactionServiceError.invalidGasPrice(gasLimitString) - } - guard let nonce = BigUInt.fromHexString(nonceString) else { throw EvmTransactionServiceError.invalidNonce(nonceString) } @@ -172,7 +167,7 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { } sendOperation.addDependency(gasEstimationOperation) - sendOperation.addDependency(gasPriceOperation) + sendOperation.addDependency(gasPriceWrapper.targetOperation) sendOperation.addDependency(nonceOperation) sendOperation.completionBlock = { @@ -186,7 +181,7 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { } } - let operations = [gasEstimationOperation, gasPriceOperation, nonceOperation, sendOperation] + let operations = [gasEstimationOperation] + gasPriceWrapper.allOperations + [nonceOperation, sendOperation] operationQueue.addOperations(operations, waitUntilFinished: false) } catch { diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift index 58cfdbc58a..b276cb4aee 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift @@ -94,4 +94,31 @@ extension EvmWebSocketOperationFactory: EthereumOperationFactoryProtocol { return operation } + + func createReducedBlockOperation( + for blockOption: EthereumBlock + ) -> RobinHood.BaseOperation { + let params = JSON.arrayValue( + [ + JSON.stringValue(blockOption.rawValue), // block number + JSON.boolValue(false) // should return full transactions + ] + ) + + return JSONRPCOperation( + engine: connection, + method: EthereumMethod.blockByNumber.rawValue, + parameters: params, + timeout: timeout + ) + } + + func createMaxPriorityPerGasOperation() -> BaseOperation> { + JSONRPCListOperation>( + engine: connection, + method: EthereumMethod.maxPriorityFeePerGas.rawValue, + parameters: [], + timeout: timeout + ) + } } diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProvider.swift new file mode 100644 index 0000000000..10a508d809 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProvider.swift @@ -0,0 +1,12 @@ +import Foundation +import RobinHood +import BigInt +import SubstrateSdk + +protocol EvmGasPriceProviderProtocol { + func getGasPriceWrapper() -> CompoundOperationWrapper +} + +enum EvmGasPriceProviderError: Error { + case unsupported +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProviderFactory.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProviderFactory.swift new file mode 100644 index 0000000000..91edfe851c --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProviderFactory.swift @@ -0,0 +1,20 @@ +import Foundation +import SubstrateSdk + +enum EvmGasPriceProviderFactory { + static func createMaxPriorityWithLegacyFallback( + operationFactory: EthereumOperationFactoryProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) -> EvmGasPriceProviderProtocol { + let maxPriorityProvider = EvmMaxPriorityGasPriceProvider(operationFactory: operationFactory) + let legacyProvider = EvmLegacyGasPriceProvider(operationFactory: operationFactory) + + return EvmGasPriceWithFallbackProvider( + mainPriceProvider: maxPriorityProvider, + fallbackPriceProvider: legacyProvider, + operationQueue: operationQueue, + logger: logger + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceWithFallbackProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceWithFallbackProvider.swift new file mode 100644 index 0000000000..d719e141e2 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceWithFallbackProvider.swift @@ -0,0 +1,79 @@ +import Foundation +import SubstrateSdk +import RobinHood +import BigInt + +final class EvmGasPriceWithFallbackProvider { + let mainPriceProvider: EvmGasPriceProviderProtocol + let fallbackPriceProvider: EvmGasPriceProviderProtocol + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + mainPriceProvider: EvmGasPriceProviderProtocol, + fallbackPriceProvider: EvmGasPriceProviderProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.mainPriceProvider = mainPriceProvider + self.fallbackPriceProvider = fallbackPriceProvider + self.operationQueue = operationQueue + self.logger = logger + } + + private func createWrapper( + for mainProvider: EvmGasPriceProviderProtocol, + fallbackProvider: EvmGasPriceProviderProtocol, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> CompoundOperationWrapper { + let mainGasPriceWrapper = mainProvider.getGasPriceWrapper() + + let fallbackOperation: BaseOperation<[BigUInt]> = OperationCombiningService( + operationManager: operationManager + ) { + do { + let price = try mainGasPriceWrapper.targetOperation.extractNoCancellableResultData() + + let resultWrapper = CompoundOperationWrapper.createWithResult(price) + return [resultWrapper] + } catch { + logger.warning("Using fallback for gas price due to error: \(error)") + + let resultWrapper = fallbackProvider.getGasPriceWrapper() + return [resultWrapper] + } + } + .longrunOperation() + + fallbackOperation.addDependency(mainGasPriceWrapper.targetOperation) + + let mapOperation = ClosureOperation { + let optPrice = try fallbackOperation.extractNoCancellableResultData().first + + guard let price = optPrice else { + throw CommonError.dataCorruption + } + + return price + } + + mapOperation.addDependency(fallbackOperation) + + return CompoundOperationWrapper( + targetOperation: mapOperation, + dependencies: mainGasPriceWrapper.allOperations + [fallbackOperation] + ) + } +} + +extension EvmGasPriceWithFallbackProvider: EvmGasPriceProviderProtocol { + func getGasPriceWrapper() -> CompoundOperationWrapper { + createWrapper( + for: mainPriceProvider, + fallbackProvider: fallbackPriceProvider, + operationManager: OperationManager(operationQueue: operationQueue), + logger: logger + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmLegacyGasPriceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmLegacyGasPriceProvider.swift new file mode 100644 index 0000000000..9fca2f9ce8 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmLegacyGasPriceProvider.swift @@ -0,0 +1,32 @@ +import Foundation +import RobinHood +import SubstrateSdk +import BigInt + +final class EvmLegacyGasPriceProvider { + let operationFactory: EthereumOperationFactoryProtocol + + init(operationFactory: EthereumOperationFactoryProtocol) { + self.operationFactory = operationFactory + } +} + +extension EvmLegacyGasPriceProvider: EvmGasPriceProviderProtocol { + func getGasPriceWrapper() -> CompoundOperationWrapper { + let fetchOperation = operationFactory.createGasPriceOperation() + + let mapOperation = ClosureOperation { + let gasPriceString = try fetchOperation.extractNoCancellableResultData() + + guard let gasPrice = BigUInt.fromHexString(gasPriceString) else { + throw CommonError.dataCorruption + } + + return gasPrice + } + + mapOperation.addDependency(fetchOperation) + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: [fetchOperation]) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmMaxPriorityGasPriceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmMaxPriorityGasPriceProvider.swift new file mode 100644 index 0000000000..17b37e624d --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmMaxPriorityGasPriceProvider.swift @@ -0,0 +1,37 @@ +import Foundation +import RobinHood +import SubstrateSdk +import BigInt + +final class EvmMaxPriorityGasPriceProvider { + let operationFactory: EthereumOperationFactoryProtocol + + init(operationFactory: EthereumOperationFactoryProtocol) { + self.operationFactory = operationFactory + } +} + +extension EvmMaxPriorityGasPriceProvider: EvmGasPriceProviderProtocol { + func getGasPriceWrapper() -> CompoundOperationWrapper { + let lastBlockOperation = operationFactory.createReducedBlockOperation(for: .latest) + let maxPriorityOperation = operationFactory.createMaxPriorityPerGasOperation() + + let mapOperation = ClosureOperation { + guard let baseFee = try lastBlockOperation.extractNoCancellableResultData().baseFeePerGas else { + throw EvmGasPriceProviderError.unsupported + } + + let maxPriorityFee = try maxPriorityOperation.extractNoCancellableResultData().wrappedValue + + return baseFee + maxPriorityFee + } + + mapOperation.addDependency(lastBlockOperation) + mapOperation.addDependency(maxPriorityOperation) + + return CompoundOperationWrapper( + targetOperation: mapOperation, + dependencies: [lastBlockOperation, maxPriorityOperation] + ) + } +} diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index 4e560bf6cb..b17be7ccb5 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -115,9 +115,17 @@ struct TransferConfirmOnChainViewFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue + let operationFactory = EvmWebSocketOperationFactory(connection: connection) + let gasPriceProvider = EvmGasPriceProviderFactory.createMaxPriorityWithLegacyFallback( + operationFactory: operationFactory, + operationQueue: operationQueue, + logger: Logger.shared + ) + let extrinsicService = EvmTransactionService( accountId: account.accountId, - operationFactory: EvmWebSocketOperationFactory(connection: connection), + operationFactory: operationFactory, + gasPriceProvider: gasPriceProvider, chain: chain, operationQueue: operationQueue ) diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift index 314310938a..ef1c4524d8 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift @@ -148,9 +148,17 @@ extension TransferSetupPresenterFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue + let operationFactory = EvmWebSocketOperationFactory(connection: connection) + let gasPriceProvider = EvmGasPriceProviderFactory.createMaxPriorityWithLegacyFallback( + operationFactory: operationFactory, + operationQueue: operationQueue, + logger: Logger.shared + ) + let extrinsicService = EvmTransactionService( accountId: selectedAccount.accountId, - operationFactory: EvmWebSocketOperationFactory(connection: connection), + operationFactory: operationFactory, + gasPriceProvider: gasPriceProvider, chain: chain, operationQueue: operationQueue ) diff --git a/novawalletIntegrationTests/EvmGasPriceIntegrationTests.swift b/novawalletIntegrationTests/EvmGasPriceIntegrationTests.swift new file mode 100644 index 0000000000..af258febef --- /dev/null +++ b/novawalletIntegrationTests/EvmGasPriceIntegrationTests.swift @@ -0,0 +1,60 @@ +import XCTest +@testable import novawallet +import RobinHood +import BigInt +import SubstrateSdk + +final class EvmGasPriceIntegrationTests: XCTestCase { + + func testEvmGasProvidersOnMoonbeam() { + performTest(for: KnowChainId.moonbeam) + } + + func testEvmGasProvidersOnMoonriver() { + performTest(for: KnowChainId.moonriver) + } + + func testEvmGasProvidersOnEthereum() { + performTest(for: KnowChainId.ethereum) + } + + private func performTest(for chainId: ChainModel.Id) { + let storageFacade = SubstrateStorageTestFacade() + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + guard let connection = chainRegistry.getConnection(for: chainId) else { + XCTFail("Unsupported network") + return + } + + let operationFactory = EvmWebSocketOperationFactory(connection: connection) + + let providers: [EvmGasPriceProviderProtocol] = [ + EvmLegacyGasPriceProvider(operationFactory: operationFactory), + EvmMaxPriorityGasPriceProvider(operationFactory: operationFactory) + ] + + performGasFeeValuesComparison(for: chainId, providers: providers) + } + + private func performGasFeeValuesComparison( + for chainId: ChainModel.Id, + providers: [EvmGasPriceProviderProtocol] + ) { + let wrappers = providers.map { $0.getGasPriceWrapper() } + + let allOperations = wrappers.flatMap { $0.allOperations } + + OperationQueue().addOperations(allOperations, waitUntilFinished: true) + + for (index, wrapper) in wrappers.enumerated() { + do { + let price = try wrapper.targetOperation.extractNoCancellableResultData() + + Logger.shared.info("Price \(index + 1): \(String(price))") + } catch { + XCTFail("Price provider error: \(error)") + } + } + } +} From 673492315670a760f7d0e53e0726272d35e91965 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 13 Aug 2023 22:46:31 +0300 Subject: [PATCH 09/31] refactoring ethereum interactor --- novawallet.xcodeproj/project.pbxproj | 58 ++- .../Ethereum/EthereumOperationFactory.swift | 6 +- .../Evm/EvmTransactionFeeProxy.swift | 8 +- .../Evm/EvmTransactionService.swift | 170 +++++--- .../Evm/EvmWebSocketOperationFactory.swift | 11 +- .../EvmConstantGasLimitProvider.swift | 17 + .../EvmDefaultGasLimitProvider.swift | 25 ++ .../EvmGasLimitProvider.swift | 8 + .../EvmGasLimitProviderFactory.swift | 22 + .../EvmGasLimitWithFallbackProvider.swift | 81 ++++ .../EvmConstantGasPriceProvider.swift | 17 + .../EvmGasPriceProvider.swift | 0 .../EvmGasPriceProviderFactory.swift | 0 .../EvmGasPriceWithFallbackProvider.swift | 0 .../EvmLegacyGasPriceProvider.swift | 8 +- .../EvmMaxPriorityGasPriceProvider.swift | 0 .../EvmConstantNonceProvider.swift | 20 + .../EvmDefaultNonceProvider.swift | 31 ++ .../Evm/NonceProviders/EvmNonceProvider.swift | 10 + .../DAppEthereumConfirmInteractor.swift | 404 +++++++----------- .../DAppOperationConfirmViewFactory.swift | 1 - .../TransferConfirmOnChainViewFactory.swift | 13 +- ...ransferSetupPresenterFactory+OnChain.swift | 16 +- 23 files changed, 585 insertions(+), 341 deletions(-) create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmConstantGasLimitProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmDefaultGasLimitProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProviderFactory.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitWithFallbackProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmConstantGasPriceProvider.swift rename novawallet/Common/Services/ExtrinsicService/Evm/{GasPriceProvider => GasPriceProviders}/EvmGasPriceProvider.swift (100%) rename novawallet/Common/Services/ExtrinsicService/Evm/{GasPriceProvider => GasPriceProviders}/EvmGasPriceProviderFactory.swift (100%) rename novawallet/Common/Services/ExtrinsicService/Evm/{GasPriceProvider => GasPriceProviders}/EvmGasPriceWithFallbackProvider.swift (100%) rename novawallet/Common/Services/ExtrinsicService/Evm/{GasPriceProvider => GasPriceProviders}/EvmLegacyGasPriceProvider.swift (74%) rename novawallet/Common/Services/ExtrinsicService/Evm/{GasPriceProvider => GasPriceProviders}/EvmMaxPriorityGasPriceProvider.swift (100%) create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmConstantNonceProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmDefaultNonceProvider.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmNonceProvider.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index a341b33035..f2479b88fb 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -56,6 +56,15 @@ 0C3205C42A877172002EB914 /* EthereumReducedBlockObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C32A877172002EB914 /* EthereumReducedBlockObject.swift */; }; 0C3205C62A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C52A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift */; }; 0C3205C82A877E5A002EB914 /* EvmGasPriceProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C72A877E5A002EB914 /* EvmGasPriceProviderFactory.swift */; }; + 0C3205CA2A895489002EB914 /* EvmGasLimitProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205C92A895489002EB914 /* EvmGasLimitProvider.swift */; }; + 0C3205CC2A895827002EB914 /* EvmNonceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205CB2A895827002EB914 /* EvmNonceProvider.swift */; }; + 0C3205D02A895E5B002EB914 /* EvmConstantGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205CF2A895E5B002EB914 /* EvmConstantGasPriceProvider.swift */; }; + 0C3205D22A895E85002EB914 /* EvmDefaultGasLimitProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205D12A895E85002EB914 /* EvmDefaultGasLimitProvider.swift */; }; + 0C3205D42A895EDA002EB914 /* EvmConstantGasLimitProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205D32A895EDA002EB914 /* EvmConstantGasLimitProvider.swift */; }; + 0C3205D62A895F0F002EB914 /* EvmGasLimitWithFallbackProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205D52A895F0F002EB914 /* EvmGasLimitWithFallbackProvider.swift */; }; + 0C3205D82A896009002EB914 /* EvmDefaultNonceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205D72A896009002EB914 /* EvmDefaultNonceProvider.swift */; }; + 0C3205DA2A896029002EB914 /* EvmConstantNonceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205D92A896029002EB914 /* EvmConstantNonceProvider.swift */; }; + 0C3205DC2A89677B002EB914 /* EvmGasLimitProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205DB2A89677B002EB914 /* EvmGasLimitProviderFactory.swift */; }; 0C40520C2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */; }; 0C463FC82A58126A003E71C9 /* UIView+MotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */; }; 0C463FD02A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */; }; @@ -3718,6 +3727,15 @@ 0C3205C32A877172002EB914 /* EthereumReducedBlockObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumReducedBlockObject.swift; sourceTree = ""; }; 0C3205C52A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceWithFallbackProvider.swift; sourceTree = ""; }; 0C3205C72A877E5A002EB914 /* EvmGasPriceProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceProviderFactory.swift; sourceTree = ""; }; + 0C3205C92A895489002EB914 /* EvmGasLimitProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasLimitProvider.swift; sourceTree = ""; }; + 0C3205CB2A895827002EB914 /* EvmNonceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmNonceProvider.swift; sourceTree = ""; }; + 0C3205CF2A895E5B002EB914 /* EvmConstantGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmConstantGasPriceProvider.swift; sourceTree = ""; }; + 0C3205D12A895E85002EB914 /* EvmDefaultGasLimitProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmDefaultGasLimitProvider.swift; sourceTree = ""; }; + 0C3205D32A895EDA002EB914 /* EvmConstantGasLimitProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmConstantGasLimitProvider.swift; sourceTree = ""; }; + 0C3205D52A895F0F002EB914 /* EvmGasLimitWithFallbackProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasLimitWithFallbackProvider.swift; sourceTree = ""; }; + 0C3205D72A896009002EB914 /* EvmDefaultNonceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmDefaultNonceProvider.swift; sourceTree = ""; }; + 0C3205D92A896029002EB914 /* EvmConstantNonceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmConstantNonceProvider.swift; sourceTree = ""; }; + 0C3205DB2A89677B002EB914 /* EvmGasLimitProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasLimitProviderFactory.swift; sourceTree = ""; }; 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmInteractor.swift; sourceTree = ""; }; 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBlurBackgroundView.swift; sourceTree = ""; }; 0C432D57ACFA53F42E574CBD /* TokensManageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewController.swift; sourceTree = ""; }; @@ -7535,7 +7553,7 @@ path = Model; sourceTree = ""; }; - 0C3205BC2A867A46002EB914 /* GasPriceProvider */ = { + 0C3205BC2A867A46002EB914 /* GasPriceProviders */ = { isa = PBXGroup; children = ( 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */, @@ -7543,8 +7561,31 @@ 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */, 0C3205C52A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift */, 0C3205C72A877E5A002EB914 /* EvmGasPriceProviderFactory.swift */, + 0C3205CF2A895E5B002EB914 /* EvmConstantGasPriceProvider.swift */, ); - path = GasPriceProvider; + path = GasPriceProviders; + sourceTree = ""; + }; + 0C3205CD2A895E39002EB914 /* GasLimitProviders */ = { + isa = PBXGroup; + children = ( + 0C3205C92A895489002EB914 /* EvmGasLimitProvider.swift */, + 0C3205D12A895E85002EB914 /* EvmDefaultGasLimitProvider.swift */, + 0C3205D32A895EDA002EB914 /* EvmConstantGasLimitProvider.swift */, + 0C3205D52A895F0F002EB914 /* EvmGasLimitWithFallbackProvider.swift */, + 0C3205DB2A89677B002EB914 /* EvmGasLimitProviderFactory.swift */, + ); + path = GasLimitProviders; + sourceTree = ""; + }; + 0C3205CE2A895E41002EB914 /* NonceProviders */ = { + isa = PBXGroup; + children = ( + 0C3205CB2A895827002EB914 /* EvmNonceProvider.swift */, + 0C3205D72A896009002EB914 /* EvmDefaultNonceProvider.swift */, + 0C3205D92A896029002EB914 /* EvmConstantNonceProvider.swift */, + ); + path = NonceProviders; sourceTree = ""; }; 0C463FCE2A592ACD003E71C9 /* Effects */ = { @@ -12802,7 +12843,9 @@ 84A59157292AA64E00BCCF8F /* Evm */ = { isa = PBXGroup; children = ( - 0C3205BC2A867A46002EB914 /* GasPriceProvider */, + 0C3205CE2A895E41002EB914 /* NonceProviders */, + 0C3205CD2A895E39002EB914 /* GasLimitProviders */, + 0C3205BC2A867A46002EB914 /* GasPriceProviders */, 84A59159292AA83500BCCF8F /* EvmWebSocketOperationFactory.swift */, 84A5915B292B390800BCCF8F /* EvmTransactionBuilder.swift */, 84A5915D292B3C3D00BCCF8F /* EvmTransactionBuilder+Transfer.swift */, @@ -17773,6 +17816,7 @@ 8434B604298D474D00FACF4C /* GovernanceAddDelegationTracksPresenter.swift in Sources */, 8460E11F25420A2C00826F55 /* DetailsTriangularedView+Inspectable.swift in Sources */, AE8D028E2638074D00780D18 /* CIKeys.generated.swift in Sources */, + 0C3205CC2A895827002EB914 /* EvmNonceProvider.swift in Sources */, 8472C39C298BD8CE00043061 /* GovernanceSelectTracksInteractorError.swift in Sources */, F4CE0FC727344F600094CD8A /* AcalaContributionConfirmProtocols.swift in Sources */, 842E9E9D2A2C591C00759972 /* StakingDashboardInteractorError.swift in Sources */, @@ -18336,6 +18380,7 @@ 84746B3028153E4C002642F4 /* GradientBannerModel.swift in Sources */, 8433B34A29B63661005E5D0F /* ExtrinsicSplitter.swift in Sources */, 843B1D7A263EED5C00AF8957 /* StakingUnbondConfirmLayout.swift in Sources */, + 0C3205DA2A896029002EB914 /* EvmConstantNonceProvider.swift in Sources */, 8499FEDC27BFDD4300712589 /* NftLocalSubscriptionFactory.swift in Sources */, 88BB0B4529C1E29F00D041C1 /* Web3NameLocal.swift in Sources */, F4B06BFA264509E8003214D5 /* SetControllerCall.swift in Sources */, @@ -18969,6 +19014,7 @@ 8499FECA27BF8D2200712589 /* NftModel.swift in Sources */, 841E554B282DA66700C8438F /* StakingParachainProtocols.swift in Sources */, AEE5FB0326415E40002B8FDC /* StakingRebondSetupWireframe.swift in Sources */, + 0C3205D22A895E85002EB914 /* EvmDefaultGasLimitProvider.swift in Sources */, 84D8F15D24D8178000AF43E9 /* IconWithTitleViewModel.swift in Sources */, 844DB61F262D9C070025A8F0 /* ChainHistoryRange.swift in Sources */, 842A737127DB7EF1006EE1EA /* OperationSlashViewModel.swift in Sources */, @@ -20323,6 +20369,7 @@ 8451720F298C495500489EF1 /* BorderedIconLabelView+TrackStyle.swift in Sources */, DE03CA5AD7F1D0B80DFF13B6 /* DAppBrowserViewController.swift in Sources */, 8473B47A2A2083B1003DE213 /* StakingDashboardItemMapper.swift in Sources */, + 0C3205D62A895F0F002EB914 /* EvmGasLimitWithFallbackProvider.swift in Sources */, 8442002328E6FE1E00C49C4A /* ReferendumsViewManager.swift in Sources */, 70C0E48EE41B4C7229F5946C /* DAppBrowserViewLayout.swift in Sources */, 8849C5EA29806F1E00DE35CC /* InAppUpdatesStyles.swift in Sources */, @@ -20352,6 +20399,8 @@ D600448CB75095E6873E542F /* DAppTxDetailsProtocols.swift in Sources */, 84E25BE827E751B400290BF1 /* Charset+Encoding.swift in Sources */, 847999B12888A4FF00D1BAD2 /* SwitchAccount+CreateWatchOnlyWireframe.swift in Sources */, + 0C3205D42A895EDA002EB914 /* EvmConstantGasLimitProvider.swift in Sources */, + 0C3205DC2A89677B002EB914 /* EvmGasLimitProviderFactory.swift in Sources */, 8487580727EDEB9600495306 /* BorderedIconLabelView.swift in Sources */, 84AE7AAD27D3839D00495267 /* StackCellViewModel.swift in Sources */, 848CC94628D9FC46009EB4B0 /* ConvictionVotingTally.swift in Sources */, @@ -20440,6 +20489,7 @@ 8453DE5528FD24FF0055345C /* GovernanceSubscriptionProtocol.swift in Sources */, 8446F5EE2817130600B7A86C /* TitleAmountView+Style.swift in Sources */, 842643BB2878572E0031B5B5 /* TuringStakingRemoteSubscriptionService.swift in Sources */, + 0C3205D02A895E5B002EB914 /* EvmConstantGasPriceProvider.swift in Sources */, DB37BAF11845A4E5067E07C7 /* TransferConfirmViewController.swift in Sources */, 0D5245ED354CC52A842C85A0 /* TransferConfirmViewLayout.swift in Sources */, 0DF1E0D0CCEDC1340B7A47D7 /* TransferConfirmOnChainViewFactory.swift in Sources */, @@ -20518,6 +20568,7 @@ 3B6F50061AD9FC31D6712D9F /* ParaStkCollatorsSearchProtocols.swift in Sources */, 52326E49B049C54434C95132 /* ParaStkCollatorsSearchWireframe.swift in Sources */, 84117078285B1050006F4DFB /* XcmAssetTransfer.swift in Sources */, + 0C3205D82A896009002EB914 /* EvmDefaultNonceProvider.swift in Sources */, 8428229E289BCAAB00163031 /* SwitchAccount+ParitySignerWelcomeWireframe.swift in Sources */, 84A59162292B590100BCCF8F /* EvmTransactionFeeProxy.swift in Sources */, 84EBFCF0285E84C30006327E /* XcmMultiassetFilter.swift in Sources */, @@ -20771,6 +20822,7 @@ 0CCE25212A44306200286709 /* TransactionHistoryPhishingFilter.swift in Sources */, 77A6F5C62A2F17BE004AFD1A /* SendAssetOperationPresenter.swift in Sources */, 846DA55B2A20A56D006CD6C1 /* OffchainMultistakingUpdateService.swift in Sources */, + 0C3205CA2A895489002EB914 /* EvmGasLimitProvider.swift in Sources */, AE180C8B30831C9BAA39763A /* ParaStkYieldBoostSetupViewController.swift in Sources */, 8824D4222902D92F0022D778 /* ReferendumFullDetailsInteractor.swift in Sources */, 88B1862A28EF30A600D49854 /* YourVoteView.swift in Sources */, diff --git a/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift b/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift index b10b0e8fa6..91ebd8fdcc 100644 --- a/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift +++ b/novawallet/Common/Network/Ethereum/EthereumOperationFactory.swift @@ -4,14 +4,14 @@ import SoraKeystore import BigInt protocol EthereumOperationFactoryProtocol { - func createGasLimitOperation(for transaction: EthereumTransaction) -> BaseOperation + func createGasLimitOperation(for transaction: EthereumTransaction) -> BaseOperation> - func createGasPriceOperation() -> BaseOperation + func createGasPriceOperation() -> BaseOperation> func createTransactionsCountOperation( for accountAddress: Data, block: EthereumBlock - ) -> BaseOperation + ) -> BaseOperation> func createSendTransactionOperation( for transactionDataClosure: @escaping () throws -> Data diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift index 54d6303b24..3bd0e0615e 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift @@ -18,12 +18,6 @@ protocol EvmTransactionFeeProxyProtocol: AnyObject { final class EvmTransactionFeeProxy: TransactionFeeProxy { weak var delegate: EvmTransactionFeeProxyDelegate? - let fallbackGasLimit: BigUInt - - init(fallbackGasLimit: BigUInt) { - self.fallbackGasLimit = fallbackGasLimit - } - private func handle(result: Result, for identifier: TransactionFeeId) { update(result: result, for: identifier) @@ -47,7 +41,7 @@ extension EvmTransactionFeeProxy: EvmTransactionFeeProxyProtocol { setCachedState(.loading, for: reuseIdentifier) - service.estimateFee(closure, fallbackGasLimit: fallbackGasLimit, runningIn: .main) { [weak self] result in + service.estimateFee(closure, runningIn: .main) { [weak self] result in self?.handle(result: result, for: reuseIdentifier) } } diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift index 44e8353200..a1d823ccd0 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift @@ -6,13 +6,14 @@ import RobinHood typealias EvmFeeTransactionResult = Result typealias EvmEstimateFeeClosure = (EvmFeeTransactionResult) -> Void typealias EvmSubmitTransactionResult = Result +typealias EvmSignTransactionResult = Result typealias EvmTransactionSubmitClosure = (EvmSubmitTransactionResult) -> Void +typealias EvmTransactionSignClosure = (EvmSignTransactionResult) -> Void typealias EvmTransactionBuilderClosure = (EvmTransactionBuilderProtocol) throws -> EvmTransactionBuilderProtocol protocol EvmTransactionServiceProtocol { func estimateFee( _ closure: @escaping EvmTransactionBuilderClosure, - fallbackGasLimit: BigUInt, runningIn queue: DispatchQueue, completion completionClosure: @escaping EvmEstimateFeeClosure ) @@ -23,78 +24,114 @@ protocol EvmTransactionServiceProtocol { runningIn queue: DispatchQueue, completion completionClosure: @escaping EvmTransactionSubmitClosure ) -} -enum EvmTransactionServiceError: Error { - case invalidGasLimit(String) - case invalidGasPrice(String) - case invalidNonce(String) + func sign( + _ closure: @escaping EvmTransactionBuilderClosure, + signer: SigningWrapperProtocol, + runningIn queue: DispatchQueue, + completion completionClosure: @escaping EvmTransactionSignClosure + ) } final class EvmTransactionService { let accountId: AccountId let operationFactory: EthereumOperationFactoryProtocol let gasPriceProvider: EvmGasPriceProviderProtocol - let chain: ChainModel + let gasLimitProvider: EvmGasLimitProviderProtocol + let nonceProvider: EvmNonceProviderProtocol + let chainFormat: ChainFormat + let evmChainId: String let operationQueue: OperationQueue init( accountId: AccountId, operationFactory: EthereumOperationFactoryProtocol, gasPriceProvider: EvmGasPriceProviderProtocol, + gasLimitProvider: EvmGasLimitProviderProtocol, + nonceProvider: EvmNonceProviderProtocol, chain: ChainModel, operationQueue: OperationQueue ) { self.accountId = accountId self.operationFactory = operationFactory self.gasPriceProvider = gasPriceProvider - self.chain = chain + self.gasLimitProvider = gasLimitProvider + self.nonceProvider = nonceProvider + chainFormat = chain.chainFormat + evmChainId = chain.evmChainId self.operationQueue = operationQueue } - private func createGasLimitOrDefaultWrapper( - for transaction: EthereumTransaction, - fallbackGasLimit: BigUInt - ) -> CompoundOperationWrapper { - let gasEstimationOperation = operationFactory.createGasLimitOperation(for: transaction) - - let mappingOperation = ClosureOperation { - do { - let gasLimitString = try gasEstimationOperation.extractNoCancellableResultData() - - guard let gasLimit = BigUInt.fromHexString(gasLimitString) else { - throw EvmTransactionServiceError.invalidGasLimit(gasLimitString) - } + init( + accountId: AccountId, + operationFactory: EthereumOperationFactoryProtocol, + gasPriceProvider: EvmGasPriceProviderProtocol, + gasLimitProvider: EvmGasLimitProviderProtocol, + nonceProvider: EvmNonceProviderProtocol, + chainFormat: ChainFormat, + evmChainId: String, + operationQueue: OperationQueue + ) { + self.accountId = accountId + self.operationFactory = operationFactory + self.gasPriceProvider = gasPriceProvider + self.gasLimitProvider = gasLimitProvider + self.nonceProvider = nonceProvider + self.chainFormat = chainFormat + self.evmChainId = evmChainId + self.operationQueue = operationQueue + } - return gasLimit - } catch is JSONRPCError { - return fallbackGasLimit - } + private func createSignedTransactionWrapper( + _ closure: @escaping EvmTransactionBuilderClosure, + signer: SigningWrapperProtocol + ) throws -> CompoundOperationWrapper { + let address = try accountId.toAddress(using: chainFormat) + let initBuilder = EvmTransactionBuilder(address: address, chainId: evmChainId) + let builder = try closure(initBuilder) + + let gasEstimationWrapper = gasLimitProvider.getGasLimitWrapper(for: builder.buildTransaction()) + let gasPriceWrapper = gasPriceProvider.getGasPriceWrapper() + let nonceWrapper = nonceProvider.getNonceWrapper(for: accountId, block: .pending) + + let buildOperation = ClosureOperation { + let gasLimit = try gasEstimationWrapper.targetOperation.extractNoCancellableResultData() + let gasPrice = try gasPriceWrapper.targetOperation.extractNoCancellableResultData() + let nonce = try nonceWrapper.targetOperation.extractNoCancellableResultData() + + return try builder + .usingGasLimit(gasLimit) + .usingGasPrice(gasPrice) + .usingNonce(nonce) + .signing(using: { data in + try signer.sign(data).rawData() + }) + .build() } - mappingOperation.addDependency(gasEstimationOperation) + buildOperation.addDependency(gasEstimationWrapper.targetOperation) + buildOperation.addDependency(gasPriceWrapper.targetOperation) + buildOperation.addDependency(nonceWrapper.targetOperation) + + let dependencies = gasEstimationWrapper.allOperations + gasPriceWrapper.allOperations + + nonceWrapper.allOperations - return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: [gasEstimationOperation]) + return CompoundOperationWrapper(targetOperation: buildOperation, dependencies: dependencies) } } extension EvmTransactionService: EvmTransactionServiceProtocol { func estimateFee( _ closure: @escaping EvmTransactionBuilderClosure, - fallbackGasLimit: BigUInt, runningIn queue: DispatchQueue, completion completionClosure: @escaping EvmEstimateFeeClosure ) { do { - let address = try accountId.toAddress(using: chain.chainFormat) - let builder = EvmTransactionBuilder(address: address, chainId: chain.evmChainId) + let address = try accountId.toAddress(using: chainFormat) + let builder = EvmTransactionBuilder(address: address, chainId: evmChainId) let transaction = (try closure(builder)).buildTransaction() - let gasEstimationWrapper = createGasLimitOrDefaultWrapper( - for: transaction, - fallbackGasLimit: fallbackGasLimit - ) - + let gasEstimationWrapper = gasLimitProvider.getGasLimitWrapper(for: transaction) let gasPriceWrapper = gasPriceProvider.getGasPriceWrapper() let mapOperation = ClosureOperation { @@ -135,40 +172,13 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { completion completionClosure: @escaping EvmTransactionSubmitClosure ) { do { - let address = try accountId.toAddress(using: chain.chainFormat) - let initBuilder = EvmTransactionBuilder(address: address, chainId: chain.evmChainId) - let builder = try closure(initBuilder) - - let gasEstimationOperation = operationFactory.createGasLimitOperation(for: builder.buildTransaction()) - let gasPriceWrapper = gasPriceProvider.getGasPriceWrapper() - let nonceOperation = operationFactory.createTransactionsCountOperation(for: accountId, block: .pending) + let transactionWrapper = try createSignedTransactionWrapper(closure, signer: signer) let sendOperation = operationFactory.createSendTransactionOperation { - let gasLimitString = try gasEstimationOperation.extractNoCancellableResultData() - let gasPrice = try gasPriceWrapper.targetOperation.extractNoCancellableResultData() - let nonceString = try nonceOperation.extractNoCancellableResultData() - - guard let gasLimit = BigUInt.fromHexString(gasLimitString) else { - throw EvmTransactionServiceError.invalidGasLimit(gasLimitString) - } - - guard let nonce = BigUInt.fromHexString(nonceString) else { - throw EvmTransactionServiceError.invalidNonce(nonceString) - } - - return try builder - .usingGasLimit(gasLimit) - .usingGasPrice(gasPrice) - .usingNonce(nonce) - .signing(using: { data in - try signer.sign(data).rawData() - }) - .build() + try transactionWrapper.targetOperation.extractNoCancellableResultData() } - sendOperation.addDependency(gasEstimationOperation) - sendOperation.addDependency(gasPriceWrapper.targetOperation) - sendOperation.addDependency(nonceOperation) + sendOperation.addDependency(transactionWrapper.targetOperation) sendOperation.completionBlock = { queue.async { @@ -181,7 +191,7 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { } } - let operations = [gasEstimationOperation] + gasPriceWrapper.allOperations + [nonceOperation, sendOperation] + let operations = transactionWrapper.allOperations + [sendOperation] operationQueue.addOperations(operations, waitUntilFinished: false) } catch { @@ -190,4 +200,32 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { } } } + + func sign( + _ closure: @escaping EvmTransactionBuilderClosure, + signer: SigningWrapperProtocol, + runningIn queue: DispatchQueue, + completion completionClosure: @escaping EvmTransactionSignClosure + ) { + do { + let transactionWrapper = try createSignedTransactionWrapper(closure, signer: signer) + + transactionWrapper.targetOperation.completionBlock = { + queue.async { + do { + let txData = try transactionWrapper.targetOperation.extractNoCancellableResultData() + completionClosure(.success(txData)) + } catch { + completionClosure(.failure(error)) + } + } + } + + operationQueue.addOperations(transactionWrapper.allOperations, waitUntilFinished: false) + } catch { + dispatchInQueueWhenPossible(queue) { + completionClosure(.failure(error)) + } + } + } } diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift index b276cb4aee..fed0a1f55a 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmWebSocketOperationFactory.swift @@ -45,8 +45,8 @@ extension EvmWebSocketOperationFactory: EthereumOperationFactoryProtocol { ) } - func createGasLimitOperation(for transaction: EthereumTransaction) -> BaseOperation { - JSONRPCOperation<[EthereumTransaction], String>( + func createGasLimitOperation(for transaction: EthereumTransaction) -> BaseOperation> { + JSONRPCOperation<[EthereumTransaction], HexCodable>( engine: connection, method: EthereumMethod.estimateGas.rawValue, parameters: [transaction], @@ -54,7 +54,7 @@ extension EvmWebSocketOperationFactory: EthereumOperationFactoryProtocol { ) } - func createGasPriceOperation() -> BaseOperation { + func createGasPriceOperation() -> BaseOperation> { JSONRPCListOperation( engine: connection, method: EthereumMethod.gasPrice.rawValue, @@ -63,7 +63,10 @@ extension EvmWebSocketOperationFactory: EthereumOperationFactoryProtocol { ) } - func createTransactionsCountOperation(for accountAddress: Data, block: EthereumBlock) -> BaseOperation { + func createTransactionsCountOperation( + for accountAddress: Data, + block: EthereumBlock + ) -> BaseOperation> { let parameters = [accountAddress.toHex(includePrefix: true), block.rawValue] return JSONRPCListOperation( diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmConstantGasLimitProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmConstantGasLimitProvider.swift new file mode 100644 index 0000000000..71c43a6e3a --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmConstantGasLimitProvider.swift @@ -0,0 +1,17 @@ +import Foundation +import RobinHood +import BigInt + +final class EvmConstantGasLimitProvider { + let value: BigUInt + + init(value: BigUInt) { + self.value = value + } +} + +extension EvmConstantGasLimitProvider: EvmGasLimitProviderProtocol { + func getGasLimitWrapper(for _: EthereumTransaction) -> CompoundOperationWrapper { + CompoundOperationWrapper.createWithResult(value) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmDefaultGasLimitProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmDefaultGasLimitProvider.swift new file mode 100644 index 0000000000..ed746d900f --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmDefaultGasLimitProvider.swift @@ -0,0 +1,25 @@ +import Foundation +import BigInt +import RobinHood + +final class EvmDefaultGasLimitProvider { + let operationFactory: EthereumOperationFactoryProtocol + + init(operationFactory: EthereumOperationFactoryProtocol) { + self.operationFactory = operationFactory + } +} + +extension EvmDefaultGasLimitProvider: EvmGasLimitProviderProtocol { + func getGasLimitWrapper(for transaction: EthereumTransaction) -> CompoundOperationWrapper { + let fetchOperation = operationFactory.createGasLimitOperation(for: transaction) + + let mapOperation = ClosureOperation { + try fetchOperation.extractNoCancellableResultData().wrappedValue + } + + mapOperation.addDependency(fetchOperation) + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: [fetchOperation]) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProvider.swift new file mode 100644 index 0000000000..8792ad644b --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProvider.swift @@ -0,0 +1,8 @@ +import Foundation +import RobinHood +import BigInt +import SubstrateSdk + +protocol EvmGasLimitProviderProtocol { + func getGasLimitWrapper(for transaction: EthereumTransaction) -> CompoundOperationWrapper +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProviderFactory.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProviderFactory.swift new file mode 100644 index 0000000000..e286875429 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitProviderFactory.swift @@ -0,0 +1,22 @@ +import Foundation + +enum EvmGasLimitProviderFactory { + static func createGasLimitProvider( + for asset: AssetModel, + operationFactory: EthereumOperationFactoryProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) -> EvmGasLimitProviderProtocol { + let fallbackGasLimit = EvmFallbackGasLimit.value(for: asset) + + let fallbackProvider = EvmConstantGasLimitProvider(value: fallbackGasLimit) + let defaultProvider = EvmDefaultGasLimitProvider(operationFactory: operationFactory) + + return EvmGasLimitFallbackProvider( + mainProvider: defaultProvider, + fallbackProvider: fallbackProvider, + operationQueue: operationQueue, + logger: logger + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitWithFallbackProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitWithFallbackProvider.swift new file mode 100644 index 0000000000..75d72b7af8 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasLimitProviders/EvmGasLimitWithFallbackProvider.swift @@ -0,0 +1,81 @@ +import Foundation +import RobinHood +import BigInt +import SubstrateSdk + +final class EvmGasLimitFallbackProvider { + let mainProvider: EvmGasLimitProviderProtocol + let fallbackProvider: EvmGasLimitProviderProtocol + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + mainProvider: EvmGasLimitProviderProtocol, + fallbackProvider: EvmGasLimitProviderProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.mainProvider = mainProvider + self.fallbackProvider = fallbackProvider + self.operationQueue = operationQueue + self.logger = logger + } + + private func createWrapper( + for transaction: EthereumTransaction, + mainProvider: EvmGasLimitProviderProtocol, + fallbackProvider: EvmGasLimitProviderProtocol, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> CompoundOperationWrapper { + let mainGasLimitWrapper = mainProvider.getGasLimitWrapper(for: transaction) + + let fallbackOperation: BaseOperation<[BigUInt]> = OperationCombiningService( + operationManager: operationManager + ) { + do { + let limit = try mainGasLimitWrapper.targetOperation.extractNoCancellableResultData() + + let resultWrapper = CompoundOperationWrapper.createWithResult(limit) + return [resultWrapper] + } catch is JSONRPCError { + logger.warning("Using fallback for gas limit due to error") + + let resultWrapper = fallbackProvider.getGasLimitWrapper(for: transaction) + return [resultWrapper] + } + } + .longrunOperation() + + fallbackOperation.addDependency(mainGasLimitWrapper.targetOperation) + + let mapOperation = ClosureOperation { + let optLimit = try fallbackOperation.extractNoCancellableResultData().first + + guard let limit = optLimit else { + throw CommonError.dataCorruption + } + + return limit + } + + mapOperation.addDependency(fallbackOperation) + + return CompoundOperationWrapper( + targetOperation: mapOperation, + dependencies: mainGasLimitWrapper.allOperations + [fallbackOperation] + ) + } +} + +extension EvmGasLimitFallbackProvider: EvmGasLimitProviderProtocol { + func getGasLimitWrapper(for transaction: EthereumTransaction) -> CompoundOperationWrapper { + createWrapper( + for: transaction, + mainProvider: mainProvider, + fallbackProvider: fallbackProvider, + operationManager: OperationManager(operationQueue: operationQueue), + logger: logger + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmConstantGasPriceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmConstantGasPriceProvider.swift new file mode 100644 index 0000000000..4c2d4809b0 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmConstantGasPriceProvider.swift @@ -0,0 +1,17 @@ +import Foundation +import RobinHood +import BigInt + +final class EvmConstantGasPriceProvider { + let value: BigUInt + + init(value: BigUInt) { + self.value = value + } +} + +extension EvmConstantGasPriceProvider: EvmGasPriceProviderProtocol { + func getGasPriceWrapper() -> CompoundOperationWrapper { + CompoundOperationWrapper.createWithResult(value) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmGasPriceProvider.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProvider.swift rename to novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmGasPriceProvider.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProviderFactory.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmGasPriceProviderFactory.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceProviderFactory.swift rename to novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmGasPriceProviderFactory.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceWithFallbackProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmGasPriceWithFallbackProvider.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmGasPriceWithFallbackProvider.swift rename to novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmGasPriceWithFallbackProvider.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmLegacyGasPriceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmLegacyGasPriceProvider.swift similarity index 74% rename from novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmLegacyGasPriceProvider.swift rename to novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmLegacyGasPriceProvider.swift index 9fca2f9ce8..d814b488eb 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmLegacyGasPriceProvider.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmLegacyGasPriceProvider.swift @@ -16,13 +16,7 @@ extension EvmLegacyGasPriceProvider: EvmGasPriceProviderProtocol { let fetchOperation = operationFactory.createGasPriceOperation() let mapOperation = ClosureOperation { - let gasPriceString = try fetchOperation.extractNoCancellableResultData() - - guard let gasPrice = BigUInt.fromHexString(gasPriceString) else { - throw CommonError.dataCorruption - } - - return gasPrice + try fetchOperation.extractNoCancellableResultData().wrappedValue } mapOperation.addDependency(fetchOperation) diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmMaxPriorityGasPriceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmMaxPriorityGasPriceProvider.swift similarity index 100% rename from novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProvider/EvmMaxPriorityGasPriceProvider.swift rename to novawallet/Common/Services/ExtrinsicService/Evm/GasPriceProviders/EvmMaxPriorityGasPriceProvider.swift diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmConstantNonceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmConstantNonceProvider.swift new file mode 100644 index 0000000000..b15d4c8576 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmConstantNonceProvider.swift @@ -0,0 +1,20 @@ +import Foundation +import BigInt +import RobinHood + +final class EvmConstantNonceProvider { + let value: BigUInt + + init(value: BigUInt) { + self.value = value + } +} + +extension EvmConstantNonceProvider: EvmNonceProviderProtocol { + func getNonceWrapper( + for _: Data, + block _: EthereumBlock + ) -> CompoundOperationWrapper { + CompoundOperationWrapper.createWithResult(value) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmDefaultNonceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmDefaultNonceProvider.swift new file mode 100644 index 0000000000..8359081bdb --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmDefaultNonceProvider.swift @@ -0,0 +1,31 @@ +import Foundation +import RobinHood +import BigInt + +final class EvmDefaultNonceProvider { + let operationFactory: EthereumOperationFactoryProtocol + + init(operationFactory: EthereumOperationFactoryProtocol) { + self.operationFactory = operationFactory + } +} + +extension EvmDefaultNonceProvider: EvmNonceProviderProtocol { + func getNonceWrapper( + for accountAddress: Data, + block: EthereumBlock + ) -> CompoundOperationWrapper { + let fetchOperation = operationFactory.createTransactionsCountOperation( + for: accountAddress, + block: block + ) + + let mapOperation = ClosureOperation { + try fetchOperation.extractNoCancellableResultData().wrappedValue + } + + mapOperation.addDependency(fetchOperation) + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: [fetchOperation]) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmNonceProvider.swift b/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmNonceProvider.swift new file mode 100644 index 0000000000..f47eec5aa5 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/NonceProviders/EvmNonceProvider.swift @@ -0,0 +1,10 @@ +import Foundation +import RobinHood +import BigInt + +protocol EvmNonceProviderProtocol { + func getNonceWrapper( + for accountAddress: Data, + block: EthereumBlock + ) -> CompoundOperationWrapper +} diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift index 5c0e6576c3..08358a4ca5 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift @@ -9,17 +9,19 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { let ethereumOperationFactory: EthereumOperationFactoryProtocol let operationQueue: OperationQueue let signingWrapperFactory: SigningWrapperFactoryProtocol - let serializationFactory: EthereumSerializationFactoryProtocol let shouldSendTransaction: Bool let chainId: String + private var transaction: EthereumTransaction? + private var ethereumService: EvmTransactionServiceProtocol? + private var signingWrapper: SigningWrapperProtocol? + init( chainId: String, request: DAppOperationRequest, ethereumOperationFactory: EthereumOperationFactoryProtocol, operationQueue: OperationQueue, signingWrapperFactory: SigningWrapperFactoryProtocol, - serializationFactory: EthereumSerializationFactoryProtocol, shouldSendTransaction: Bool ) { self.chainId = chainId @@ -27,125 +29,101 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { self.ethereumOperationFactory = ethereumOperationFactory self.operationQueue = operationQueue self.signingWrapperFactory = signingWrapperFactory - self.serializationFactory = serializationFactory self.shouldSendTransaction = shouldSendTransaction } - private func createGasLimitOperation(for transaction: EthereumTransaction) -> BaseOperation { - if let gasLimit = transaction.gas, let value = try? BigUInt(hex: gasLimit), value > 0 { - return BaseOperation.createWithResult(gasLimit) - } else { - let gasTransaction = EthereumTransaction.gasEstimationTransaction(from: transaction) - return ethereumOperationFactory.createGasLimitOperation(for: gasTransaction) - } - } + private func setupServices() { + let optTransaction = try? request.operationData.map(to: EthereumTransaction.self) - private func createGasPriceOperation(for transaction: EthereumTransaction) -> BaseOperation { - if let gasPrice = transaction.gasPrice, let value = try? BigUInt(hex: gasPrice), value > 0 { - return BaseOperation.createWithResult(gasPrice) - } else { - return ethereumOperationFactory.createGasPriceOperation() + guard let transaction = optTransaction else { + let error = DAppOperationConfirmInteractorError.extrinsicBadField(name: "root") + presenter?.didReceive(modelResult: .failure(error)) + return } - } - private func createNonceOperation(for transaction: EthereumTransaction) -> BaseOperation { - if let nonce = transaction.nonce { - return BaseOperation.createWithResult(nonce) - } else { - guard let addressData = try? Data(hexString: transaction.from) else { - let error = DAppOperationConfirmInteractorError.extrinsicBadField(name: "from") - return BaseOperation.createWithError(error) - } + self.transaction = transaction - return ethereumOperationFactory.createTransactionsCountOperation( - for: addressData, - block: .pending - ) + guard + let transaction = try? request.operationData.map(to: EthereumTransaction.self), + let chainAccountId = try? AccountId(hexString: transaction.from), + let accountResponse = request.wallet.fetchEthereum(for: chainAccountId) else { + presenter?.didReceive(modelResult: .failure(ChainAccountFetchingError.accountNotExists)) + return } - } - private func createSigningTransactionWrapper( - for request: DAppOperationRequest - ) -> CompoundOperationWrapper { - guard let transaction = try? request.operationData.map(to: EthereumTransaction.self) else { - let error = DAppOperationConfirmInteractorError.extrinsicBadField(name: "root") - return CompoundOperationWrapper.createWithError(error) - } + let gasPriceProvider = createGasPriceProvider(for: transaction) + let gasLimitProvider = createGasLimitProvider(for: transaction) + let nonceProvider = createNonceProvider(for: transaction) + + ethereumService = EvmTransactionService( + accountId: chainAccountId, + operationFactory: ethereumOperationFactory, + gasPriceProvider: gasPriceProvider, + gasLimitProvider: gasLimitProvider, + nonceProvider: nonceProvider, + chainFormat: .ethereum, + evmChainId: chainId, + operationQueue: operationQueue + ) - let nonceOperation = createNonceOperation(for: transaction) - let gasOperation = createGasLimitOperation(for: transaction) - let gasPriceOperation = createGasPriceOperation(for: transaction) + signingWrapper = signingWrapperFactory.createSigningWrapper(for: accountResponse) + } - let mapOperation = ClosureOperation { - let nonce = try nonceOperation.extractNoCancellableResultData() - let gas = try gasOperation.extractNoCancellableResultData() - let gasPrice = try gasPriceOperation.extractNoCancellableResultData() + private func createBuilderClosure(for transaction: EthereumTransaction) -> EvmTransactionBuilderClosure { + { builder in - let gasTransaction = EthereumTransaction.gasEstimationTransaction(from: transaction) - return gasTransaction - .replacing(gas: gas) - .replacing(gasPrice: gasPrice) - .replacing(nonce: nonce) - } + var currentBuilder = builder - let dependencies = [gasOperation, gasPriceOperation, nonceOperation] - dependencies.forEach { mapOperation.addDependency($0) } + if let dataHex = transaction.data { + guard let data = try? Data(hexString: dataHex) else { + throw DAppOperationConfirmInteractorError.extrinsicBadField(name: "data") + } - return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) - } + currentBuilder = currentBuilder.usingTransactionData(data) + } - private func createSerializationOperation( - chainId: String, - dependingOn transactionOperation: BaseOperation, - signatureOperation: BaseOperation?, - serializationFactory: EthereumSerializationFactoryProtocol - ) -> BaseOperation { - ClosureOperation { - let transaction = try transactionOperation.extractNoCancellableResultData() - let maybeRawSignature = try signatureOperation?.extractNoCancellableResultData() - - let maybeSignature = try maybeRawSignature.map { rawSignature in - guard let signature = EthereumSignature(rawValue: rawSignature) else { - throw DAppOperationConfirmInteractorError.signingFailed + if let value = transaction.value { + guard let valueInt = BigUInt.fromHexString(value) else { + throw DAppOperationConfirmInteractorError.extrinsicBadField(name: "value") } - return signature + currentBuilder = currentBuilder.sendingValue(valueInt) } - return try serializationFactory.serialize( - transaction: transaction, - chainId: chainId, - signature: maybeSignature - ) + if let receiver = transaction.to { + currentBuilder = currentBuilder.toAddress(receiver) + } + + return currentBuilder } } - private func createSigningOperation( - using wallet: MetaAccountModel, - dependingOn signingDataOperation: BaseOperation, - transactionOperation: BaseOperation, - signingWrapperFactory: SigningWrapperFactoryProtocol - ) -> BaseOperation { - ClosureOperation { - let transaction = try transactionOperation.extractNoCancellableResultData() - let signingData = try signingDataOperation.extractNoCancellableResultData() - - guard - let addressData = try? Data(hexString: transaction.from), - let accountResponse = wallet.fetchEthereum(for: addressData) else { - throw ChainAccountFetchingError.accountNotExists - } + private func createGasLimitProvider(for transaction: EthereumTransaction) -> EvmGasLimitProviderProtocol { + if let gasLimit = transaction.gas, let value = try? BigUInt(hex: gasLimit), value > 0 { + return EvmConstantGasLimitProvider(value: value) + } else { + return EvmDefaultGasLimitProvider(operationFactory: ethereumOperationFactory) + } + } - let signingWrapper = signingWrapperFactory.createSigningWrapper(for: accountResponse) + private func createGasPriceProvider(for transaction: EthereumTransaction) -> EvmGasPriceProviderProtocol { + if let gasPrice = transaction.gasPrice, let value = try? BigUInt(hex: gasPrice), value > 0 { + return EvmConstantGasPriceProvider(value: value) + } else { + return EvmLegacyGasPriceProvider(operationFactory: ethereumOperationFactory) + } + } - return try signingWrapper.sign(signingData).rawData() + private func createNonceProvider(for transaction: EthereumTransaction) -> EvmNonceProviderProtocol { + if let nonce = transaction.nonce, let value = BigUInt.fromHexString(nonce) { + return EvmConstantNonceProvider(value: value) + } else { + return EvmDefaultNonceProvider(operationFactory: ethereumOperationFactory) } } - private func provideConfirmationModel() { - guard - let transaction = try? request.operationData.map(to: EthereumTransaction.self), - let chainAccountId = try? Data(hexString: transaction.from) else { + private func provideConfirmationModel(for transaction: EthereumTransaction) { + guard let chainAccountId = try? Data(hexString: transaction.from) else { presenter?.didReceive(feeResult: .failure(ChainAccountFetchingError.accountNotExists)) return } @@ -162,189 +140,123 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { presenter?.didReceive(modelResult: .success(model)) } - private func provideFeeViewModel() { - guard let transaction = try? request.operationData.map(to: EthereumTransaction.self) else { - let result: Result = .failure( - DAppOperationConfirmInteractorError.extrinsicBadField(name: "root") - ) - presenter?.didReceive(feeResult: result) - return - } - - let gasOperation = createGasLimitOperation(for: transaction) - let gasPriceOperation = createGasPriceOperation(for: transaction) - - let mapOperation = ClosureOperation { - let gasHex = try gasOperation.extractNoCancellableResultData() - let gasPriceHex = try gasPriceOperation.extractNoCancellableResultData() - - guard - let gas = BigUInt.fromHexString(gasHex), - let gasPrice = BigUInt.fromHexString(gasPriceHex) else { - throw DAppOperationConfirmInteractorError.extrinsicBadField(name: "gas") - } - - let fee = gas * gasPrice - - return RuntimeDispatchInfo( - fee: String(fee), - weight: 0 - ) - } - - mapOperation.addDependency(gasOperation) - mapOperation.addDependency(gasPriceOperation) - - mapOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - do { - let dispatchInfo = try mapOperation.extractNoCancellableResultData() - self?.presenter?.didReceive(feeResult: .success(dispatchInfo)) - } catch { - self?.presenter?.didReceive(feeResult: .failure(error)) - } + private func provideFeeModel( + for transaction: EthereumTransaction, + service: EvmTransactionServiceProtocol + ) { + service.estimateFee(createBuilderClosure(for: transaction), runningIn: .main) { [weak self] result in + switch result { + case let .success(fee): + let dispatchInfo = RuntimeDispatchInfo( + fee: String(fee), + weight: 0 + ) + + self?.presenter?.didReceive(feeResult: .success(dispatchInfo)) + case let .failure(error): + self?.presenter?.didReceive(feeResult: .failure(error)) } } - - operationQueue.addOperations( - [gasOperation, gasPriceOperation, mapOperation], - waitUntilFinished: false - ) } - private func confirmSend() { - let transactionWrapper = createSigningTransactionWrapper(for: request) - let signatureDataOperation = createSerializationOperation( - chainId: chainId, - dependingOn: transactionWrapper.targetOperation, - signatureOperation: nil, - serializationFactory: serializationFactory - ) - - signatureDataOperation.addDependency(transactionWrapper.targetOperation) - - let signingOperation = createSigningOperation( - using: request.wallet, - dependingOn: signatureDataOperation, - transactionOperation: transactionWrapper.targetOperation, - signingWrapperFactory: signingWrapperFactory - ) - - signingOperation.addDependency(signatureDataOperation) - signingOperation.addDependency(transactionWrapper.targetOperation) - - let serializationOperation = createSerializationOperation( - chainId: chainId, - dependingOn: transactionWrapper.targetOperation, - signatureOperation: signingOperation, - serializationFactory: serializationFactory - ) - - serializationOperation.addDependency(transactionWrapper.targetOperation) - serializationOperation.addDependency(signingOperation) - - let sendOperation = ethereumOperationFactory.createSendTransactionOperation { - try serializationOperation.extractNoCancellableResultData() - } - - sendOperation.addDependency(serializationOperation) - - sendOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard let strongSelf = self else { - return - } + private func confirmSend( + for transaction: EthereumTransaction, + service: EvmTransactionServiceProtocol, + signer: SigningWrapperProtocol + ) { + service.submit( + createBuilderClosure(for: transaction), + signer: signer, + runningIn: .main + ) { [weak self] result in + guard let self = self else { + return + } - do { - let txHash = try sendOperation.extractNoCancellableResultData() + do { + switch result { + case let .success(txHash): let txHashData = try Data(hexString: txHash) let response = DAppOperationResponse(signature: txHashData) let result: Result = .success(response) - strongSelf.presenter?.didReceive(responseResult: result, for: strongSelf.request) - } catch { - let result: Result = .failure(error) - strongSelf.presenter?.didReceive(responseResult: result, for: strongSelf.request) + self.presenter?.didReceive(responseResult: result, for: self.request) + case let .failure(error): + throw error } + } catch { + let result: Result = .failure(error) + self.presenter?.didReceive(responseResult: result, for: self.request) } } - - let allOperations = transactionWrapper.allOperations + - [signatureDataOperation, signingOperation, serializationOperation, sendOperation] - - operationQueue.addOperations(allOperations, waitUntilFinished: false) } - private func confirmSign() { - let transactionWrapper = createSigningTransactionWrapper(for: request) - let signatureDataOperation = createSerializationOperation( - chainId: chainId, - dependingOn: transactionWrapper.targetOperation, - signatureOperation: nil, - serializationFactory: serializationFactory - ) - - signatureDataOperation.addDependency(transactionWrapper.targetOperation) - - let signingOperation = createSigningOperation( - using: request.wallet, - dependingOn: signatureDataOperation, - transactionOperation: transactionWrapper.targetOperation, - signingWrapperFactory: signingWrapperFactory - ) - - signingOperation.addDependency(signatureDataOperation) - signingOperation.addDependency(transactionWrapper.targetOperation) - - let serializationOperation = createSerializationOperation( - chainId: chainId, - dependingOn: transactionWrapper.targetOperation, - signatureOperation: signingOperation, - serializationFactory: serializationFactory - ) - - serializationOperation.addDependency(transactionWrapper.targetOperation) - serializationOperation.addDependency(signingOperation) - - serializationOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard let strongSelf = self else { - return - } + private func confirmSign( + for transaction: EthereumTransaction, + service: EvmTransactionServiceProtocol, + signer: SigningWrapperProtocol + ) { + service.sign( + createBuilderClosure(for: transaction), + signer: signer, + runningIn: .main + ) { [weak self] result in + guard let self = self else { + return + } - do { - let transaction = try serializationOperation.extractNoCancellableResultData() - let response = DAppOperationResponse(signature: transaction) + do { + switch result { + case let .success(signedTransaction): + let response = DAppOperationResponse(signature: signedTransaction) let result: Result = .success(response) - strongSelf.presenter?.didReceive(responseResult: result, for: strongSelf.request) - } catch { - let result: Result = .failure(error) - strongSelf.presenter?.didReceive(responseResult: result, for: strongSelf.request) + self.presenter?.didReceive(responseResult: result, for: self.request) + case let .failure(error): + throw error } + } catch { + let result: Result = .failure(error) + self.presenter?.didReceive(responseResult: result, for: self.request) } } - - let allOperations = transactionWrapper.allOperations + - [signatureDataOperation, signingOperation, serializationOperation] - - operationQueue.addOperations(allOperations, waitUntilFinished: false) } } extension DAppEthereumConfirmInteractor: DAppOperationConfirmInteractorInputProtocol { func setup() { - provideConfirmationModel() - provideFeeViewModel() + setupServices() + + guard + let transaction = transaction, + let ethereumService = ethereumService else { + return + } + + provideConfirmationModel(for: transaction) + provideFeeModel(for: transaction, service: ethereumService) } func estimateFee() { - provideFeeViewModel() + guard + let transaction = transaction, + let ethereumService = ethereumService else { + return + } + + provideFeeModel(for: transaction, service: ethereumService) } func confirm() { + guard + let transaction = transaction, + let ethereumService = ethereumService, + let signer = signingWrapper else { + return + } + if shouldSendTransaction { - confirmSend() + confirmSend(for: transaction, service: ethereumService, signer: signer) } else { - confirmSign() + confirmSign(for: transaction, service: ethereumService, signer: signer) } } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift index 6308959ee4..1c000f99ff 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift @@ -153,7 +153,6 @@ struct DAppOperationConfirmViewFactory { ethereumOperationFactory: operationFactory, operationQueue: OperationManagerFacade.sharedDefaultQueue, signingWrapperFactory: SigningWrapperFactory(keystore: Keychain()), - serializationFactory: EthereumSerializationFactory(), shouldSendTransaction: shouldSendTransaction ) } diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index b17be7ccb5..0a5804d276 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -122,10 +122,21 @@ struct TransferConfirmOnChainViewFactory { logger: Logger.shared ) + let gasLimitProvider = EvmGasLimitProviderFactory.createGasLimitProvider( + for: asset, + operationFactory: operationFactory, + operationQueue: operationQueue, + logger: Logger.shared + ) + + let nonceProvider = EvmDefaultNonceProvider(operationFactory: operationFactory) + let extrinsicService = EvmTransactionService( accountId: account.accountId, operationFactory: operationFactory, gasPriceProvider: gasPriceProvider, + gasLimitProvider: gasLimitProvider, + nonceProvider: nonceProvider, chain: chain, operationQueue: operationQueue ) @@ -145,7 +156,7 @@ struct TransferConfirmOnChainViewFactory { selectedAccount: account, chain: chain, asset: asset, - feeProxy: EvmTransactionFeeProxy(fallbackGasLimit: fallbackGasLimit), + feeProxy: EvmTransactionFeeProxy(), extrinsicService: extrinsicService, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift index ef1c4524d8..8a9950fd30 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift @@ -149,27 +149,37 @@ extension TransferSetupPresenterFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue let operationFactory = EvmWebSocketOperationFactory(connection: connection) + let gasPriceProvider = EvmGasPriceProviderFactory.createMaxPriorityWithLegacyFallback( operationFactory: operationFactory, operationQueue: operationQueue, logger: Logger.shared ) + let gasLimitProvider = EvmGasLimitProviderFactory.createGasLimitProvider( + for: asset, + operationFactory: operationFactory, + operationQueue: operationQueue, + logger: Logger.shared + ) + + let nonceProvider = EvmDefaultNonceProvider(operationFactory: operationFactory) + let extrinsicService = EvmTransactionService( accountId: selectedAccount.accountId, operationFactory: operationFactory, gasPriceProvider: gasPriceProvider, + gasLimitProvider: gasLimitProvider, + nonceProvider: nonceProvider, chain: chain, operationQueue: operationQueue ) - let fallbackGasLimit = EvmFallbackGasLimit.value(for: asset) - return EvmOnChainTransferSetupInteractor( selectedAccount: selectedAccount, chain: chain, asset: asset, - feeProxy: EvmTransactionFeeProxy(fallbackGasLimit: fallbackGasLimit), + feeProxy: EvmTransactionFeeProxy(), extrinsicService: extrinsicService, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, From da1c362046ef3b4655d170076d9954c912ab369a Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 13 Aug 2023 22:50:06 +0300 Subject: [PATCH 10/31] fix test --- .../Services/ChainRegistry/ChainSyncServiceTests.swift | 1 - .../Modules/ControllerAccount/ControllerAccountTests.swift | 5 ----- novawalletTests/Modules/Root/RootTests.swift | 1 - .../StakingRewardPayouts/StakingPayoutsConfirmTests.swift | 5 ----- 4 files changed, 12 deletions(-) diff --git a/novawalletTests/Common/Services/ChainRegistry/ChainSyncServiceTests.swift b/novawalletTests/Common/Services/ChainRegistry/ChainSyncServiceTests.swift index 83dc729d0c..2c444e9d4d 100644 --- a/novawalletTests/Common/Services/ChainRegistry/ChainSyncServiceTests.swift +++ b/novawalletTests/Common/Services/ChainRegistry/ChainSyncServiceTests.swift @@ -176,7 +176,6 @@ class ChainSyncServiceTests: XCTestCase { let remoteItems = ChainModelGenerator.generateRemote(count: 3) let chainWithEvmTokens = remoteItems[0] let otherChainWithEvmTokens = remoteItems[1] - let chainWithoutEvmTokens = remoteItems[2] let evmToken = ChainModelGenerator.generateEvmToken(chainId1: chainWithEvmTokens.chainId, chainId2: otherChainWithEvmTokens.chainId) let usdChainAssets = [evmToken].chainAssets() diff --git a/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift b/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift index 9e60c09487..306d87cd53 100644 --- a/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift +++ b/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift @@ -105,11 +105,6 @@ class ControllerAccountTests: XCTestCase { ) presenter.didReceiveControllerAccountInfo(result: .success(controllerAccountInfo), address: controllerAddress) - let stashAccountInfo = AccountInfo( - nonce: 0, - data: AccountData(free: 100000000000000, reserved: 0, miscFrozen: 0, feeFrozen: 0) - ) - let stashAccountId = try! stashAddress.toAccountId() let stashBalance = AssetBalance( chainAssetId: ChainAssetId(chainId: chain.chainId, assetId: chain.utilityAsset()!.assetId), diff --git a/novawalletTests/Modules/Root/RootTests.swift b/novawalletTests/Modules/Root/RootTests.swift index 90921db965..8ae1fe67ef 100644 --- a/novawalletTests/Modules/Root/RootTests.swift +++ b/novawalletTests/Modules/Root/RootTests.swift @@ -27,7 +27,6 @@ class RootTests: XCTestCase { ) let onboardingExpectation = XCTestExpectation() - let securityLayerExpectation = XCTestExpectation() stub(wireframe) { stub in when(stub).showOnboarding(on: any()).then { _ in diff --git a/novawalletTests/Modules/Staking/StakingRewardPayouts/StakingPayoutsConfirmTests.swift b/novawalletTests/Modules/Staking/StakingRewardPayouts/StakingPayoutsConfirmTests.swift index 2838a53b54..31a8a52c78 100644 --- a/novawalletTests/Modules/Staking/StakingRewardPayouts/StakingPayoutsConfirmTests.swift +++ b/novawalletTests/Modules/Staking/StakingRewardPayouts/StakingPayoutsConfirmTests.swift @@ -52,7 +52,6 @@ class StakingPayoutsConfirmTests: XCTestCase { let chainRegistry = MockChainRegistryProtocol().applyDefault(for: [chain]) - let stakingLocalSubscriptionFactory = StakingLocalSubscriptionFactoryStub() let walletLocalSubscriptionFactory = WalletLocalSubscriptionFactoryStub( balance: BigUInt(2e+12) ) @@ -66,10 +65,6 @@ class StakingPayoutsConfirmTests: XCTestCase { ) ) - let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: UserDataStorageTestFacade()) - - let extrinsicOperationFactory = ExtrinsicOperationFactoryStub() - let interactor = StakingPayoutConfirmationInteractor( selectedAccount: selectedAccount, chainAsset: chainAsset, From 25dada0e9ff6639d52b08e9f6a9b1bf143fb74b2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 13 Aug 2023 23:54:41 +0300 Subject: [PATCH 11/31] refactored extrinsic service interface --- novawallet.xcodeproj/project.pbxproj | 8 +++ .../ExtrinsicService/Evm/EvmFeeModel.swift | 16 +++++ .../Evm/EvmTransactionFeeProxy.swift | 6 +- .../Evm/EvmTransactionPrice.swift | 7 ++ .../Evm/EvmTransactionService.swift | 69 ++++++++++++------- .../DAppEthereumConfirmInteractor.swift | 48 ++++++++++--- .../DAppEthereumSignBytesInteractor.swift | 4 +- ...pOperationConfirmInteractor+Protocol.swift | 7 +- .../DAppOperationConfirmPresenter.swift | 10 ++- .../DAppOperationConfirmProtocols.swift | 3 +- .../DAppSignBytesConfirmInteractor.swift | 4 +- .../EvmOnChainTransferInteractor.swift | 10 ++- .../TransferEvmOnChainConfirmInteractor.swift | 4 +- .../TransferConfirmOnChainViewFactory.swift | 10 +-- ...ransferSetupPresenterFactory+OnChain.swift | 9 +-- novawalletTests/Mocks/ModuleMocks.swift | 19 ++--- 16 files changed, 153 insertions(+), 81 deletions(-) create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/EvmFeeModel.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionPrice.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index f2479b88fb..d97e2b43e0 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -65,6 +65,8 @@ 0C3205D82A896009002EB914 /* EvmDefaultNonceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205D72A896009002EB914 /* EvmDefaultNonceProvider.swift */; }; 0C3205DA2A896029002EB914 /* EvmConstantNonceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205D92A896029002EB914 /* EvmConstantNonceProvider.swift */; }; 0C3205DC2A89677B002EB914 /* EvmGasLimitProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205DB2A89677B002EB914 /* EvmGasLimitProviderFactory.swift */; }; + 0C3205DE2A896C19002EB914 /* EvmFeeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205DD2A896C19002EB914 /* EvmFeeModel.swift */; }; + 0C3205E02A896C7E002EB914 /* EvmTransactionPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205DF2A896C7E002EB914 /* EvmTransactionPrice.swift */; }; 0C40520C2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */; }; 0C463FC82A58126A003E71C9 /* UIView+MotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */; }; 0C463FD02A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */; }; @@ -3736,6 +3738,8 @@ 0C3205D72A896009002EB914 /* EvmDefaultNonceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmDefaultNonceProvider.swift; sourceTree = ""; }; 0C3205D92A896029002EB914 /* EvmConstantNonceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmConstantNonceProvider.swift; sourceTree = ""; }; 0C3205DB2A89677B002EB914 /* EvmGasLimitProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasLimitProviderFactory.swift; sourceTree = ""; }; + 0C3205DD2A896C19002EB914 /* EvmFeeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmFeeModel.swift; sourceTree = ""; }; + 0C3205DF2A896C7E002EB914 /* EvmTransactionPrice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmTransactionPrice.swift; sourceTree = ""; }; 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmInteractor.swift; sourceTree = ""; }; 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBlurBackgroundView.swift; sourceTree = ""; }; 0C432D57ACFA53F42E574CBD /* TokensManageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewController.swift; sourceTree = ""; }; @@ -12853,6 +12857,8 @@ 84A59161292B590100BCCF8F /* EvmTransactionFeeProxy.swift */, 84FCCD97292E3610002D2D3D /* JSONRPCError+Evm.swift */, 84B1318B29ED70BF004EA1FF /* EvmFallbackGasLimit.swift */, + 0C3205DD2A896C19002EB914 /* EvmFeeModel.swift */, + 0C3205DF2A896C7E002EB914 /* EvmTransactionPrice.swift */, ); path = Evm; sourceTree = ""; @@ -20272,6 +20278,7 @@ 2EC610DC06643A00876BED6E /* AssetListWireframe.swift in Sources */, 8499FE6A27BD2FA900712589 /* RMRKNftV2.swift in Sources */, 844B6EC528A3153C00A8BE83 /* MetaAccountModelType+SignatureFormat.swift in Sources */, + 0C3205DE2A896C19002EB914 /* EvmFeeModel.swift in Sources */, 2932BD7922FDCD64F4E9A57D /* AssetListPresenter.swift in Sources */, 84355CF628B63D19004E5C5E /* LedgerCrypto+Conversion.swift in Sources */, 8463781F2979DACB0034162B /* ReferendumsActivityViewModelFactory.swift in Sources */, @@ -20312,6 +20319,7 @@ E04E3ABA985AA3D89AE20BF5 /* CrowdloanYourContributionsProtocols.swift in Sources */, 84C98A5C29A158D700F5328B /* DelegateVotedReferendaParams.swift in Sources */, 5E621A350A6DDD78597CC9E5 /* CrowdloanYourContributionsWireframe.swift in Sources */, + 0C3205E02A896C7E002EB914 /* EvmTransactionPrice.swift in Sources */, 716F0819BAB14322E34E416C /* CrowdloanYourContributionsPresenter.swift in Sources */, F0675F495766D07473B065F7 /* CrowdloanYourContributionsInteractor.swift in Sources */, 845B080D2918D4F8005785D3 /* Democracy+Call.swift in Sources */, diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmFeeModel.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmFeeModel.swift new file mode 100644 index 0000000000..a5bb2d2fc9 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmFeeModel.swift @@ -0,0 +1,16 @@ +import Foundation +import BigInt + +struct EvmFeeModel { + let gasLimit: BigUInt + let defaultGasPrice: BigUInt + let maxPriorityGasPrice: BigUInt? + + var gasPrice: BigUInt { + maxPriorityGasPrice ?? defaultGasPrice + } + + var fee: BigUInt { + gasPrice * gasLimit + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift index 3bd0e0615e..eed6894090 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionFeeProxy.swift @@ -2,7 +2,7 @@ import Foundation import BigInt protocol EvmTransactionFeeProxyDelegate: AnyObject { - func didReceiveFee(result: Result, for identifier: TransactionFeeId) + func didReceiveFee(result: Result, for identifier: TransactionFeeId) } protocol EvmTransactionFeeProxyProtocol: AnyObject { @@ -15,10 +15,10 @@ protocol EvmTransactionFeeProxyProtocol: AnyObject { ) } -final class EvmTransactionFeeProxy: TransactionFeeProxy { +final class EvmTransactionFeeProxy: TransactionFeeProxy { weak var delegate: EvmTransactionFeeProxyDelegate? - private func handle(result: Result, for identifier: TransactionFeeId) { + private func handle(result: Result, for identifier: TransactionFeeId) { update(result: result, for: identifier) delegate?.didReceiveFee(result: result, for: identifier) diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionPrice.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionPrice.swift new file mode 100644 index 0000000000..e519d9c9f1 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionPrice.swift @@ -0,0 +1,7 @@ +import Foundation +import BigInt + +struct EvmTransactionPrice { + let gasLimit: BigUInt + let gasPrice: BigUInt +} diff --git a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift index a1d823ccd0..4076059a46 100644 --- a/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift +++ b/novawallet/Common/Services/ExtrinsicService/Evm/EvmTransactionService.swift @@ -3,7 +3,7 @@ import BigInt import SubstrateSdk import RobinHood -typealias EvmFeeTransactionResult = Result +typealias EvmFeeTransactionResult = Result typealias EvmEstimateFeeClosure = (EvmFeeTransactionResult) -> Void typealias EvmSubmitTransactionResult = Result typealias EvmSignTransactionResult = Result @@ -20,6 +20,7 @@ protocol EvmTransactionServiceProtocol { func submit( _ closure: @escaping EvmTransactionBuilderClosure, + price: EvmTransactionPrice, signer: SigningWrapperProtocol, runningIn queue: DispatchQueue, completion completionClosure: @escaping EvmTransactionSubmitClosure @@ -27,6 +28,7 @@ protocol EvmTransactionServiceProtocol { func sign( _ closure: @escaping EvmTransactionBuilderClosure, + price: EvmTransactionPrice, signer: SigningWrapperProtocol, runningIn queue: DispatchQueue, completion completionClosure: @escaping EvmTransactionSignClosure @@ -36,7 +38,8 @@ protocol EvmTransactionServiceProtocol { final class EvmTransactionService { let accountId: AccountId let operationFactory: EthereumOperationFactoryProtocol - let gasPriceProvider: EvmGasPriceProviderProtocol + let maxPriorityGasPriceProvider: EvmGasPriceProviderProtocol + let defaultGasPriceProvider: EvmGasPriceProviderProtocol let gasLimitProvider: EvmGasLimitProviderProtocol let nonceProvider: EvmNonceProviderProtocol let chainFormat: ChainFormat @@ -46,7 +49,8 @@ final class EvmTransactionService { init( accountId: AccountId, operationFactory: EthereumOperationFactoryProtocol, - gasPriceProvider: EvmGasPriceProviderProtocol, + maxPriorityGasPriceProvider: EvmGasPriceProviderProtocol, + defaultGasPriceProvider: EvmGasPriceProviderProtocol, gasLimitProvider: EvmGasLimitProviderProtocol, nonceProvider: EvmNonceProviderProtocol, chain: ChainModel, @@ -54,7 +58,8 @@ final class EvmTransactionService { ) { self.accountId = accountId self.operationFactory = operationFactory - self.gasPriceProvider = gasPriceProvider + self.maxPriorityGasPriceProvider = maxPriorityGasPriceProvider + self.defaultGasPriceProvider = defaultGasPriceProvider self.gasLimitProvider = gasLimitProvider self.nonceProvider = nonceProvider chainFormat = chain.chainFormat @@ -65,7 +70,8 @@ final class EvmTransactionService { init( accountId: AccountId, operationFactory: EthereumOperationFactoryProtocol, - gasPriceProvider: EvmGasPriceProviderProtocol, + maxPriorityGasPriceProvider: EvmGasPriceProviderProtocol, + defaultGasPriceProvider: EvmGasPriceProviderProtocol, gasLimitProvider: EvmGasLimitProviderProtocol, nonceProvider: EvmNonceProviderProtocol, chainFormat: ChainFormat, @@ -74,7 +80,8 @@ final class EvmTransactionService { ) { self.accountId = accountId self.operationFactory = operationFactory - self.gasPriceProvider = gasPriceProvider + self.maxPriorityGasPriceProvider = maxPriorityGasPriceProvider + self.defaultGasPriceProvider = defaultGasPriceProvider self.gasLimitProvider = gasLimitProvider self.nonceProvider = nonceProvider self.chainFormat = chainFormat @@ -84,24 +91,21 @@ final class EvmTransactionService { private func createSignedTransactionWrapper( _ closure: @escaping EvmTransactionBuilderClosure, + price: EvmTransactionPrice, signer: SigningWrapperProtocol ) throws -> CompoundOperationWrapper { let address = try accountId.toAddress(using: chainFormat) let initBuilder = EvmTransactionBuilder(address: address, chainId: evmChainId) let builder = try closure(initBuilder) - let gasEstimationWrapper = gasLimitProvider.getGasLimitWrapper(for: builder.buildTransaction()) - let gasPriceWrapper = gasPriceProvider.getGasPriceWrapper() let nonceWrapper = nonceProvider.getNonceWrapper(for: accountId, block: .pending) let buildOperation = ClosureOperation { - let gasLimit = try gasEstimationWrapper.targetOperation.extractNoCancellableResultData() - let gasPrice = try gasPriceWrapper.targetOperation.extractNoCancellableResultData() let nonce = try nonceWrapper.targetOperation.extractNoCancellableResultData() return try builder - .usingGasLimit(gasLimit) - .usingGasPrice(gasPrice) + .usingGasLimit(price.gasLimit) + .usingGasPrice(price.gasPrice) .usingNonce(nonce) .signing(using: { data in try signer.sign(data).rawData() @@ -109,12 +113,9 @@ final class EvmTransactionService { .build() } - buildOperation.addDependency(gasEstimationWrapper.targetOperation) - buildOperation.addDependency(gasPriceWrapper.targetOperation) buildOperation.addDependency(nonceWrapper.targetOperation) - let dependencies = gasEstimationWrapper.allOperations + gasPriceWrapper.allOperations + - nonceWrapper.allOperations + let dependencies = nonceWrapper.allOperations return CompoundOperationWrapper(targetOperation: buildOperation, dependencies: dependencies) } @@ -132,17 +133,24 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { let transaction = (try closure(builder)).buildTransaction() let gasEstimationWrapper = gasLimitProvider.getGasLimitWrapper(for: transaction) - let gasPriceWrapper = gasPriceProvider.getGasPriceWrapper() + let defaultGasPriceWrapper = defaultGasPriceProvider.getGasPriceWrapper() + let maxPriorityPriceWrapper = maxPriorityGasPriceProvider.getGasPriceWrapper() - let mapOperation = ClosureOperation { + let mapOperation = ClosureOperation { let gasLimit = try gasEstimationWrapper.targetOperation.extractNoCancellableResultData() - let gasPrice = try gasPriceWrapper.targetOperation.extractNoCancellableResultData() - - return gasLimit * gasPrice + let defaultGasPrice = try defaultGasPriceWrapper.targetOperation.extractNoCancellableResultData() + let maxPriorityGasPrice = try? maxPriorityPriceWrapper.targetOperation.extractNoCancellableResultData() + + return EvmFeeModel( + gasLimit: gasLimit, + defaultGasPrice: defaultGasPrice, + maxPriorityGasPrice: maxPriorityGasPrice + ) } mapOperation.addDependency(gasEstimationWrapper.targetOperation) - mapOperation.addDependency(gasPriceWrapper.targetOperation) + mapOperation.addDependency(defaultGasPriceWrapper.targetOperation) + mapOperation.addDependency(maxPriorityPriceWrapper.targetOperation) mapOperation.completionBlock = { queue.async { @@ -155,7 +163,8 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { } } - let operations = gasEstimationWrapper.allOperations + gasPriceWrapper.allOperations + [mapOperation] + let operations = gasEstimationWrapper.allOperations + defaultGasPriceWrapper.allOperations + + maxPriorityPriceWrapper.allOperations + [mapOperation] operationQueue.addOperations(operations, waitUntilFinished: false) } catch { @@ -167,12 +176,17 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { func submit( _ closure: @escaping EvmTransactionBuilderClosure, + price: EvmTransactionPrice, signer: SigningWrapperProtocol, runningIn queue: DispatchQueue, completion completionClosure: @escaping EvmTransactionSubmitClosure ) { do { - let transactionWrapper = try createSignedTransactionWrapper(closure, signer: signer) + let transactionWrapper = try createSignedTransactionWrapper( + closure, + price: price, + signer: signer + ) let sendOperation = operationFactory.createSendTransactionOperation { try transactionWrapper.targetOperation.extractNoCancellableResultData() @@ -203,12 +217,17 @@ extension EvmTransactionService: EvmTransactionServiceProtocol { func sign( _ closure: @escaping EvmTransactionBuilderClosure, + price: EvmTransactionPrice, signer: SigningWrapperProtocol, runningIn queue: DispatchQueue, completion completionClosure: @escaping EvmTransactionSignClosure ) { do { - let transactionWrapper = try createSignedTransactionWrapper(closure, signer: signer) + let transactionWrapper = try createSignedTransactionWrapper( + closure, + price: price, + signer: signer + ) transactionWrapper.targetOperation.completionBlock = { queue.async { diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift index 08358a4ca5..8ed3c21b8f 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift @@ -15,6 +15,7 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { private var transaction: EthereumTransaction? private var ethereumService: EvmTransactionServiceProtocol? private var signingWrapper: SigningWrapperProtocol? + private var lastFee: EvmFeeModel? init( chainId: String, @@ -51,14 +52,16 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { return } - let gasPriceProvider = createGasPriceProvider(for: transaction) + let defaultGasPriceProvider = createDefaultGasPriceProvider(for: transaction) + let maxPriorityGasPriceProvider = createMaxPriorityGasPriceProvider(for: transaction) let gasLimitProvider = createGasLimitProvider(for: transaction) let nonceProvider = createNonceProvider(for: transaction) ethereumService = EvmTransactionService( accountId: chainAccountId, operationFactory: ethereumOperationFactory, - gasPriceProvider: gasPriceProvider, + maxPriorityGasPriceProvider: maxPriorityGasPriceProvider, + defaultGasPriceProvider: defaultGasPriceProvider, gasLimitProvider: gasLimitProvider, nonceProvider: nonceProvider, chainFormat: .ethereum, @@ -106,7 +109,9 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { } } - private func createGasPriceProvider(for transaction: EthereumTransaction) -> EvmGasPriceProviderProtocol { + private func createDefaultGasPriceProvider( + for transaction: EthereumTransaction + ) -> EvmGasPriceProviderProtocol { if let gasPrice = transaction.gasPrice, let value = try? BigUInt(hex: gasPrice), value > 0 { return EvmConstantGasPriceProvider(value: value) } else { @@ -114,6 +119,16 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { } } + private func createMaxPriorityGasPriceProvider( + for transaction: EthereumTransaction + ) -> EvmGasPriceProviderProtocol { + if let gasPrice = transaction.gasPrice, let value = try? BigUInt(hex: gasPrice), value > 0 { + return EvmConstantGasPriceProvider(value: value) + } else { + return EvmMaxPriorityGasPriceProvider(operationFactory: ethereumOperationFactory) + } + } + private func createNonceProvider(for transaction: EthereumTransaction) -> EvmNonceProviderProtocol { if let nonce = transaction.nonce, let value = BigUInt.fromHexString(nonce) { return EvmConstantNonceProvider(value: value) @@ -144,15 +159,14 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { for transaction: EthereumTransaction, service: EvmTransactionServiceProtocol ) { + lastFee = nil + service.estimateFee(createBuilderClosure(for: transaction), runningIn: .main) { [weak self] result in switch result { - case let .success(fee): - let dispatchInfo = RuntimeDispatchInfo( - fee: String(fee), - weight: 0 - ) + case let .success(model): + self?.lastFee = model - self?.presenter?.didReceive(feeResult: .success(dispatchInfo)) + self?.presenter?.didReceive(feeResult: .success(model.fee)) case let .failure(error): self?.presenter?.didReceive(feeResult: .failure(error)) } @@ -161,11 +175,13 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { private func confirmSend( for transaction: EthereumTransaction, + price: EvmTransactionPrice, service: EvmTransactionServiceProtocol, signer: SigningWrapperProtocol ) { service.submit( createBuilderClosure(for: transaction), + price: price, signer: signer, runningIn: .main ) { [weak self] result in @@ -192,11 +208,13 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { private func confirmSign( for transaction: EthereumTransaction, + price: EvmTransactionPrice, service: EvmTransactionServiceProtocol, signer: SigningWrapperProtocol ) { service.sign( createBuilderClosure(for: transaction), + price: price, signer: signer, runningIn: .main ) { [weak self] result in @@ -250,13 +268,21 @@ extension DAppEthereumConfirmInteractor: DAppOperationConfirmInteractorInputProt let transaction = transaction, let ethereumService = ethereumService, let signer = signingWrapper else { + presenter?.didReceive(modelResult: .failure(CommonError.dataCorruption)) return } + guard let feeModel = lastFee else { + presenter?.didReceive(feeResult: .failure(CommonError.dataCorruption)) + return + } + + let txPrice = EvmTransactionPrice(gasLimit: feeModel.gasLimit, gasPrice: feeModel.gasPrice) + if shouldSendTransaction { - confirmSend(for: transaction, service: ethereumService, signer: signer) + confirmSend(for: transaction, price: txPrice, service: ethereumService, signer: signer) } else { - confirmSign(for: transaction, service: ethereumService, signer: signer) + confirmSign(for: transaction, price: txPrice, service: ethereumService, signer: signer) } } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift index 71b68ce0c4..6052c6d388 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift @@ -39,9 +39,7 @@ final class DAppEthereumSignBytesInteractor: DAppOperationBaseInteractor { } private func provideZeroFee() { - let fee = RuntimeDispatchInfo(fee: "0", weight: 0) - - presenter?.didReceive(feeResult: .success(fee)) + presenter?.didReceive(feeResult: .success(0)) presenter?.didReceive(priceResult: .success(nil)) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift index 29f9cd3a08..2e52dcc05a 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift @@ -1,6 +1,7 @@ import Foundation import RobinHood import SubstrateSdk +import BigInt extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputProtocol { func setup() { @@ -96,7 +97,11 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro do { let info = try feeWrapper.targetOperation.extractNoCancellableResultData() - self?.presenter?.didReceive(feeResult: .success(info)) + if let fee = BigUInt(info.fee) { + self?.presenter?.didReceive(feeResult: .success(fee)) + } else { + self?.presenter?.didReceive(feeResult: .failure(CommonError.dataCorruption)) + } } catch { self?.presenter?.didReceive(feeResult: .failure(error)) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift index afa279a5de..0f093398d4 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift @@ -16,7 +16,7 @@ final class DAppOperationConfirmPresenter { let balanceViewModelFactory: BalanceViewModelFactoryProtocol private var confirmationModel: DAppOperationConfirmModel? - private var feeModel: RuntimeDispatchInfo? + private var feeModel: BigUInt? private var priceData: PriceData? init( @@ -55,14 +55,12 @@ final class DAppOperationConfirmPresenter { return } - guard - let fee = BigUInt(feeModel.fee), - let feeDecimal = viewModelFactory.convertBalanceToDecimal(fee) else { + guard let feeDecimal = viewModelFactory.convertBalanceToDecimal(feeModel) else { view?.didReceive(feeViewModel: .loading) return } - if fee > 0 { + if feeModel > 0 { let viewModel = balanceViewModelFactory.balanceFromPrice(feeDecimal, priceData: priceData) .value(for: selectedLocale) view?.didReceive(feeViewModel: .loaded(value: viewModel)) @@ -147,7 +145,7 @@ extension DAppOperationConfirmPresenter: DAppOperationConfirmInteractorOutputPro provideFeeViewModel() } - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { switch feeResult { case let .success(fee): feeModel = fee diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift index 63894971ab..9a059b5dc3 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift @@ -1,4 +1,5 @@ import SubstrateSdk +import BigInt protocol DAppOperationConfirmViewProtocol: ControllerBackedProtocol { func didReceive(confirmationViewModel: DAppOperationConfirmViewModel) @@ -23,7 +24,7 @@ protocol DAppOperationConfirmInteractorInputProtocol: AnyObject { protocol DAppOperationConfirmInteractorOutputProtocol: AnyObject { func didReceive(modelResult: Result) - func didReceive(feeResult: Result) + func didReceive(feeResult: Result) func didReceive(priceResult: Result) func didReceive(responseResult: Result, for request: DAppOperationRequest) func didReceive(txDetailsResult: Result) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift index 30dd9d2eff..f11540a3be 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift @@ -45,9 +45,7 @@ final class DAppSignBytesConfirmInteractor: DAppOperationBaseInteractor { } private func provideZeroFee() { - let fee = RuntimeDispatchInfo(fee: "0", weight: 0) - - presenter?.didReceive(feeResult: .success(fee)) + presenter?.didReceive(feeResult: .success(0)) presenter?.didReceive(priceResult: .success(nil)) } diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift index 0ded056fde..0c3e039483 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift @@ -21,6 +21,7 @@ class EvmOnChainTransferInteractor: OnChainTransferBaseInteractor { let extrinsicService: EvmTransactionServiceProtocol private(set) var transferType: TransferType? + private(set) var lastFeeModel: EvmFeeModel? init( selectedAccount: ChainAccountResponse, @@ -143,6 +144,8 @@ extension EvmOnChainTransferInteractor { let identifier = String(amount.value) + "-" + recepientAccountId.toHex() + "-" + amount.name + lastFeeModel = nil + feeProxy.estimateFee( using: extrinsicService, reuseIdentifier: identifier @@ -188,12 +191,13 @@ extension EvmOnChainTransferInteractor { extension EvmOnChainTransferInteractor: EvmTransactionFeeProxyDelegate { func didReceiveFee( - result: Result, + result: Result, for _: TransactionFeeId ) { switch result { - case let .success(fee): - presenter?.didReceiveFee(result: .success(fee)) + case let .success(model): + lastFeeModel = model + presenter?.didReceiveFee(result: .success(model.fee)) case let .failure(error): presenter?.didReceiveFee(result: .failure(error)) } diff --git a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift index 50e4487793..64f209dbbc 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift @@ -70,7 +70,8 @@ final class TransferEvmOnChainConfirmInteractor: EvmOnChainTransferInteractor { extension TransferEvmOnChainConfirmInteractor: TransferConfirmOnChainInteractorInputProtocol { func submit(amount: OnChainTransferAmount, recepient: AccountAddress, lastFee: BigUInt?) { do { - guard let transferType = transferType else { + guard let transferType = transferType, let lastFeeModel = lastFeeModel else { + presenter?.didReceiveError(CommonError.dataCorruption) return } @@ -97,6 +98,7 @@ extension TransferEvmOnChainConfirmInteractor: TransferConfirmOnChainInteractorI extrinsicService.submit( extrinsicClosure, + price: EvmTransactionPrice(gasLimit: lastFeeModel.gasLimit, gasPrice: lastFeeModel.gasPrice), signer: signingWrapper, runningIn: .main ) { [weak self] result in diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index 0a5804d276..493a213322 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -116,11 +116,6 @@ struct TransferConfirmOnChainViewFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue let operationFactory = EvmWebSocketOperationFactory(connection: connection) - let gasPriceProvider = EvmGasPriceProviderFactory.createMaxPriorityWithLegacyFallback( - operationFactory: operationFactory, - operationQueue: operationQueue, - logger: Logger.shared - ) let gasLimitProvider = EvmGasLimitProviderFactory.createGasLimitProvider( for: asset, @@ -134,7 +129,8 @@ struct TransferConfirmOnChainViewFactory { let extrinsicService = EvmTransactionService( accountId: account.accountId, operationFactory: operationFactory, - gasPriceProvider: gasPriceProvider, + maxPriorityGasPriceProvider: EvmMaxPriorityGasPriceProvider(operationFactory: operationFactory), + defaultGasPriceProvider: EvmLegacyGasPriceProvider(operationFactory: operationFactory), gasLimitProvider: gasLimitProvider, nonceProvider: nonceProvider, chain: chain, @@ -150,8 +146,6 @@ struct TransferConfirmOnChainViewFactory { operationQueue: OperationManagerFacade.sharedDefaultQueue ) - let fallbackGasLimit = EvmFallbackGasLimit.value(for: asset) - return TransferEvmOnChainConfirmInteractor( selectedAccount: account, chain: chain, diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift index 8a9950fd30..c40844bc59 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift @@ -150,12 +150,6 @@ extension TransferSetupPresenterFactory { let operationFactory = EvmWebSocketOperationFactory(connection: connection) - let gasPriceProvider = EvmGasPriceProviderFactory.createMaxPriorityWithLegacyFallback( - operationFactory: operationFactory, - operationQueue: operationQueue, - logger: Logger.shared - ) - let gasLimitProvider = EvmGasLimitProviderFactory.createGasLimitProvider( for: asset, operationFactory: operationFactory, @@ -168,7 +162,8 @@ extension TransferSetupPresenterFactory { let extrinsicService = EvmTransactionService( accountId: selectedAccount.accountId, operationFactory: operationFactory, - gasPriceProvider: gasPriceProvider, + maxPriorityGasPriceProvider: EvmMaxPriorityGasPriceProvider(operationFactory: operationFactory), + defaultGasPriceProvider: EvmLegacyGasPriceProvider(operationFactory: operationFactory), gasLimitProvider: gasLimitProvider, nonceProvider: nonceProvider, chain: chain, diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index 4a2915fa7e..988b3cdc4f 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -11739,6 +11739,7 @@ import SubstrateSdk import Cuckoo @testable import novawallet +import BigInt import SubstrateSdk @@ -12415,9 +12416,9 @@ import SubstrateSdk - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { - return cuckoo_manager.call("didReceive(feeResult: Result)", + return cuckoo_manager.call("didReceive(feeResult: Result)", parameters: (feeResult), escapingParameters: (feeResult), superclassCall: @@ -12487,9 +12488,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(modelResult: Result)", parameterMatchers: matchers)) } - func didReceive(feeResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { - let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(feeResult: Result)", parameterMatchers: matchers)) + func didReceive(feeResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(feeResult: Result)", parameterMatchers: matchers)) } func didReceive(priceResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { @@ -12530,9 +12531,9 @@ import SubstrateSdk } @discardableResult - func didReceive(feeResult: M1) -> Cuckoo.__DoNotUse<(Result), Void> where M1.MatchedType == Result { - let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] - return cuckoo_manager.verify("didReceive(feeResult: Result)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func didReceive(feeResult: M1) -> Cuckoo.__DoNotUse<(Result), Void> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] + return cuckoo_manager.verify("didReceive(feeResult: Result)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -12570,7 +12571,7 @@ import SubstrateSdk - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { return DefaultValueRegistry.defaultValue(for: (Void).self) } From aa44f6cb03d5daa2135797c8e49e6e67ee4c67dd Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 14 Aug 2023 11:28:06 +0300 Subject: [PATCH 12/31] add evm gas price validation logic --- novawallet.xcodeproj/project.pbxproj | 36 ++++++++ novawallet/Common/Model/FeeOutputModel.swift | 7 ++ .../Evm/EvmGasPriceValidation.swift | 70 +++++++++++++++ .../Evm/EvmValidationErrorPresentable.swift | 59 +++++++++++++ .../Evm/EvmValidationProviderFactory.swift | 32 +++++++ .../ExtrinsicValidationProvider.swift | 9 ++ .../DAppEthereumConfirmInteractor.swift | 22 ++++- .../DAppEthereumSignBytesInteractor.swift | 2 +- ...pOperationConfirmInteractor+Protocol.swift | 3 +- .../DAppOperationConfirmPresenter.swift | 35 ++++++-- .../DAppOperationConfirmProtocols.swift | 7 +- .../DAppOperationConfirmViewFactory.swift | 85 ++++++++++++++++--- .../DAppOperationConfirmWireframe.swift | 4 +- .../DAppSignBytesConfirmInteractor.swift | 3 +- .../EvmOnChainTransferInteractor.swift | 8 +- .../OnChain/OnChainTransferInteractor.swift | 4 +- .../OnChain/OnChainTransferPresenter.swift | 25 ++++-- .../TransferEvmOnChainConfirmInteractor.swift | 2 + .../TransferOnChainConfirmPresenter.swift | 7 +- .../TransferConfirmOnChainViewFactory.swift | 55 +++++++----- .../TransferConfirmWireframe.swift | 4 +- .../OnChainTransferSetupPresenter.swift | 9 +- .../OnChainTransferSetupProtocols.swift | 2 +- .../OnChainTransferSetupWireframe.swift | 4 +- ...ransferSetupPresenterFactory+OnChain.swift | 42 ++++++--- novawallet/en.lproj/Localizable.strings | 3 + novawallet/ru.lproj/Localizable.strings | 3 + 27 files changed, 461 insertions(+), 81 deletions(-) create mode 100644 novawallet/Common/Model/FeeOutputModel.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationErrorPresentable.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationProviderFactory.swift create mode 100644 novawallet/Common/Services/ExtrinsicService/Validation/ExtrinsicValidationProvider.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index d97e2b43e0..edc0416d0e 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -67,6 +67,11 @@ 0C3205DC2A89677B002EB914 /* EvmGasLimitProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205DB2A89677B002EB914 /* EvmGasLimitProviderFactory.swift */; }; 0C3205DE2A896C19002EB914 /* EvmFeeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205DD2A896C19002EB914 /* EvmFeeModel.swift */; }; 0C3205E02A896C7E002EB914 /* EvmTransactionPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205DF2A896C7E002EB914 /* EvmTransactionPrice.swift */; }; + 0C3205E32A897B5A002EB914 /* ExtrinsicValidationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205E22A897B5A002EB914 /* ExtrinsicValidationProvider.swift */; }; + 0C3205E62A897BE3002EB914 /* EvmGasPriceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205E52A897BE3002EB914 /* EvmGasPriceValidation.swift */; }; + 0C3205E82A898195002EB914 /* EvmValidationErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205E72A898195002EB914 /* EvmValidationErrorPresentable.swift */; }; + 0C3205EA2A8A0539002EB914 /* EvmValidationProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205E92A8A0539002EB914 /* EvmValidationProviderFactory.swift */; }; + 0C3205EC2A8A122D002EB914 /* FeeOutputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */; }; 0C40520C2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */; }; 0C463FC82A58126A003E71C9 /* UIView+MotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */; }; 0C463FD02A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */; }; @@ -3740,6 +3745,11 @@ 0C3205DB2A89677B002EB914 /* EvmGasLimitProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasLimitProviderFactory.swift; sourceTree = ""; }; 0C3205DD2A896C19002EB914 /* EvmFeeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmFeeModel.swift; sourceTree = ""; }; 0C3205DF2A896C7E002EB914 /* EvmTransactionPrice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmTransactionPrice.swift; sourceTree = ""; }; + 0C3205E22A897B5A002EB914 /* ExtrinsicValidationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicValidationProvider.swift; sourceTree = ""; }; + 0C3205E52A897BE3002EB914 /* EvmGasPriceValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceValidation.swift; sourceTree = ""; }; + 0C3205E72A898195002EB914 /* EvmValidationErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmValidationErrorPresentable.swift; sourceTree = ""; }; + 0C3205E92A8A0539002EB914 /* EvmValidationProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmValidationProviderFactory.swift; sourceTree = ""; }; + 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeOutputModel.swift; sourceTree = ""; }; 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmInteractor.swift; sourceTree = ""; }; 0C40520B2A53DC4100B3E6EC /* OverlayBlurBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBlurBackgroundView.swift; sourceTree = ""; }; 0C432D57ACFA53F42E574CBD /* TokensManageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewController.swift; sourceTree = ""; }; @@ -7592,6 +7602,25 @@ path = NonceProviders; sourceTree = ""; }; + 0C3205E12A897B32002EB914 /* Validation */ = { + isa = PBXGroup; + children = ( + 0C3205E42A897BD5002EB914 /* Evm */, + 0C3205E22A897B5A002EB914 /* ExtrinsicValidationProvider.swift */, + ); + path = Validation; + sourceTree = ""; + }; + 0C3205E42A897BD5002EB914 /* Evm */ = { + isa = PBXGroup; + children = ( + 0C3205E52A897BE3002EB914 /* EvmGasPriceValidation.swift */, + 0C3205E72A898195002EB914 /* EvmValidationErrorPresentable.swift */, + 0C3205E92A8A0539002EB914 /* EvmValidationProviderFactory.swift */, + ); + path = Evm; + sourceTree = ""; + }; 0C463FCE2A592ACD003E71C9 /* Effects */ = { isa = PBXGroup; children = ( @@ -10333,6 +10362,7 @@ 845BB8B625E4464D00E5FCDC /* ExtrinsicService */ = { isa = PBXGroup; children = ( + 0C3205E12A897B32002EB914 /* Validation */, 84A59158292AA65500BCCF8F /* Substrate */, 84A59157292AA64E00BCCF8F /* Evm */, 84A59163292B593E00BCCF8F /* TransactionFeeProxy.swift */, @@ -11747,6 +11777,7 @@ 0CB064672A403ADE00BFBA3F /* AmountDecimal.swift */, 0C17BD9A2A43025E004AF9E7 /* Pagination.swift */, 8455F1A32A1F606B003F072D /* OnchainStorage.swift */, + 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */, ); path = Model; sourceTree = ""; @@ -18635,6 +18666,7 @@ 8476D39D27F44E73004D9A7A /* PhishingSiteVerifier+Init.swift in Sources */, 8442002028E6FDBE00C49C4A /* CrowdloanListViewManager.swift in Sources */, 8471577B2910F0AF00D7D003 /* GovernanceUnlockInteractor.swift in Sources */, + 0C3205E82A898195002EB914 /* EvmValidationErrorPresentable.swift in Sources */, 84CE69E82566750D00559427 /* ByteLengthProcessor.swift in Sources */, 8499FEDA27BFDB8C00712589 /* NFTStreamableSource.swift in Sources */, 849976BE27B269A400B14A6C /* DAppTransports.swift in Sources */, @@ -19173,6 +19205,7 @@ 84B6349D28F4A06D00503306 /* Preimage.swift in Sources */, 8428768524AE046300D91AD8 /* LanguageSelectionProtocols.swift in Sources */, AEA0C8A4267B6B1900F9666F /* SelectedValidatorListProtocols.swift in Sources */, + 0C3205EC2A8A122D002EB914 /* FeeOutputModel.swift in Sources */, 84D911AA292C923D0032EF33 /* Data+Fill.swift in Sources */, F4B39C4E27326E8400BB6E10 /* AcalaContributionSetupViewController.swift in Sources */, 84E63C1728FFC69A0093534A /* DiscreteGradientSlider.swift in Sources */, @@ -19703,6 +19736,7 @@ 8488ECEA258CE456004591CC /* PurchaseViewController.swift in Sources */, 847C9659255362F7002D288F /* RestoreJson.swift in Sources */, 8430D6C52800040A00FFB6AE /* EthereumExecuted.swift in Sources */, + 0C3205EA2A8A0539002EB914 /* EvmValidationProviderFactory.swift in Sources */, 8487583227F06AF300495306 /* AddressScanViewFactory.swift in Sources */, 849632072555CE9D00B8E316 /* ExportSeedData.swift in Sources */, 847EA1D62A1CA47500F1CBD8 /* SubqueryMultistaking.swift in Sources */, @@ -19866,6 +19900,7 @@ 6BAF97802DB9C640515F47C7 /* StakingMainInteractor.swift in Sources */, 84BF12A128CFCD70007AA576 /* ParaStkYieldBoostStartError.swift in Sources */, 8466BB42263F501000E065A8 /* StakingUnbondConfirmViewModel.swift in Sources */, + 0C3205E32A897B5A002EB914 /* ExtrinsicValidationProvider.swift in Sources */, 840D929F278D8D2E0007B979 /* DAppBrowserInteractorError.swift in Sources */, F402BC8B273AD20D0075F803 /* AstarBonusServiceError.swift in Sources */, E5F3DF66415E54AE04D0C9A9 /* StakingMainViewController.swift in Sources */, @@ -20743,6 +20778,7 @@ 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */, 84770F2C291F893200852A33 /* GovernanceUnlockConfirmInitData.swift in Sources */, 84B24FAC2A2F25FF00F9BF59 /* StakingDashboardActiveDetailsView.swift in Sources */, + 0C3205E62A897BE3002EB914 /* EvmGasPriceValidation.swift in Sources */, 84E90BA128D0B51000529633 /* CheckboxControlView.swift in Sources */, 821518375113295E41E0481C /* ParitySignerTxQrViewFactory.swift in Sources */, 843ADAC32A3C30A1003AE2B5 /* Decimal+Fiat.swift in Sources */, diff --git a/novawallet/Common/Model/FeeOutputModel.swift b/novawallet/Common/Model/FeeOutputModel.swift new file mode 100644 index 0000000000..e414008d66 --- /dev/null +++ b/novawallet/Common/Model/FeeOutputModel.swift @@ -0,0 +1,7 @@ +import Foundation +import BigInt + +struct FeeOutputModel { + let value: BigUInt + let validationProvider: ExtrinsicValidationProviderProtocol? +} diff --git a/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift new file mode 100644 index 0000000000..55cad89181 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift @@ -0,0 +1,70 @@ +import Foundation +import BigInt + +final class EvmGasPriceValidationProvider { + let model: EvmFeeModel + let multiplier: BigUInt + let divisor: BigUInt + let presentable: EvmValidationErrorPresentable + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let assetInfo: AssetBalanceDisplayInfo + + init( + presentable: EvmValidationErrorPresentable, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + assetInfo: AssetBalanceDisplayInfo, + model: EvmFeeModel, + multiplier: BigUInt = 3, + divisor: BigUInt = 2 + ) { + self.presentable = presentable + self.balanceViewModelFactory = balanceViewModelFactory + self.assetInfo = assetInfo + self.model = model + self.multiplier = multiplier + self.divisor = divisor + } +} + +extension EvmGasPriceValidationProvider: ExtrinsicValidationProviderProtocol { + func getValidations( + for view: ControllerBackedProtocol?, + onRefresh: @escaping () -> Void, + locale: Locale + ) -> DataValidating? { + guard let maxPriorityPrice = model.maxPriorityGasPrice else { + return nil + } + + let factory = balanceViewModelFactory + + let maxPriorityFee = maxPriorityPrice * model.gasLimit + let defaultFee = model.defaultGasPrice * model.gasLimit + let precision = UInt16(bitPattern: assetInfo.assetPrecision) + + return WarningConditionViolation(onWarning: { [weak self] delegate in + let maxPriorityDecimal = maxPriorityFee.decimal(precision: precision) + let maxPriorityString = factory.amountFromValue(maxPriorityDecimal).value(for: locale) + + let defaultDecimal = defaultFee.decimal(precision: precision) + let defaultString = factory.amountFromValue(defaultDecimal).value(for: locale) + + self?.presentable.presentFeeToHigh( + for: view, + params: .init(maxPriorityFee: maxPriorityString, defaultFee: defaultString), + onRefresh: onRefresh, + onProceed: { + delegate.didCompleteWarningHandling() + }, + locale: locale + ) + + }, preservesCondition: { [weak self] in + guard let self = self else { + return true + } + + return self.model.defaultGasPrice * self.multiplier < maxPriorityPrice * self.divisor + }) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationErrorPresentable.swift b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationErrorPresentable.swift new file mode 100644 index 0000000000..088c1a57c6 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationErrorPresentable.swift @@ -0,0 +1,59 @@ +import Foundation + +struct EvmGasValidatedParams { + let maxPriorityFee: String + let defaultFee: String +} + +protocol EvmValidationErrorPresentable { + func presentFeeToHigh( + for view: ControllerBackedProtocol?, + params: EvmGasValidatedParams, + onRefresh: @escaping () -> Void, + onProceed: @escaping () -> Void, + locale: Locale + ) +} + +extension EvmValidationErrorPresentable where Self: AlertPresentable { + func presentFeeToHigh( + for view: ControllerBackedProtocol?, + params: EvmGasValidatedParams, + onRefresh: @escaping () -> Void, + onProceed: @escaping () -> Void, + locale: Locale + ) { + let title = R.string.localizable.evmTransactionFeeTooHighTitle( + preferredLanguages: locale.rLanguages + ) + + let message = R.string.localizable.evmTransactionFeeTooHighMessage( + params.maxPriorityFee, + params.defaultFee, + preferredLanguages: locale.rLanguages + ) + + let refreshAction = AlertPresentableAction( + title: R.string.localizable.commonRefreshFee(preferredLanguages: locale.rLanguages), + style: .normal + ) { + onRefresh() + } + + let proceedAction = AlertPresentableAction( + title: R.string.localizable.commonProceed(preferredLanguages: locale.rLanguages), + style: .destructive + ) { + onProceed() + } + + let model = AlertPresentableViewModel( + title: title, + message: message, + actions: [refreshAction, proceedAction], + closeAction: nil + ) + + present(viewModel: model, style: .alert, from: view) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationProviderFactory.swift b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationProviderFactory.swift new file mode 100644 index 0000000000..8ec512e53d --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmValidationProviderFactory.swift @@ -0,0 +1,32 @@ +import Foundation + +protocol EvmValidationProviderFactoryProtocol { + func createGasPriceValidation(for model: EvmFeeModel) -> ExtrinsicValidationProviderProtocol +} + +final class EvmValidationProviderFactory { + let presentable: EvmValidationErrorPresentable + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let assetInfo: AssetBalanceDisplayInfo + + init( + presentable: EvmValidationErrorPresentable, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + assetInfo: AssetBalanceDisplayInfo + ) { + self.presentable = presentable + self.balanceViewModelFactory = balanceViewModelFactory + self.assetInfo = assetInfo + } +} + +extension EvmValidationProviderFactory: EvmValidationProviderFactoryProtocol { + func createGasPriceValidation(for model: EvmFeeModel) -> ExtrinsicValidationProviderProtocol { + EvmGasPriceValidationProvider( + presentable: presentable, + balanceViewModelFactory: balanceViewModelFactory, + assetInfo: assetInfo, + model: model + ) + } +} diff --git a/novawallet/Common/Services/ExtrinsicService/Validation/ExtrinsicValidationProvider.swift b/novawallet/Common/Services/ExtrinsicService/Validation/ExtrinsicValidationProvider.swift new file mode 100644 index 0000000000..1c15d680b8 --- /dev/null +++ b/novawallet/Common/Services/ExtrinsicService/Validation/ExtrinsicValidationProvider.swift @@ -0,0 +1,9 @@ +import Foundation + +protocol ExtrinsicValidationProviderProtocol { + func getValidations( + for view: ControllerBackedProtocol?, + onRefresh: @escaping () -> Void, + locale: Locale + ) -> DataValidating? +} diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift index 8ed3c21b8f..da93abdad3 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift @@ -7,6 +7,7 @@ import SubstrateSdk final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { let request: DAppOperationRequest let ethereumOperationFactory: EthereumOperationFactoryProtocol + let validationProviderFactory: EvmValidationProviderFactoryProtocol let operationQueue: OperationQueue let signingWrapperFactory: SigningWrapperFactoryProtocol let shouldSendTransaction: Bool @@ -21,6 +22,7 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { chainId: String, request: DAppOperationRequest, ethereumOperationFactory: EthereumOperationFactoryProtocol, + validationProviderFactory: EvmValidationProviderFactoryProtocol, operationQueue: OperationQueue, signingWrapperFactory: SigningWrapperFactoryProtocol, shouldSendTransaction: Bool @@ -28,6 +30,7 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { self.chainId = chainId self.request = request self.ethereumOperationFactory = ethereumOperationFactory + self.validationProviderFactory = validationProviderFactory self.operationQueue = operationQueue self.signingWrapperFactory = signingWrapperFactory self.shouldSendTransaction = shouldSendTransaction @@ -157,7 +160,8 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { private func provideFeeModel( for transaction: EthereumTransaction, - service: EvmTransactionServiceProtocol + service: EvmTransactionServiceProtocol, + validationProviderFactory: EvmValidationProviderFactoryProtocol ) { lastFee = nil @@ -165,8 +169,10 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { switch result { case let .success(model): self?.lastFee = model + let validationProvider = validationProviderFactory.createGasPriceValidation(for: model) + let feeModel = DAppOperationConfirmFee(value: model.fee, validationProvider: validationProvider) - self?.presenter?.didReceive(feeResult: .success(model.fee)) + self?.presenter?.didReceive(feeResult: .success(feeModel)) case let .failure(error): self?.presenter?.didReceive(feeResult: .failure(error)) } @@ -250,7 +256,11 @@ extension DAppEthereumConfirmInteractor: DAppOperationConfirmInteractorInputProt } provideConfirmationModel(for: transaction) - provideFeeModel(for: transaction, service: ethereumService) + provideFeeModel( + for: transaction, + service: ethereumService, + validationProviderFactory: validationProviderFactory + ) } func estimateFee() { @@ -260,7 +270,11 @@ extension DAppEthereumConfirmInteractor: DAppOperationConfirmInteractorInputProt return } - provideFeeModel(for: transaction, service: ethereumService) + provideFeeModel( + for: transaction, + service: ethereumService, + validationProviderFactory: validationProviderFactory + ) } func confirm() { diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift index 6052c6d388..aa15eab26a 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift @@ -39,7 +39,7 @@ final class DAppEthereumSignBytesInteractor: DAppOperationBaseInteractor { } private func provideZeroFee() { - presenter?.didReceive(feeResult: .success(0)) + presenter?.didReceive(feeResult: .success(.init(value: 0, validationProvider: nil))) presenter?.didReceive(priceResult: .success(nil)) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift index 2e52dcc05a..e96460e949 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift @@ -98,7 +98,8 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro do { let info = try feeWrapper.targetOperation.extractNoCancellableResultData() if let fee = BigUInt(info.fee) { - self?.presenter?.didReceive(feeResult: .success(fee)) + let feeModel = DAppOperationConfirmFee(value: fee, validationProvider: nil) + self?.presenter?.didReceive(feeResult: .success(feeModel)) } else { self?.presenter?.didReceive(feeResult: .failure(CommonError.dataCorruption)) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift index 0f093398d4..cf87953840 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift @@ -16,7 +16,7 @@ final class DAppOperationConfirmPresenter { let balanceViewModelFactory: BalanceViewModelFactoryProtocol private var confirmationModel: DAppOperationConfirmModel? - private var feeModel: BigUInt? + private var feeModel: DAppOperationConfirmFee? private var priceData: PriceData? init( @@ -49,18 +49,25 @@ final class DAppOperationConfirmPresenter { view?.didReceive(confirmationViewModel: viewModel) } + private func refreshFee() { + feeModel = nil + provideFeeViewModel() + + interactor.estimateFee() + } + private func provideFeeViewModel() { guard let feeModel = feeModel else { view?.didReceive(feeViewModel: .loading) return } - guard let feeDecimal = viewModelFactory.convertBalanceToDecimal(feeModel) else { + guard let feeDecimal = viewModelFactory.convertBalanceToDecimal(feeModel.value) else { view?.didReceive(feeViewModel: .loading) return } - if feeModel > 0 { + if feeModel.value > 0 { let viewModel = balanceViewModelFactory.balanceFromPrice(feeDecimal, priceData: priceData) .value(for: selectedLocale) view?.didReceive(feeViewModel: .loaded(value: viewModel)) @@ -73,7 +80,7 @@ final class DAppOperationConfirmPresenter { let rejectAction = AlertPresentableAction( title: R.string.localizable.commonReject(preferredLanguages: selectedLocale.rLanguages) ) { [weak self] in - self?.interactor.reject() + self?.refreshFee() } let errorContent = error.toErrorContent(for: selectedLocale) @@ -98,7 +105,21 @@ extension DAppOperationConfirmPresenter: DAppOperationConfirmPresenterProtocol { } func confirm() { - interactor.confirm() + let optValidation = feeModel?.validationProvider?.getValidations( + for: view, + onRefresh: { [weak self] in + self?.interactor.estimateFee() + }, + locale: selectedLocale + ) + + if let validation = optValidation { + DataValidationRunner(validators: [validation]).runValidation { [weak self] in + self?.interactor.confirm() + } + } else { + interactor.confirm() + } } func reject() { @@ -145,7 +166,7 @@ extension DAppOperationConfirmPresenter: DAppOperationConfirmInteractorOutputPro provideFeeViewModel() } - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { switch feeResult { case let .success(fee): feeModel = fee @@ -156,7 +177,7 @@ extension DAppOperationConfirmPresenter: DAppOperationConfirmInteractorOutputPro on: view, locale: selectedLocale ) { [weak self] in - self?.interactor.estimateFee() + self?.refreshFee() } } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift index 9a059b5dc3..14e8619ef7 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift @@ -22,9 +22,14 @@ protocol DAppOperationConfirmInteractorInputProtocol: AnyObject { func prepareTxDetails() } +struct DAppOperationConfirmFee { + let value: BigUInt + let validationProvider: ExtrinsicValidationProviderProtocol? +} + protocol DAppOperationConfirmInteractorOutputProtocol: AnyObject { func didReceive(modelResult: Result) - func didReceive(feeResult: Result) + func didReceive(feeResult: Result) func didReceive(priceResult: Result) func didReceive(responseResult: Result, for request: DAppOperationRequest) func didReceive(txDetailsResult: Result) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift index 1c000f99ff..bb93d1739f 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift @@ -13,33 +13,88 @@ struct DAppOperationConfirmViewFactory { switch type { case let .extrinsic(chain): let interactor = createExtrinsicInteractor(for: request, chain: chain) - return createView(for: interactor, chain: .left(chain), delegate: delegate) + return createGenericView(for: interactor, chain: .left(chain), delegate: delegate) case let .bytes(chain): let interactor = createSignBytesInteractor(for: request, chain: chain) - return createView(for: interactor, chain: .left(chain), delegate: delegate) + return createGenericView(for: interactor, chain: .left(chain), delegate: delegate) case let .ethereumSendTransaction(chain): - let interactor = createEthereumInteractor( - for: request, - chain: chain, + return createEvmTransactionView( + for: chain, + request: request, + delegate: delegate, shouldSendTransaction: true ) - - return createView(for: interactor, chain: chain, delegate: delegate) case let .ethereumSignTransaction(chain): - let interactor = createEthereumInteractor( - for: request, - chain: chain, + return createEvmTransactionView( + for: chain, + request: request, + delegate: delegate, shouldSendTransaction: false ) - - return createView(for: interactor, chain: chain, delegate: delegate) case let .ethereumBytes(chain): let interactor = createEthereumPersonalSignInteractor(for: request) - return createView(for: interactor, chain: chain, delegate: delegate) + return createGenericView(for: interactor, chain: chain, delegate: delegate) + } + } + + private static func createEvmTransactionView( + for chain: DAppEitherChain, + request: DAppOperationRequest, + delegate: DAppOperationConfirmDelegate, + shouldSendTransaction: Bool + ) -> DAppOperationConfirmViewProtocol? { + guard + let assetInfo = chain.utilityAssetBalanceInfo, + let currencyManager = CurrencyManager.shared else { + return nil } + + let wireframe = DAppOperationEvmConfirmWireframe() + let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: assetInfo, + priceAssetInfoFactory: priceAssetInfoFactory + ) + + let validationProviderFactory = EvmValidationProviderFactory( + presentable: wireframe, + balanceViewModelFactory: balanceViewModelFactory, + assetInfo: assetInfo + ) + + guard + let interactor = createEthereumInteractor( + for: request, + chain: chain, + validationProviderFactory: validationProviderFactory, + shouldSendTransaction: shouldSendTransaction + ) else { + return nil + } + + let presenter = DAppOperationConfirmPresenter( + interactor: interactor, + wireframe: wireframe, + delegate: delegate, + viewModelFactory: DAppOperationConfirmViewModelFactory(chain: chain), + balanceViewModelFactory: balanceViewModelFactory, + chain: chain, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = DAppOperationConfirmViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return view } - private static func createView( + private static func createGenericView( for interactor: (DAppOperationBaseInteractor & DAppOperationConfirmInteractorInputProtocol)?, chain: DAppEitherChain, delegate: DAppOperationConfirmDelegate @@ -119,6 +174,7 @@ struct DAppOperationConfirmViewFactory { private static func createEthereumInteractor( for request: DAppOperationRequest, chain: Either, + validationProviderFactory: EvmValidationProviderFactoryProtocol, shouldSendTransaction: Bool ) -> DAppEthereumConfirmInteractor? { let operationFactory: EthereumOperationFactoryProtocol @@ -151,6 +207,7 @@ struct DAppOperationConfirmViewFactory { chainId: chainId, request: request, ethereumOperationFactory: operationFactory, + validationProviderFactory: validationProviderFactory, operationQueue: OperationManagerFacade.sharedDefaultQueue, signingWrapperFactory: SigningWrapperFactory(keystore: Keychain()), shouldSendTransaction: shouldSendTransaction diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmWireframe.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmWireframe.swift index 5cab25dee1..a9df676c0e 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmWireframe.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmWireframe.swift @@ -1,7 +1,7 @@ import Foundation import SubstrateSdk -final class DAppOperationConfirmWireframe: DAppOperationConfirmWireframeProtocol { +class DAppOperationConfirmWireframe: DAppOperationConfirmWireframeProtocol { func close(view: DAppOperationConfirmViewProtocol?) { view?.controller.presentingViewController?.dismiss(animated: true, completion: nil) } @@ -15,3 +15,5 @@ final class DAppOperationConfirmWireframe: DAppOperationConfirmWireframeProtocol view?.controller.present(navigationController, animated: true, completion: nil) } } + +final class DAppOperationEvmConfirmWireframe: DAppOperationConfirmWireframe, EvmValidationErrorPresentable {} diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift index f11540a3be..5ede006676 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift @@ -45,7 +45,8 @@ final class DAppSignBytesConfirmInteractor: DAppOperationBaseInteractor { } private func provideZeroFee() { - presenter?.didReceive(feeResult: .success(0)) + let feeModel = DAppOperationConfirmFee(value: 0, validationProvider: nil) + presenter?.didReceive(feeResult: .success(feeModel)) presenter?.didReceive(priceResult: .success(nil)) } diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift index 0c3e039483..6cb642867a 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/EvmOnChainTransferInteractor.swift @@ -19,6 +19,7 @@ class EvmOnChainTransferInteractor: OnChainTransferBaseInteractor { let feeProxy: EvmTransactionFeeProxyProtocol let extrinsicService: EvmTransactionServiceProtocol + let validationProviderFactory: EvmValidationProviderFactoryProtocol private(set) var transferType: TransferType? private(set) var lastFeeModel: EvmFeeModel? @@ -29,6 +30,7 @@ class EvmOnChainTransferInteractor: OnChainTransferBaseInteractor { asset: AssetModel, feeProxy: EvmTransactionFeeProxyProtocol, extrinsicService: EvmTransactionServiceProtocol, + validationProviderFactory: EvmValidationProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, currencyManager: CurrencyManagerProtocol, @@ -36,6 +38,7 @@ class EvmOnChainTransferInteractor: OnChainTransferBaseInteractor { ) { self.feeProxy = feeProxy self.extrinsicService = extrinsicService + self.validationProviderFactory = validationProviderFactory super.init( selectedAccount: selectedAccount, @@ -197,7 +200,10 @@ extension EvmOnChainTransferInteractor: EvmTransactionFeeProxyDelegate { switch result { case let .success(model): lastFeeModel = model - presenter?.didReceiveFee(result: .success(model.fee)) + + let validationProvider = validationProviderFactory.createGasPriceValidation(for: model) + let feeModel = FeeOutputModel(value: model.fee, validationProvider: validationProvider) + presenter?.didReceiveFee(result: .success(feeModel)) case let .failure(error): presenter?.didReceiveFee(result: .failure(error)) } diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift index 4295dfdff1..545fd8baec 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift @@ -560,7 +560,9 @@ extension OnChainTransferInteractor: ExtrinsicFeeProxyDelegate { switch result { case let .success(info): let fee = BigUInt(info.fee) ?? 0 - presenter?.didReceiveFee(result: .success(fee)) + + let feeModel = FeeOutputModel(value: fee, validationProvider: nil) + presenter?.didReceiveFee(result: .success(feeModel)) case let .failure(error): presenter?.didReceiveFee(result: .failure(error)) } diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift index 3c4ab0c11d..9e1628cbf2 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferPresenter.swift @@ -30,7 +30,7 @@ class OnChainTransferPresenter { private(set) lazy var iconGenerator = PolkadotIconGenerator() - private(set) var fee: BigUInt? + private(set) var fee: FeeOutputModel? let networkViewModelFactory: NetworkViewModelFactoryProtocol let sendingBalanceViewModelFactory: BalanceViewModelFactoryProtocol @@ -70,7 +70,7 @@ class OnChainTransferPresenter { fatalError("Child classes must implement this method") } - func updateFee(_ newValue: BigUInt?) { + func updateFee(_ newValue: FeeOutputModel?) { fee = newValue } @@ -83,6 +83,7 @@ class OnChainTransferPresenter { for sendingAmount: Decimal?, recepientAddress: AccountAddress?, utilityAssetInfo: AssetBalanceDisplayInfo, + view: ControllerBackedProtocol?, selectedLocale: Locale ) -> [DataValidating] { var validators: [DataValidating] = [ @@ -99,7 +100,7 @@ class OnChainTransferPresenter { locale: selectedLocale ), - dataValidatingFactory.has(fee: fee, locale: selectedLocale) { [weak self] in + dataValidatingFactory.has(fee: fee?.value, locale: selectedLocale) { [weak self] in self?.refreshFee() return }, @@ -113,14 +114,14 @@ class OnChainTransferPresenter { dataValidatingFactory.canPayFeeSpendingAmountInPlank( balance: senderUtilityAssetTransferable, - fee: fee, + fee: fee?.value, spendingAmount: isUtilityTransfer ? sendingAmount : nil, asset: utilityAssetInfo, locale: selectedLocale ), dataValidatingFactory.notViolatingMinBalancePaying( - fee: fee, + fee: fee?.value, total: senderUtilityAssetTotal, minBalance: isUtilityTransfer ? sendingAssetExistence?.minBalance : utilityAssetMinBalance, locale: selectedLocale @@ -150,6 +151,18 @@ class OnChainTransferPresenter { validators.append(accountProviderValidation) } + let optFeeValidation = fee?.validationProvider?.getValidations( + for: view, + onRefresh: { [weak self] in + self?.refreshFee() + }, + locale: selectedLocale + ) + + if let feeValidation = optFeeValidation { + validators.append(feeValidation) + } + return validators } @@ -169,7 +182,7 @@ class OnChainTransferPresenter { recepientUtilityAssetBalance = balance } - func didReceiveFee(result: Result) { + func didReceiveFee(result: Result) { switch result { case let .success(fee): self.fee = fee diff --git a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift index 64f209dbbc..1deb5dc850 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferEvmOnChainConfirmInteractor.swift @@ -17,6 +17,7 @@ final class TransferEvmOnChainConfirmInteractor: EvmOnChainTransferInteractor { asset: AssetModel, feeProxy: EvmTransactionFeeProxyProtocol, extrinsicService: EvmTransactionServiceProtocol, + validationProviderFactory: EvmValidationProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, signingWrapper: SigningWrapperProtocol, @@ -35,6 +36,7 @@ final class TransferEvmOnChainConfirmInteractor: EvmOnChainTransferInteractor { asset: asset, feeProxy: feeProxy, extrinsicService: extrinsicService, + validationProviderFactory: validationProviderFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, currencyManager: currencyManager, diff --git a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift index 280a69a1fe..a73b54071f 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift @@ -88,7 +88,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { let optAssetInfo = chainAsset.chain.utilityAssets().first?.displayInfo if let fee = fee, let assetInfo = optAssetInfo { let feeDecimal = Decimal.fromSubstrateAmount( - fee, + fee.value, precision: assetInfo.assetPrecision ) ?? 0.0 @@ -144,7 +144,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { } } - override func didReceiveFee(result: Result) { + override func didReceiveFee(result: Result) { super.didReceiveFee(result: result) if case .success = result { @@ -215,6 +215,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { for: amount.value, recepientAddress: recepientAccountAddress, utilityAssetInfo: utilityAssetInfo, + view: view, selectedLocale: selectedLocale ) @@ -228,7 +229,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { strongSelf.interactor.submit( amount: amountInPlank, recepient: strongSelf.recepientAccountAddress, - lastFee: strongSelf.fee + lastFee: strongSelf.fee?.value ) } } diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index 493a213322..6d2a3da3c0 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -16,16 +16,12 @@ struct TransferConfirmOnChainViewFactory { let wallet = walletSettings.value, let selectedAccount = wallet.fetch(for: chainAsset.chain.accountRequest()), let senderAccountAddress = selectedAccount.toAddress(), - let currencyManager = CurrencyManager.shared, - let interactor = createInteractor( - for: chainAsset, - account: selectedAccount, - accountMetaId: wallet.metaId - ) else { + let currencyManager = CurrencyManager.shared else { return nil } - let wireframe = TransferConfirmWireframe() + let optInteractor: (OnChainTransferBaseInteractor & TransferConfirmOnChainInteractorInputProtocol)? + let wireframe: TransferConfirmWireframeProtocol let localizationManager = LocalizationManager.shared @@ -50,6 +46,35 @@ struct TransferConfirmOnChainViewFactory { utilityBalanceViewModelFactory = nil } + if chainAsset.asset.isAnyEvm { + let evmWireframe = EvmTransferConfirmWireframe() + wireframe = evmWireframe + + let assetInfo = chainAsset.chain.utilityAssetDisplayInfo() ?? chainAsset.assetDisplayInfo + let validationProviderFactory = EvmValidationProviderFactory( + presentable: evmWireframe, + balanceViewModelFactory: utilityBalanceViewModelFactory ?? sendingBalanceViewModelFactory, + assetInfo: assetInfo + ) + + optInteractor = createEvmInteractor( + for: chainAsset, + account: selectedAccount, + validationProviderFactory: validationProviderFactory + ) + } else { + wireframe = TransferConfirmWireframe() + optInteractor = createSubstrateInteractor( + for: chainAsset, + account: selectedAccount, + accountMetaId: wallet.metaId + ) + } + + guard let interactor = optInteractor else { + return nil + } + let dataValidatingFactory = TransferDataValidatorFactory( presentable: wireframe, assetDisplayInfo: chainAsset.assetDisplayInfo, @@ -86,21 +111,10 @@ struct TransferConfirmOnChainViewFactory { return view } - private static func createInteractor( - for chainAsset: ChainAsset, - account: ChainAccountResponse, - accountMetaId: String - ) -> (OnChainTransferBaseInteractor & TransferConfirmOnChainInteractorInputProtocol)? { - if chainAsset.asset.isAnyEvm { - return createEvmInteractor(for: chainAsset, account: account) - } else { - return createSubstrateInteractor(for: chainAsset, account: account, accountMetaId: accountMetaId) - } - } - private static func createEvmInteractor( for chainAsset: ChainAsset, - account: ChainAccountResponse + account: ChainAccountResponse, + validationProviderFactory: EvmValidationProviderFactoryProtocol ) -> TransferEvmOnChainConfirmInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry let chain = chainAsset.chain @@ -152,6 +166,7 @@ struct TransferConfirmOnChainViewFactory { asset: asset, feeProxy: EvmTransactionFeeProxy(), extrinsicService: extrinsicService, + validationProviderFactory: validationProviderFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, signingWrapper: signingWrapper, diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmWireframe.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmWireframe.swift index e0177d541c..8e6c7098b4 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmWireframe.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmWireframe.swift @@ -1,6 +1,6 @@ import Foundation -final class TransferConfirmWireframe: TransferConfirmWireframeProtocol, ModalAlertPresenting { +class TransferConfirmWireframe: TransferConfirmWireframeProtocol, ModalAlertPresenting { func complete(on view: TransferConfirmCommonViewProtocol?, locale: Locale, completion: @escaping () -> Void) { let title = R.string.localizable .commonTransactionSubmitted(preferredLanguages: locale.rLanguages) @@ -13,3 +13,5 @@ final class TransferConfirmWireframe: TransferConfirmWireframeProtocol, ModalAle } } } + +final class EvmTransferConfirmWireframe: TransferConfirmWireframe, EvmValidationErrorPresentable {} diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift index 291509049e..9b0b533c4e 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift @@ -106,7 +106,7 @@ final class OnChainTransferSetupPresenter: OnChainTransferPresenter, OnChainTran let optAssetInfo = chainAsset.chain.utilityAssets().first?.displayInfo if let fee = fee, let assetInfo = optAssetInfo { let feeDecimal = Decimal.fromSubstrateAmount( - fee, + fee.value, precision: assetInfo.assetPrecision ) ?? 0.0 @@ -160,7 +160,7 @@ final class OnChainTransferSetupPresenter: OnChainTransferPresenter, OnChainTran private func balanceMinusFee() -> Decimal { let balanceValue = senderSendingAssetBalance?.transferable ?? 0 - let feeValue = isUtilityTransfer ? (fee ?? 0) : 0 + let feeValue = isUtilityTransfer ? (fee?.value ?? 0) : 0 let precision = chainAsset.assetDisplayInfo.assetPrecision @@ -231,7 +231,7 @@ final class OnChainTransferSetupPresenter: OnChainTransferPresenter, OnChainTran updateTransferableBalance() } - override func didReceiveFee(result: Result) { + override func didReceiveFee(result: Result) { super.didReceiveFee(result: result) if case .success = result { @@ -325,13 +325,14 @@ extension OnChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { for: sendingAmount, recepientAddress: partialRecepientAddress, utilityAssetInfo: utilityAssetInfo, + view: view, selectedLocale: selectedLocale ) validators.append( dataValidatingFactory.willBeReaped( amount: sendingAmount, - fee: isUtilityTransfer ? fee : 0, + fee: isUtilityTransfer ? fee?.value : 0, totalAmount: senderSendingAssetBalance?.totalInPlank, minBalance: sendingAssetExistence?.minBalance, locale: selectedLocale diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift index 5f1c8647d5..df577c0414 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift @@ -12,7 +12,7 @@ protocol OnChainTransferSetupInteractorOutputProtocol: AnyObject { func didReceiveUtilityAssetSenderBalance(_ balance: AssetBalance) func didReceiveSendingAssetRecepientBalance(_ balance: AssetBalance) func didReceiveUtilityAssetRecepientBalance(_ balance: AssetBalance) - func didReceiveFee(result: Result) + func didReceiveFee(result: Result) func didReceiveSendingAssetPrice(_ price: PriceData?) func didReceiveUtilityAssetPrice(_ price: PriceData?) func didReceiveUtilityAssetMinBalance(_ value: BigUInt) diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift index 98db6bcd87..0cd9c8b8c7 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift @@ -1,6 +1,6 @@ import Foundation -final class OnChainTransferSetupWireframe: OnChainTransferSetupWireframeProtocol { +class OnChainTransferSetupWireframe: OnChainTransferSetupWireframeProtocol { let transferCompletion: TransferCompletionClosure? init(transferCompletion: TransferCompletionClosure?) { @@ -29,3 +29,5 @@ final class OnChainTransferSetupWireframe: OnChainTransferSetupWireframeProtocol navigationController.pushViewController(confirmView.controller, animated: true) } } + +final class EvmOnChainTransferSetupWireframe: OnChainTransferSetupWireframe, EvmValidationErrorPresentable {} diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift index c40844bc59..4fd69e0f9d 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/TransferSetupPresenterFactory+OnChain.swift @@ -10,12 +10,13 @@ extension TransferSetupPresenterFactory { ) -> TransferSetupChildPresenterProtocol? { guard let selectedAccountAddress = wallet.fetch(for: chainAsset.chain.accountRequest())?.toAddress(), - let interactor = createInteractor(for: chainAsset), let currencyManager = CurrencyManager.shared else { return nil } - let wireframe = OnChainTransferSetupWireframe(transferCompletion: transferCompletion) + let optInteractor: (OnChainTransferBaseInteractor & OnChainTransferSetupInteractorInputProtocol)? + let wireframe: OnChainTransferSetupWireframeProtocol + let localizationManager = LocalizationManager.shared let networkViewModelFactory = NetworkViewModelFactory() @@ -40,6 +41,27 @@ extension TransferSetupPresenterFactory { utilityBalanceViewModelFactory = nil } + if chainAsset.asset.isAnyEvm { + let evmWireframe = EvmOnChainTransferSetupWireframe(transferCompletion: transferCompletion) + wireframe = evmWireframe + + let assetInfo = chainAsset.chain.utilityAssetDisplayInfo() ?? chainAsset.assetDisplayInfo + let validationProviderFactory = EvmValidationProviderFactory( + presentable: evmWireframe, + balanceViewModelFactory: utilityBalanceViewModelFactory ?? sendingBalanceViewModelFactory, + assetInfo: assetInfo + ) + + optInteractor = createEvmInteractor(for: chainAsset, validationProviderFactory: validationProviderFactory) + } else { + wireframe = OnChainTransferSetupWireframe(transferCompletion: transferCompletion) + optInteractor = createSubstrateInteractor(for: chainAsset) + } + + guard let interactor = optInteractor else { + return nil + } + let dataValidatingFactory = TransferDataValidatorFactory( presentable: wireframe, assetDisplayInfo: chainAsset.assetDisplayInfo, @@ -78,16 +100,6 @@ extension TransferSetupPresenterFactory { return presenter } - private func createInteractor( - for chainAsset: ChainAsset - ) -> (OnChainTransferBaseInteractor & OnChainTransferSetupInteractorInputProtocol)? { - if chainAsset.asset.isAnyEvm { - return createEvmInteractor(for: chainAsset) - } else { - return createSubstrateInteractor(for: chainAsset) - } - } - private func createSubstrateInteractor(for chainAsset: ChainAsset) -> OnChainTransferSetupInteractor? { let chain = chainAsset.chain let asset = chainAsset.asset @@ -135,7 +147,10 @@ extension TransferSetupPresenterFactory { ) } - private func createEvmInteractor(for chainAsset: ChainAsset) -> EvmOnChainTransferSetupInteractor? { + private func createEvmInteractor( + for chainAsset: ChainAsset, + validationProviderFactory: EvmValidationProviderFactoryProtocol + ) -> EvmOnChainTransferSetupInteractor? { let chain = chainAsset.chain let asset = chainAsset.asset @@ -176,6 +191,7 @@ extension TransferSetupPresenterFactory { asset: asset, feeProxy: EvmTransactionFeeProxy(), extrinsicService: extrinsicService, + validationProviderFactory: validationProviderFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, currencyManager: currencyManager, diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 1b36c9b7a7..86e5bc3677 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1289,3 +1289,6 @@ "asset.operation.send.title" = "Send"; "asset.operation.receive.title" = "Receive"; "asset.operation.buy.title" = "Buy"; +"evm.transaction.fee.too.high.title" = "Network fee too high"; +"evm.transaction.fee.too.high.message" = "The estimated network fee​ (%@) is much higher than the default network fee (%@). This might be due to temporary network congestion. You can refresh to wait for a lower network fee."; +"common.refresh.fee" = "Refresh fee"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 6ceadd1e9e..656bc868e8 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1289,3 +1289,6 @@ "asset.operation.send.title" = "Отправить"; "asset.operation.receive.title" = "Получить"; "asset.operation.buy.title" = "Купить"; +"evm.transaction.fee.too.high.title" = "Комиссия сети слишком большая"; +"evm.transaction.fee.too.high.message" = "Подсчитанная комиссия сети​ (%@) получилась намного чем комиссия сети по умолчанию (%@). Это может быть из-за временной нагрузки на сеть. Вы можете обновить комиссию для получения меньшего значения."; +"common.refresh.fee" = "Обновить"; From faf404c49feae4820c81c6d9b0538b9b5cd6ac25 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 14 Aug 2023 11:39:11 +0300 Subject: [PATCH 13/31] fix mocks --- novawalletTests/Mocks/ModuleMocks.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index 988b3cdc4f..6e8153cae1 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -12416,9 +12416,9 @@ import SubstrateSdk - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { - return cuckoo_manager.call("didReceive(feeResult: Result)", + return cuckoo_manager.call("didReceive(feeResult: Result)", parameters: (feeResult), escapingParameters: (feeResult), superclassCall: @@ -12488,9 +12488,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(modelResult: Result)", parameterMatchers: matchers)) } - func didReceive(feeResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { - let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(feeResult: Result)", parameterMatchers: matchers)) + func didReceive(feeResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(feeResult: Result)", parameterMatchers: matchers)) } func didReceive(priceResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { @@ -12531,9 +12531,9 @@ import SubstrateSdk } @discardableResult - func didReceive(feeResult: M1) -> Cuckoo.__DoNotUse<(Result), Void> where M1.MatchedType == Result { - let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] - return cuckoo_manager.verify("didReceive(feeResult: Result)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func didReceive(feeResult: M1) -> Cuckoo.__DoNotUse<(Result), Void> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] + return cuckoo_manager.verify("didReceive(feeResult: Result)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -12571,7 +12571,7 @@ import SubstrateSdk - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { return DefaultValueRegistry.defaultValue(for: (Void).self) } From 55196ecb6f93a4a7eb3817ff701858da31ddb16e Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 14 Aug 2023 11:53:32 +0300 Subject: [PATCH 14/31] fix validation --- .../Validation/Evm/EvmGasPriceValidation.swift | 2 +- .../DAppEthereumConfirmInteractor.swift | 2 +- .../DAppOperationConfirmInteractor+Protocol.swift | 2 +- .../DAppOperationConfirmPresenter.swift | 4 ++-- .../DAppOperationConfirmProtocols.swift | 7 +------ .../DAppSignBytesConfirmInteractor.swift | 2 +- 6 files changed, 7 insertions(+), 12 deletions(-) diff --git a/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift index 55cad89181..ce7511b4ea 100644 --- a/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift +++ b/novawallet/Common/Services/ExtrinsicService/Validation/Evm/EvmGasPriceValidation.swift @@ -64,7 +64,7 @@ extension EvmGasPriceValidationProvider: ExtrinsicValidationProviderProtocol { return true } - return self.model.defaultGasPrice * self.multiplier < maxPriorityPrice * self.divisor + return self.model.defaultGasPrice * self.multiplier > maxPriorityPrice * self.divisor }) } } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift index da93abdad3..e89949daf9 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumConfirmInteractor.swift @@ -170,7 +170,7 @@ final class DAppEthereumConfirmInteractor: DAppOperationBaseInteractor { case let .success(model): self?.lastFee = model let validationProvider = validationProviderFactory.createGasPriceValidation(for: model) - let feeModel = DAppOperationConfirmFee(value: model.fee, validationProvider: validationProvider) + let feeModel = FeeOutputModel(value: model.fee, validationProvider: validationProvider) self?.presenter?.didReceive(feeResult: .success(feeModel)) case let .failure(error): diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift index e96460e949..af9a99b3e8 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Protocol.swift @@ -98,7 +98,7 @@ extension DAppOperationConfirmInteractor: DAppOperationConfirmInteractorInputPro do { let info = try feeWrapper.targetOperation.extractNoCancellableResultData() if let fee = BigUInt(info.fee) { - let feeModel = DAppOperationConfirmFee(value: fee, validationProvider: nil) + let feeModel = FeeOutputModel(value: fee, validationProvider: nil) self?.presenter?.didReceive(feeResult: .success(feeModel)) } else { self?.presenter?.didReceive(feeResult: .failure(CommonError.dataCorruption)) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift index cf87953840..6db7a013b1 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift @@ -16,7 +16,7 @@ final class DAppOperationConfirmPresenter { let balanceViewModelFactory: BalanceViewModelFactoryProtocol private var confirmationModel: DAppOperationConfirmModel? - private var feeModel: DAppOperationConfirmFee? + private var feeModel: FeeOutputModel? private var priceData: PriceData? init( @@ -166,7 +166,7 @@ extension DAppOperationConfirmPresenter: DAppOperationConfirmInteractorOutputPro provideFeeViewModel() } - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { switch feeResult { case let .success(fee): feeModel = fee diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift index 14e8619ef7..6cce4e04a3 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift @@ -22,14 +22,9 @@ protocol DAppOperationConfirmInteractorInputProtocol: AnyObject { func prepareTxDetails() } -struct DAppOperationConfirmFee { - let value: BigUInt - let validationProvider: ExtrinsicValidationProviderProtocol? -} - protocol DAppOperationConfirmInteractorOutputProtocol: AnyObject { func didReceive(modelResult: Result) - func didReceive(feeResult: Result) + func didReceive(feeResult: Result) func didReceive(priceResult: Result) func didReceive(responseResult: Result, for request: DAppOperationRequest) func didReceive(txDetailsResult: Result) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift index 5ede006676..c6039de0a2 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift @@ -45,7 +45,7 @@ final class DAppSignBytesConfirmInteractor: DAppOperationBaseInteractor { } private func provideZeroFee() { - let feeModel = DAppOperationConfirmFee(value: 0, validationProvider: nil) + let feeModel = FeeOutputModel(value: 0, validationProvider: nil) presenter?.didReceive(feeResult: .success(feeModel)) presenter?.didReceive(priceResult: .success(nil)) } From 2c9e6e80a92288a65dde060e9514bfd5297fbf73 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 14 Aug 2023 23:54:23 +0300 Subject: [PATCH 15/31] support token edit --- novawallet.xcodeproj/project.pbxproj | 8 ++ .../ChainRegistry/LocalChain/ChainModel.swift | 9 +- .../AssetList/Base/AssetListBaseBuilder.swift | 30 ++++++ .../Base/AssetListBaseInteractor.swift | 12 ++- .../AddToken/TokensManageAddInteractor.swift | 21 ++-- .../AddToken/TokensManageAddPresenter.swift | 58 ++++++----- .../AddToken/TokensManageAddProtocols.swift | 4 +- .../AddToken/TokensManageAddViewFactory.swift | 5 + .../AddToken/TokensManageAddWireframe.swift | 16 ++- .../Model/EvmTokenAddResult.swift | 6 ++ .../Validating/TokenAddErrorPresentable.swift | 33 +++++++ .../TokenAddValidationFactory.swift | 99 +++++++++++++++++++ novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 1 + 14 files changed, 260 insertions(+), 44 deletions(-) create mode 100644 novawallet/Modules/TokensManage/Model/EvmTokenAddResult.swift create mode 100644 novawallet/Modules/TokensManage/Validating/TokenAddValidationFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index edc0416d0e..f61a8c7d40 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -81,6 +81,8 @@ 0C56B4FD2A4B0CA90030F9C9 /* AssetListBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C56B4FC2A4B0CA90030F9C9 /* AssetListBuilderResult.swift */; }; 0C83775D2A4EEB380072102D /* AssetListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C83775C2A4EEB380072102D /* AssetListState.swift */; }; 0C8A25592A553A6C0072882A /* KeyboardAppearanceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */; }; + 0C9680F12A8A85BB006A411B /* TokenAddValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */; }; + 0C9680F32A8AC2F2006A411B /* EvmTokenAddResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9680F22A8AC2F2006A411B /* EvmTokenAddResult.swift */; }; 0C9ECB5A2A4A9AB400BDCA73 /* AssetListAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */; }; 0CA307BC2F570941CD22C9AA /* ExportMnemonicConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4688AF0658F8BB7A90C2BE /* ExportMnemonicConfirmViewFactory.swift */; }; 0CAC01552A52E0CC0069413E /* AssetListModelHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */; }; @@ -3761,6 +3763,8 @@ 0C797A5B5863A026E84062AE /* MessageSheetProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessageSheetProtocols.swift; sourceTree = ""; }; 0C83775C2A4EEB380072102D /* AssetListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListState.swift; sourceTree = ""; }; 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardAppearanceState.swift; sourceTree = ""; }; + 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenAddValidationFactory.swift; sourceTree = ""; }; + 0C9680F22A8AC2F2006A411B /* EvmTokenAddResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmTokenAddResult.swift; sourceTree = ""; }; 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListAccountCell.swift; sourceTree = ""; }; 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListModelHelpers.swift; sourceTree = ""; }; 0CAC01562A52E1960069413E /* AssetListPresenterHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListPresenterHelpers.swift; sourceTree = ""; }; @@ -12748,6 +12752,7 @@ 849F1444294360BB00D9F9BA /* EvmTokenAddRequest.swift */, 849F14482944443500D9F9BA /* CoingeckoUrlParser.swift */, 849F144C29444CB800D9F9BA /* AssetModel+TokenAddRequest.swift */, + 0C9680F22A8AC2F2006A411B /* EvmTokenAddResult.swift */, ); path = Model; sourceTree = ""; @@ -12947,6 +12952,7 @@ isa = PBXGroup; children = ( 84A7AC7F29465BF9001A39CF /* TokenAddErrorPresentable.swift */, + 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */, ); path = Validating; sourceTree = ""; @@ -18767,6 +18773,7 @@ 8860F3E8289D7CF400C0BF86 /* Atomic.swift in Sources */, 0C3205C02A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift in Sources */, 8499FECF27BFA25100712589 /* NftType.swift in Sources */, + 0C9680F12A8A85BB006A411B /* TokenAddValidationFactory.swift in Sources */, 84EF8D40288FDA7700265346 /* WalletListLocalStorageSubscriptionHandler.swift in Sources */, AEA2C1B62681E9B20069492E /* ValidatorSearchViewFactory.swift in Sources */, 84AE7ABB27D411CE00495267 /* RMRKV2DetailsInteractor.swift in Sources */, @@ -19742,6 +19749,7 @@ 847EA1D62A1CA47500F1CBD8 /* SubqueryMultistaking.swift in Sources */, 8485D924277E16C400767243 /* DAppBrowserScriptHandler.swift in Sources */, 8427495528FEB92700B2B70B /* GovernanceLockStateFactory.swift in Sources */, + 0C9680F32A8AC2F2006A411B /* EvmTokenAddResult.swift in Sources */, 842BDB2C278C4FFE00AB4B5A /* DAppBrowserAuthorizedState.swift in Sources */, 849346AF2A1F7F7D00CB75B7 /* MultistakingServices.swift in Sources */, 848919DB26FB663D004DBAD5 /* CrowdloansChainViewModel.swift in Sources */, diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index cb7b43f69b..fd70a7b81e 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -199,12 +199,15 @@ enum ChainOptions: String, Codable { } extension ChainModel { - func adding(asset: AssetModel) -> ChainModel { - .init( + func addingOrUpdating(asset: AssetModel) -> ChainModel { + let filteredAssets = assets.filter { $0.assetId != asset.assetId } + let newAssets = filteredAssets.union([asset]) + + return .init( chainId: chainId, parentId: parentId, name: name, - assets: assets.union([asset]), + assets: newAssets, nodes: nodes, nodeSwitchStrategy: nodeSwitchStrategy, addressPrefix: addressPrefix, diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift b/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift index b44c76a3d0..7b9789cdae 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift @@ -208,6 +208,24 @@ class AssetListBaseBuilder { groups.apply(changes: groupChanges) } + private func processRemovedPriceChainAssets(_ chainAssetIds: Set) -> Bool { + let currentPrices: [ChainAssetId: PriceData] = (try? priceResult?.get()) ?? [:] + + let removedChainAssetIds = currentPrices.keys.filter { !chainAssetIds.contains($0) } + + guard !removedChainAssetIds.isEmpty else { + return false + } + + let newPrices = currentPrices.filter { !removedChainAssetIds.contains($0.key) } + + priceResult = .success(newPrices) + + updateAssetModels() + + return true + } + private func processPriceChanges(_ changes: [ChainAssetId: DataProviderChange]) { var currentPrices: [ChainAssetId: PriceData] = (try? priceResult?.get()) ?? [:] @@ -262,6 +280,18 @@ extension AssetListBaseBuilder { } } + func applyRemovedPriceChainAssets(_ chainAssetIds: Set) { + workingQueue.async { [weak self] in + guard let self = self else { + return + } + + if self.processRemovedPriceChainAssets(chainAssetIds) { + self.scheduleRebuildModel() + } + } + } + func applyPriceChanges(_ priceChanges: [ChainAssetId: DataProviderChange]) { workingQueue.async { [weak self] in self?.processPriceChanges(priceChanges) diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift index 467b000af1..dde1389f9d 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift @@ -222,18 +222,15 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip func updatePriceSubscription(from changes: [DataProviderChange]) { let prevPrices = availableTokenPrice + for change in changes { switch change { case let .insert(chain), let .update(chain): availableTokenPrice = availableTokenPrice.filter { $0.key.chainId != chain.chainId } availableTokenPrice = chain.assets.reduce(into: availableTokenPrice) { result, asset in - guard let priceId = asset.priceId else { - return - } - let chainAssetId = ChainAssetId(chainId: chain.chainId, assetId: asset.assetId) - result[chainAssetId] = priceId + result[chainAssetId] = asset.priceId } case let .delete(deletedIdentifier): availableTokenPrice = availableTokenPrice.filter { $0.key.chainId != deletedIdentifier } @@ -241,6 +238,7 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip } if prevPrices != availableTokenPrice { + removeNotExistingPriceIds(from: Set(availableTokenPrice.keys)) updatePriceProvider(for: Set(availableTokenPrice.values), currency: selectedCurrency) } } @@ -254,6 +252,10 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip } } + private func removeNotExistingPriceIds(from chainAssetIds: Set) { + baseBuilder?.applyRemovedPriceChainAssets(chainAssetIds) + } + private func updatePriceProvider( for priceIdSet: Set, currency: Currency diff --git a/novawallet/Modules/TokensManage/AddToken/TokensManageAddInteractor.swift b/novawallet/Modules/TokensManage/AddToken/TokensManageAddInteractor.swift index 6848b596fe..53178a3a9b 100644 --- a/novawallet/Modules/TokensManage/AddToken/TokensManageAddInteractor.swift +++ b/novawallet/Modules/TokensManage/AddToken/TokensManageAddInteractor.swift @@ -120,7 +120,7 @@ final class TokensManageAddInteractor: AnyCancellableCleaning { chain: chain ) - let chainModifyOperation = ClosureOperation { + let chainModifyOperation = ClosureOperation { try contractExistenseWrapper.targetOperation.extractNoCancellableResultData() let priceId = try priceIdWrapper.targetOperation.extractNoCancellableResultData() @@ -129,20 +129,26 @@ final class TokensManageAddInteractor: AnyCancellableCleaning { throw TokensManageAddInteractorError.tokenSaveFailed(CommonError.dataCorruption) } - if let asset = chain.assets.first(where: { $0.assetId == newAsset.assetId }) { + let optAsset = chain.assets.first(where: { $0.assetId == newAsset.assetId }) + + // a user can't update default assets + if let asset = optAsset, asset.source == .remote { throw TokensManageAddInteractorError.tokenAlreadyExists(asset) } - let newChain = chain.adding(asset: newAsset) + let newChain = chain.addingOrUpdating(asset: newAsset) + + let chainAsset = ChainAsset(chain: newChain, asset: newAsset) + let isNew = optAsset == nil - return ChainAsset(chain: newChain, asset: newAsset) + return EvmTokenAddResult(chainAsset: chainAsset, isNew: isNew) } chainModifyOperation.addDependency(contractExistenseWrapper.targetOperation) chainModifyOperation.addDependency(priceIdWrapper.targetOperation) let saveOperation = chainRepository.saveOperation({ - let newChain = try chainModifyOperation.extractNoCancellableResultData().chain + let newChain = try chainModifyOperation.extractNoCancellableResultData().chainAsset.chain return [newChain] }, { @@ -154,11 +160,10 @@ final class TokensManageAddInteractor: AnyCancellableCleaning { saveOperation.completionBlock = { [weak self] in DispatchQueue.main.async { do { - let chainAsset = try chainModifyOperation.extractNoCancellableResultData() + let result = try chainModifyOperation.extractNoCancellableResultData() try saveOperation.extractNoCancellableResultData() - self?.chain = chainAsset.chain - self?.presenter?.didSaveEvmToken(chainAsset.asset) + self?.presenter?.didSaveEvmToken(result) } catch { if let interactorError = error as? TokensManageAddInteractorError { self?.presenter?.didReceiveError(interactorError) diff --git a/novawallet/Modules/TokensManage/AddToken/TokensManageAddPresenter.swift b/novawallet/Modules/TokensManage/AddToken/TokensManageAddPresenter.swift index 5033c75d66..d9f1d55d7f 100644 --- a/novawallet/Modules/TokensManage/AddToken/TokensManageAddPresenter.swift +++ b/novawallet/Modules/TokensManage/AddToken/TokensManageAddPresenter.swift @@ -3,12 +3,14 @@ import SoraFoundation final class TokensManageAddPresenter { static let priceIdUrlPlaceholder = "coingecko.com/coins/tether" - static let maxDecimals: Int = 36 + static let maxDecimals: UInt8 = 36 weak var view: TokensManageAddViewProtocol? let wireframe: TokensManageAddWireframeProtocol let interactor: TokensManageAddInteractorInputProtocol let localizationManager: LocalizationManagerProtocol + let chain: ChainModel + let validationFactory: TokenAddValidationFactoryProtocol let logger: LoggerProtocol private var partialAddress: String? @@ -21,11 +23,15 @@ final class TokensManageAddPresenter { init( interactor: TokensManageAddInteractorInputProtocol, wireframe: TokensManageAddWireframeProtocol, + chain: ChainModel, + validationFactory: TokenAddValidationFactoryProtocol, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { self.interactor = interactor self.wireframe = wireframe + self.chain = chain + self.validationFactory = validationFactory self.localizationManager = localizationManager self.logger = logger } @@ -118,41 +124,45 @@ extension TokensManageAddPresenter: TokensManageAddPresenterProtocol { } func confirmTokenAdd() { + let locale = localizationManager.selectedLocale + guard let contractAddress = constructEvmAddress() else { if let view = view { - wireframe.presentInvalidContractAddress(from: view, locale: localizationManager.selectedLocale) + wireframe.presentInvalidContractAddress(from: view, locale: locale) } return } - guard let symbol = partialSymbol, let decimals = partialDecimals.flatMap({ UInt8($0) }) else { + guard + let symbol = partialSymbol, + let decimalsString = partialDecimals, + let decimals = UInt8(decimalsString) else { return } - guard decimals <= Self.maxDecimals else { - if let view = view { - wireframe.presentInvalidDecimals( - from: view, - maxValue: String(Self.maxDecimals), - locale: localizationManager.selectedLocale - ) + DataValidationRunner(validators: [ + validationFactory.decimalsNotExceedMax(for: decimals, maxValue: Self.maxDecimals, locale: locale), + validationFactory.noRemoteToken(for: contractAddress, chain: chain, locale: locale), + validationFactory.warnDuplicates(for: contractAddress, chain: chain, locale: locale) + ]).runValidation { [weak self] in + guard let self = self else { + return } - return - } - let isPriceIdUrlEmpty = (partialPriceIdUrl ?? "").isEmpty + let isPriceIdUrlEmpty = (self.partialPriceIdUrl ?? "").isEmpty - let request = EvmTokenAddRequest( - contractAddress: contractAddress, - name: nil, - symbol: symbol, - decimals: decimals, - priceIdUrl: !isPriceIdUrlEmpty ? partialPriceIdUrl : nil - ) + let request = EvmTokenAddRequest( + contractAddress: contractAddress, + name: nil, + symbol: symbol, + decimals: decimals, + priceIdUrl: !isPriceIdUrlEmpty ? self.partialPriceIdUrl : nil + ) - view?.didStartLoading() + self.view?.didStartLoading() - interactor.save(newToken: request) + self.interactor.save(newToken: request) + } } } @@ -178,8 +188,8 @@ extension TokensManageAddPresenter: TokensManageAddInteractorOutputProtocol { } } - func didSaveEvmToken(_ token: AssetModel) { - wireframe.complete(from: view, token: token, locale: localizationManager.selectedLocale) + func didSaveEvmToken(_ result: EvmTokenAddResult) { + wireframe.complete(from: view, result: result, locale: localizationManager.selectedLocale) } func didReceiveError(_ error: TokensManageAddInteractorError) { diff --git a/novawallet/Modules/TokensManage/AddToken/TokensManageAddProtocols.swift b/novawallet/Modules/TokensManage/AddToken/TokensManageAddProtocols.swift index 33c9ca8bc1..2a58c336ba 100644 --- a/novawallet/Modules/TokensManage/AddToken/TokensManageAddProtocols.swift +++ b/novawallet/Modules/TokensManage/AddToken/TokensManageAddProtocols.swift @@ -23,11 +23,11 @@ protocol TokensManageAddInteractorInputProtocol: AnyObject { protocol TokensManageAddInteractorOutputProtocol: AnyObject { func didReceiveDetails(_ tokenDetails: EvmContractMetadata, for address: AccountAddress) - func didSaveEvmToken(_ token: AssetModel) + func didSaveEvmToken(_ result: EvmTokenAddResult) func didReceiveError(_ error: TokensManageAddInteractorError) } protocol TokensManageAddWireframeProtocol: AlertPresentable, ErrorPresentable, CommonRetryable, TokenAddErrorPresentable { - func complete(from view: TokensManageAddViewProtocol?, token: AssetModel, locale: Locale) + func complete(from view: TokensManageAddViewProtocol?, result: EvmTokenAddResult, locale: Locale) } diff --git a/novawallet/Modules/TokensManage/AddToken/TokensManageAddViewFactory.swift b/novawallet/Modules/TokensManage/AddToken/TokensManageAddViewFactory.swift index 7e29418f9d..6e43f25f18 100644 --- a/novawallet/Modules/TokensManage/AddToken/TokensManageAddViewFactory.swift +++ b/novawallet/Modules/TokensManage/AddToken/TokensManageAddViewFactory.swift @@ -9,9 +9,13 @@ struct TokensManageAddViewFactory { let wireframe = TokensManageAddWireframe() + let validationFactory = TokenAddValidationFactory(wireframe: wireframe) + let presenter = TokensManageAddPresenter( interactor: interactor, wireframe: wireframe, + chain: chain, + validationFactory: validationFactory, localizationManager: LocalizationManager.shared, logger: Logger.shared ) @@ -23,6 +27,7 @@ struct TokensManageAddViewFactory { presenter.view = view interactor.presenter = presenter + validationFactory.view = view return view } diff --git a/novawallet/Modules/TokensManage/AddToken/TokensManageAddWireframe.swift b/novawallet/Modules/TokensManage/AddToken/TokensManageAddWireframe.swift index 0bd756ea14..27c1cd6cc0 100644 --- a/novawallet/Modules/TokensManage/AddToken/TokensManageAddWireframe.swift +++ b/novawallet/Modules/TokensManage/AddToken/TokensManageAddWireframe.swift @@ -1,8 +1,20 @@ import Foundation final class TokensManageAddWireframe: TokensManageAddWireframeProtocol, ModalAlertPresenting { - func complete(from view: TokensManageAddViewProtocol?, token: AssetModel, locale: Locale) { - let title = R.string.localizable.addTokenCompletionMessage(token.symbol, preferredLanguages: locale.rLanguages) + func complete(from view: TokensManageAddViewProtocol?, result: EvmTokenAddResult, locale: Locale) { + let title: String + + if result.isNew { + title = R.string.localizable.addTokenCompletionMessage( + result.chainAsset.asset.symbol, + preferredLanguages: locale.rLanguages + ) + } else { + title = R.string.localizable.updateTokenCompletionMessage( + result.chainAsset.asset.symbol, + preferredLanguages: locale.rLanguages + ) + } let presenter = view?.controller.navigationController?.presentingViewController diff --git a/novawallet/Modules/TokensManage/Model/EvmTokenAddResult.swift b/novawallet/Modules/TokensManage/Model/EvmTokenAddResult.swift new file mode 100644 index 0000000000..9b4a9471c5 --- /dev/null +++ b/novawallet/Modules/TokensManage/Model/EvmTokenAddResult.swift @@ -0,0 +1,6 @@ +import Foundation + +struct EvmTokenAddResult { + let chainAsset: ChainAsset + let isNew: Bool +} diff --git a/novawallet/Modules/TokensManage/Validating/TokenAddErrorPresentable.swift b/novawallet/Modules/TokensManage/Validating/TokenAddErrorPresentable.swift index 8fd697d4e2..f64008b249 100644 --- a/novawallet/Modules/TokensManage/Validating/TokenAddErrorPresentable.swift +++ b/novawallet/Modules/TokensManage/Validating/TokenAddErrorPresentable.swift @@ -28,6 +28,13 @@ protocol TokenAddErrorPresentable: BaseErrorPresentable { from view: ControllerBackedProtocol, locale: Locale? ) + + func presentTokenUpdate( + from view: ControllerBackedProtocol, + symbol: String, + onContinue: @escaping () -> Void, + locale: Locale? + ) } extension TokenAddErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -94,4 +101,30 @@ extension TokenAddErrorPresentable where Self: AlertPresentable & ErrorPresentab present(message: message, title: title, closeAction: closeAction, from: view) } + + func presentTokenUpdate( + from view: ControllerBackedProtocol, + symbol: String, + onContinue: @escaping () -> Void, + locale: Locale? + ) { + let title = R.string.localizable.addTokenAlreadyExistsTitle(preferredLanguages: locale?.rLanguages) + let message = R.string.localizable.tokenAddRemoteExistMessage(symbol, preferredLanguages: locale?.rLanguages) + + let continueAction = AlertPresentableAction( + title: R.string.localizable.commonContinue(preferredLanguages: locale?.rLanguages), + style: .destructive + ) { + onContinue() + } + + let viewModel = AlertPresentableViewModel( + title: title, + message: message, + actions: [continueAction], + closeAction: R.string.localizable.commonCancel(preferredLanguages: locale?.rLanguages) + ) + + present(viewModel: viewModel, style: .alert, from: view) + } } diff --git a/novawallet/Modules/TokensManage/Validating/TokenAddValidationFactory.swift b/novawallet/Modules/TokensManage/Validating/TokenAddValidationFactory.swift new file mode 100644 index 0000000000..b1d8fea731 --- /dev/null +++ b/novawallet/Modules/TokensManage/Validating/TokenAddValidationFactory.swift @@ -0,0 +1,99 @@ +import Foundation + +protocol TokenAddValidationFactoryProtocol { + func decimalsNotExceedMax(for decimals: UInt8, maxValue: UInt8, locale: Locale) -> DataValidating + + func noRemoteToken( + for contractAddress: AccountAddress, + chain: ChainModel, + locale: Locale + ) -> DataValidating + + func warnDuplicates( + for contractAddress: AccountAddress, + chain: ChainModel, + locale: Locale + ) -> DataValidating +} + +final class TokenAddValidationFactory { + weak var view: ControllerBackedProtocol? + let wireframe: TokenAddErrorPresentable + + init(wireframe: TokenAddErrorPresentable) { + self.wireframe = wireframe + } +} + +extension TokenAddValidationFactory: TokenAddValidationFactoryProtocol { + func decimalsNotExceedMax(for decimals: UInt8, maxValue: UInt8, locale: Locale) -> DataValidating { + ErrorConditionViolation( + onError: { [weak self] in + guard let view = self?.view else { + return + } + + self?.wireframe.presentInvalidDecimals( + from: view, + maxValue: String(maxValue), + locale: locale + ) + }, + preservesCondition: { + decimals <= maxValue + } + ) + } + + func noRemoteToken( + for contractAddress: AccountAddress, + chain: ChainModel, + locale: Locale + ) -> DataValidating { + let assetId = AssetModel.createAssetId(from: contractAddress) + let optAsset = chain.assets.first(where: { $0.assetId == assetId }) + + return ErrorConditionViolation( + onError: { [weak self] in + guard let view = self?.view, let asset = optAsset else { + return + } + + self?.wireframe.presentTokenAlreadyExists( + from: view, + symbol: asset.symbol, + locale: locale + ) + }, + preservesCondition: { + optAsset?.source != .remote + } + ) + } + + func warnDuplicates( + for contractAddress: AccountAddress, + chain: ChainModel, + locale: Locale + ) -> DataValidating { + let assetId = AssetModel.createAssetId(from: contractAddress) + let optAsset = chain.assets.first(where: { $0.assetId == assetId }) + + return WarningConditionViolation(onWarning: { [weak self] delegate in + guard let view = self?.view, let asset = optAsset else { + return + } + + self?.wireframe.presentTokenUpdate( + from: view, + symbol: asset.symbol, + onContinue: { + delegate.didCompleteWarningHandling() + }, + locale: locale + ) + }, preservesCondition: { + optAsset == nil + }) + } +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 86e5bc3677..0e79f63ad4 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1292,3 +1292,5 @@ "evm.transaction.fee.too.high.title" = "Network fee too high"; "evm.transaction.fee.too.high.message" = "The estimated network fee​ (%@) is much higher than the default network fee (%@). This might be due to temporary network congestion. You can refresh to wait for a lower network fee."; "common.refresh.fee" = "Refresh fee"; +"token.add.remote.exist.message" = "The entered contract address is present in Nova as a %@ token. Are you sure you want to modify it?"; +"update.token.completion.message" = "%@ token updated"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 656bc868e8..16a6238c60 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1292,3 +1292,4 @@ "evm.transaction.fee.too.high.title" = "Комиссия сети слишком большая"; "evm.transaction.fee.too.high.message" = "Подсчитанная комиссия сети​ (%@) получилась намного чем комиссия сети по умолчанию (%@). Это может быть из-за временной нагрузки на сеть. Вы можете обновить комиссию для получения меньшего значения."; "common.refresh.fee" = "Обновить"; +"token.add.remote.exist.message" = "Адрес смарт-контракта добавлен в Nova как %@ токен. Вы уверены, что хотите изменить его?"; From 15c463c35d182320127ec4f4bc9261a9da581a7b Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 15 Aug 2023 00:05:10 +0300 Subject: [PATCH 16/31] fix tests --- .../ChainRegistry/LocalChain/ChainModel.swift | 19 +++++++++++++++++++ novawalletTests/Mocks/ModuleMocks.swift | 18 +++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index fd70a7b81e..58189ddd28 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -199,6 +199,25 @@ enum ChainOptions: String, Codable { } extension ChainModel { + func adding(asset: AssetModel) -> ChainModel { + .init( + chainId: chainId, + parentId: parentId, + name: name, + assets: assets.union([asset]), + nodes: nodes, + nodeSwitchStrategy: nodeSwitchStrategy, + addressPrefix: addressPrefix, + types: types, + icon: icon, + options: options, + externalApis: externalApis, + explorers: explorers, + order: order, + additional: additional + ) + } + func addingOrUpdating(asset: AssetModel) -> ChainModel { let filteredAssets = assets.filter { $0.assetId != asset.assetId } let newAssets = filteredAssets.union([asset]) diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index 6e8153cae1..b20e887d3d 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -12416,9 +12416,9 @@ import SubstrateSdk - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { - return cuckoo_manager.call("didReceive(feeResult: Result)", + return cuckoo_manager.call("didReceive(feeResult: Result)", parameters: (feeResult), escapingParameters: (feeResult), superclassCall: @@ -12488,9 +12488,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(modelResult: Result)", parameterMatchers: matchers)) } - func didReceive(feeResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { - let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(feeResult: Result)", parameterMatchers: matchers)) + func didReceive(feeResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockDAppOperationConfirmInteractorOutputProtocol.self, method: "didReceive(feeResult: Result)", parameterMatchers: matchers)) } func didReceive(priceResult: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { @@ -12531,9 +12531,9 @@ import SubstrateSdk } @discardableResult - func didReceive(feeResult: M1) -> Cuckoo.__DoNotUse<(Result), Void> where M1.MatchedType == Result { - let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] - return cuckoo_manager.verify("didReceive(feeResult: Result)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func didReceive(feeResult: M1) -> Cuckoo.__DoNotUse<(Result), Void> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: feeResult) { $0 }] + return cuckoo_manager.verify("didReceive(feeResult: Result)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -12571,7 +12571,7 @@ import SubstrateSdk - func didReceive(feeResult: Result) { + func didReceive(feeResult: Result) { return DefaultValueRegistry.defaultValue(for: (Void).self) } From 25d88da9f6831d1142fc4efcaad3b55c85be6de1 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 15 Aug 2023 09:08:16 +0300 Subject: [PATCH 17/31] fix tests --- .../Staking/StakingRebondSetup/StakingRebondSetupTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift b/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift index f119bd5c00..1e979b289f 100644 --- a/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift +++ b/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift @@ -176,7 +176,7 @@ class StakingRebondSetupTests: XCTestCase { // then - wait(for: [inputExpectation, assetExpectation, feeExpectation], timeout: 10) + wait(for: [inputExpectation, assetExpectation, feeExpectation, transferableExpectation], timeout: 10) return presenter } From 9324ba58adde7e375636a71ee1a15daf79b12ab8 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 15 Aug 2023 10:00:45 +0300 Subject: [PATCH 18/31] fix balance calculation logic --- .../AssetDetails/AssetDetailsPresenter.swift | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift index 4285db85ae..292b17d25f 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift @@ -37,6 +37,14 @@ final class AssetDetailsPresenter { localizationManager = localizableManager } + private func hasLocks(for balance: AssetBalance, crowdloans: [CrowdloanContributionData]) -> Bool { + balance.locked > 0 || !crowdloans.isEmpty + } + + private func calculateTotalCrowdloans(for crowdloans: [CrowdloanContributionData]) -> BigUInt { + crowdloans.reduce(0) { $0 + $1.amount } + } + private func updateView() { guard let view = view else { return @@ -54,8 +62,10 @@ final class AssetDetailsPresenter { ) view.didReceive(assetModel: assetDetailsModel) + let totalCrowdloans = calculateTotalCrowdloans(for: crowdloans) + let totalBalance = viewModelFactory.createBalanceViewModel( - value: balance.totalInPlank, + value: balance.totalInPlank + totalCrowdloans, assetDisplayInfo: chainAsset.assetDisplayInfo, priceData: priceData, locale: selectedLocale @@ -69,7 +79,7 @@ final class AssetDetailsPresenter { ) let lockedBalance = viewModelFactory.createBalanceViewModel( - value: balance.locked, + value: balance.locked + totalCrowdloans, assetDisplayInfo: chainAsset.assetDisplayInfo, priceData: priceData, locale: selectedLocale @@ -77,7 +87,10 @@ final class AssetDetailsPresenter { view.didReceive(totalBalance: totalBalance) view.didReceive(transferableBalance: transferableBalance) - view.didReceive(lockedBalance: lockedBalance, isSelectable: !locks.isEmpty || !crowdloans.isEmpty) + + let isSelectable = hasLocks(for: balance, crowdloans: crowdloans) + view.didReceive(lockedBalance: lockedBalance, isSelectable: isSelectable) + view.didReceive(availableOperations: availableOperations) } @@ -173,7 +186,7 @@ extension AssetDetailsPresenter: AssetDetailsPresenterProtocol { free: balance.freeInPlank.decimal(precision: precision), reserved: balance.reservedInPlank.decimal(precision: precision), frozen: balance.frozenInPlank.decimal(precision: precision), - crowdloans: crowdloans.reduce(0) { $0 + $1.amount }.decimal(precision: precision), + crowdloans: calculateTotalCrowdloans(for: crowdloans).decimal(precision: precision), price: priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0, priceChange: priceData?.dayChange ?? 0, priceId: priceData?.currencyId, From 76e056bcd738210129ce0fa2b3a9d5caddb48bb2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 15 Aug 2023 10:24:15 +0300 Subject: [PATCH 19/31] fix filters for local transaction history --- .../Common/Extension/Foundation/NSPredicate+Transaction.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Common/Extension/Foundation/NSPredicate+Transaction.swift b/novawallet/Common/Extension/Foundation/NSPredicate+Transaction.swift index c67011abbb..4fdc503341 100644 --- a/novawallet/Common/Extension/Foundation/NSPredicate+Transaction.swift +++ b/novawallet/Common/Extension/Foundation/NSPredicate+Transaction.swift @@ -44,7 +44,7 @@ extension NSPredicate { if let filter = filter { let filterPredicate = filterTransactionsByType(filter) - return NSCompoundPredicate(orPredicateWithSubpredicates: [filterPredicate, filterByAsset]) + return NSCompoundPredicate(andPredicateWithSubpredicates: [filterByAsset, filterPredicate]) } else { return filterByAsset } @@ -93,7 +93,7 @@ extension NSPredicate { if let filter = filter { let filterPredicate = filterTransactionsByType(filter) - return NSCompoundPredicate(orPredicateWithSubpredicates: [filterPredicate, filterByAsset]) + return NSCompoundPredicate(andPredicateWithSubpredicates: [filterByAsset, filterPredicate]) } else { return filterByAsset } From 7b992a7b76af4137582c118391de1262fab7d223 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 15 Aug 2023 18:11:01 +0300 Subject: [PATCH 20/31] fix sign bytes when pincode enabled --- .../DAppOperationConfirmViewFactory.swift | 3 +- .../DAppSignBytesConfirmInteractor.swift | 30 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift index bb93d1739f..ac58a5e711 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift @@ -167,7 +167,8 @@ struct DAppOperationConfirmViewFactory { DAppSignBytesConfirmInteractor( request: request, chain: chain, - signingWrapperFactory: SigningWrapperFactory(keystore: Keychain()) + signingWrapperFactory: SigningWrapperFactory(keystore: Keychain()), + operationQueue: OperationManagerFacade.sharedDefaultQueue ) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift index c6039de0a2..63dd933c7e 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppSignBytesConfirmInteractor.swift @@ -1,22 +1,26 @@ import Foundation import SubstrateSdk import SoraKeystore +import RobinHood final class DAppSignBytesConfirmInteractor: DAppOperationBaseInteractor { let request: DAppOperationRequest let chain: ChainModel let signingWrapperFactory: SigningWrapperFactoryProtocol + let operationQueue: OperationQueue private(set) var account: ChainAccountResponse? init( request: DAppOperationRequest, chain: ChainModel, - signingWrapperFactory: SigningWrapperFactoryProtocol + signingWrapperFactory: SigningWrapperFactoryProtocol, + operationQueue: OperationQueue ) { self.request = request self.chain = chain self.signingWrapperFactory = signingWrapperFactory + self.operationQueue = operationQueue } private func validateAndProvideConfirmationModel() { @@ -96,11 +100,29 @@ extension DAppSignBytesConfirmInteractor: DAppOperationConfirmInteractorInputPro let rawBytes = try prepareRawBytes() - let signature = try signer.sign(rawBytes).rawData() + let signingOperation = ClosureOperation { + try signer.sign(rawBytes).rawData() + } + + signingOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard let self = self else { + return + } + + do { + let signature = try signingOperation.extractNoCancellableResultData() - let response = DAppOperationResponse(signature: signature) + let response = DAppOperationResponse(signature: signature) + + self.presenter?.didReceive(responseResult: .success(response), for: self.request) + } catch { + self.presenter?.didReceive(responseResult: .failure(error), for: self.request) + } + } + } - presenter?.didReceive(responseResult: .success(response), for: request) + operationQueue.addOperation(signingOperation) } catch { presenter?.didReceive(responseResult: .failure(error), for: request) } From 8bcfa0df2a69720616ca9c9627c32d57a31c0a0b Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 15 Aug 2023 23:09:11 +0300 Subject: [PATCH 21/31] add signing bytes auth support --- novawallet.xcodeproj/project.pbxproj | 4 ++ novawallet/Common/Crypto/BaseSigner.swift | 57 +++++++++++++++++ novawallet/Common/Crypto/EthereumSigner.swift | 14 +++-- novawallet/Common/Crypto/SigningWrapper.swift | 62 ++++--------------- .../Common/Crypto/SigningWrapperFactory.swift | 6 +- .../DAppEthereumSignBytesInteractor.swift | 29 +++++++-- .../DAppOperationConfirmViewFactory.swift | 3 +- 7 files changed, 114 insertions(+), 61 deletions(-) create mode 100644 novawallet/Common/Crypto/BaseSigner.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index f61a8c7d40..aee8e9d5cc 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ 0C56B29DBA5245728AF7EDA4 /* GovernanceEditDelegationTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18E8361691E548ABAB33EA4 /* GovernanceEditDelegationTracksViewController.swift */; }; 0C56B4FB2A4B0C320030F9C9 /* AssetListBaseBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C56B4FA2A4B0C320030F9C9 /* AssetListBaseBuilder.swift */; }; 0C56B4FD2A4B0CA90030F9C9 /* AssetListBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C56B4FC2A4B0CA90030F9C9 /* AssetListBuilderResult.swift */; }; + 0C6D66AB2A8C0B6700AAB988 /* BaseSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6D66AA2A8C0B6700AAB988 /* BaseSigner.swift */; }; 0C83775D2A4EEB380072102D /* AssetListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C83775C2A4EEB380072102D /* AssetListState.swift */; }; 0C8A25592A553A6C0072882A /* KeyboardAppearanceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */; }; 0C9680F12A8A85BB006A411B /* TokenAddValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */; }; @@ -3760,6 +3761,7 @@ 0C53649F2A4D6EB700990478 /* AssetListBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBuilder.swift; sourceTree = ""; }; 0C56B4FA2A4B0C320030F9C9 /* AssetListBaseBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBaseBuilder.swift; sourceTree = ""; }; 0C56B4FC2A4B0CA90030F9C9 /* AssetListBuilderResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBuilderResult.swift; sourceTree = ""; }; + 0C6D66AA2A8C0B6700AAB988 /* BaseSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSigner.swift; sourceTree = ""; }; 0C797A5B5863A026E84062AE /* MessageSheetProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessageSheetProtocols.swift; sourceTree = ""; }; 0C83775C2A4EEB380072102D /* AssetListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListState.swift; sourceTree = ""; }; 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardAppearanceState.swift; sourceTree = ""; }; @@ -12138,6 +12140,7 @@ 844B6EC428A3153C00A8BE83 /* MetaAccountModelType+SignatureFormat.swift */, 84466B3428B6731B00FA1E0D /* LedgerSigningWrapper.swift */, 84466B3F28B77B4500FA1E0D /* SignatureVerificationWrapper.swift */, + 0C6D66AA2A8C0B6700AAB988 /* BaseSigner.swift */, ); path = Crypto; sourceTree = ""; @@ -18425,6 +18428,7 @@ 843B1D7A263EED5C00AF8957 /* StakingUnbondConfirmLayout.swift in Sources */, 0C3205DA2A896029002EB914 /* EvmConstantNonceProvider.swift in Sources */, 8499FEDC27BFDD4300712589 /* NftLocalSubscriptionFactory.swift in Sources */, + 0C6D66AB2A8C0B6700AAB988 /* BaseSigner.swift in Sources */, 88BB0B4529C1E29F00D041C1 /* Web3NameLocal.swift in Sources */, F4B06BFA264509E8003214D5 /* SetControllerCall.swift in Sources */, 84C3420D283192D000156569 /* CallbackStorageSubscription.swift in Sources */, diff --git a/novawallet/Common/Crypto/BaseSigner.swift b/novawallet/Common/Crypto/BaseSigner.swift new file mode 100644 index 0000000000..fd1250d572 --- /dev/null +++ b/novawallet/Common/Crypto/BaseSigner.swift @@ -0,0 +1,57 @@ +import Foundation +import IrohaCrypto +import SoraKeystore + +class BaseSigner: SignatureCreatorProtocol, AuthorizationPresentable { + let settingsManager: SettingsManagerProtocol + + init(settingsManager: SettingsManagerProtocol) { + self.settingsManager = settingsManager + } + + func sign(_ originalData: Data) throws -> IRSignatureProtocol { + if settingsManager.pinConfirmationEnabled == true { + let signingResult = signAfterAutorization(originalData) + switch signingResult { + case let .success(signature): + return signature + case let .failure(error): + throw error + } + } else { + return try signData(originalData) + } + } + + private func signAfterAutorization(_ originalData: Data) -> Result { + let semaphore = DispatchSemaphore(value: 0) + var signResult: Result? + + DispatchQueue.main.async { + self.authorize(animated: true, cancellable: true) { [weak self] completed in + defer { + semaphore.signal() + } + guard let self = self else { + return + } + if completed { + do { + let sign = try self.signData(originalData) + signResult = .success(sign) + } catch { + signResult = .failure(error) + } + } + } + } + + semaphore.wait() + + return signResult ?? .failure(SigningWrapperError.pinCheckNotPassed) + } + + func signData(_: Data) throws -> IRSignatureProtocol { + fatalError("Must be overriden by subsclass") + } +} diff --git a/novawallet/Common/Crypto/EthereumSigner.swift b/novawallet/Common/Crypto/EthereumSigner.swift index badc81a9f1..e4022d9b69 100644 --- a/novawallet/Common/Crypto/EthereumSigner.swift +++ b/novawallet/Common/Crypto/EthereumSigner.swift @@ -3,20 +3,26 @@ import IrohaCrypto import SoraKeystore import SubstrateSdk -final class EthereumSigner: SignatureCreatorProtocol { +final class EthereumSigner: BaseSigner { let keystore: KeystoreProtocol let metaId: String let accountId: AccountId? let publicKeyData: Data - init(keystore: KeystoreProtocol, ethereumAccountResponse: MetaEthereumAccountResponse) { + init( + keystore: KeystoreProtocol, + ethereumAccountResponse: MetaEthereumAccountResponse, + settingsManager: SettingsManagerProtocol + ) { self.keystore = keystore metaId = ethereumAccountResponse.metaId accountId = ethereumAccountResponse.isChainAccount ? ethereumAccountResponse.address : nil publicKeyData = ethereumAccountResponse.publicKey + + super.init(settingsManager: settingsManager) } - func sign(_ hashedData: Data) throws -> IRSignatureProtocol { + override func signData(_ data: Data) throws -> IRSignatureProtocol { let tag = KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId, accountId: accountId) let secretKey = try keystore.fetchKey(for: tag) @@ -28,6 +34,6 @@ final class EthereumSigner: SignatureCreatorProtocol { let signer = SECSigner(privateKey: privateKey) - return try signer.sign(hashedData) + return try signer.sign(data) } } diff --git a/novawallet/Common/Crypto/SigningWrapper.swift b/novawallet/Common/Crypto/SigningWrapper.swift index 9b60ff815c..f7804b0dc1 100644 --- a/novawallet/Common/Crypto/SigningWrapper.swift +++ b/novawallet/Common/Crypto/SigningWrapper.swift @@ -9,14 +9,13 @@ enum SigningWrapperError: Error { case pinCheckNotPassed } -final class SigningWrapper: SigningWrapperProtocol, AuthorizationPresentable { +final class SigningWrapper: BaseSigner, SigningWrapperProtocol { let keystore: KeystoreProtocol let metaId: String let accountId: AccountId? let isEthereumBased: Bool let cryptoType: MultiassetCryptoType let publicKeyData: Data - let settingsManager: SettingsManagerProtocol init( keystore: KeystoreProtocol, @@ -33,7 +32,8 @@ final class SigningWrapper: SigningWrapperProtocol, AuthorizationPresentable { self.cryptoType = cryptoType self.isEthereumBased = isEthereumBased self.publicKeyData = publicKeyData - self.settingsManager = settingsManager + + super.init(settingsManager: settingsManager) } init( @@ -48,7 +48,8 @@ final class SigningWrapper: SigningWrapperProtocol, AuthorizationPresentable { isEthereumBased = accountResponse.isEthereumBased cryptoType = accountResponse.cryptoType publicKeyData = accountResponse.publicKey - self.settingsManager = settingsManager + + super.init(settingsManager: settingsManager) } init( @@ -62,52 +63,11 @@ final class SigningWrapper: SigningWrapperProtocol, AuthorizationPresentable { isEthereumBased = true cryptoType = MultiassetCryptoType.ethereumEcdsa publicKeyData = ethereumAccountResponse.publicKey - self.settingsManager = settingsManager - } - - func sign(_ originalData: Data) throws -> IRSignatureProtocol { - if settingsManager.pinConfirmationEnabled == true { - let signingResult = signAfterAutorization(originalData) - switch signingResult { - case let .success(signature): - return signature - case let .failure(error): - throw error - } - } else { - return try _sign(originalData) - } - } - - private func signAfterAutorization(_ originalData: Data) -> Result { - let semaphore = DispatchSemaphore(value: 0) - var signResult: Result? - - DispatchQueue.main.async { - self.authorize(animated: true, cancellable: true) { [weak self] completed in - defer { - semaphore.signal() - } - guard let self = self else { - return - } - if completed { - do { - let sign = try self._sign(originalData) - signResult = .success(sign) - } catch { - signResult = .failure(error) - } - } - } - } - - semaphore.wait() - return signResult ?? .failure(SigningWrapperError.pinCheckNotPassed) + super.init(settingsManager: settingsManager) } - private func _sign(_ originalData: Data) throws -> IRSignatureProtocol { + override func signData(_ data: Data) throws -> IRSignatureProtocol { let tag: String = isEthereumBased ? KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId, accountId: accountId) : KeystoreTagV2.substrateSecretKeyTagForMetaId(metaId, accountId: accountId) @@ -116,13 +76,13 @@ final class SigningWrapper: SigningWrapperProtocol, AuthorizationPresentable { switch cryptoType { case .sr25519: - return try signSr25519(originalData, secretKeyData: secretKey, publicKeyData: publicKeyData) + return try signSr25519(data, secretKeyData: secretKey, publicKeyData: publicKeyData) case .ed25519: - return try signEd25519(originalData, secretKey: secretKey) + return try signEd25519(data, secretKey: secretKey) case .substrateEcdsa: - return try signEcdsa(originalData, secretKey: secretKey) + return try signEcdsa(data, secretKey: secretKey) case .ethereumEcdsa: - return try signEthereum(originalData, secretKey: secretKey) + return try signEthereum(data, secretKey: secretKey) } } } diff --git a/novawallet/Common/Crypto/SigningWrapperFactory.swift b/novawallet/Common/Crypto/SigningWrapperFactory.swift index 14bbb9d59d..7a4be04af4 100644 --- a/novawallet/Common/Crypto/SigningWrapperFactory.swift +++ b/novawallet/Common/Crypto/SigningWrapperFactory.swift @@ -90,7 +90,11 @@ final class SigningWrapperFactory: SigningWrapperFactoryProtocol { func createEthereumSigner(for ethereumAccountResponse: MetaEthereumAccountResponse) -> SignatureCreatorProtocol { switch ethereumAccountResponse.type { case .secrets: - return EthereumSigner(keystore: keystore, ethereumAccountResponse: ethereumAccountResponse) + return EthereumSigner( + keystore: keystore, + ethereumAccountResponse: ethereumAccountResponse, + settingsManager: settingsManager + ) case .watchOnly: return NoKeysSigningWrapper() case .paritySigner: diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift index aa15eab26a..20fe74166b 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppEthereumSignBytesInteractor.swift @@ -1,19 +1,23 @@ import Foundation import SubstrateSdk import SoraKeystore +import RobinHood final class DAppEthereumSignBytesInteractor: DAppOperationBaseInteractor { let request: DAppOperationRequest let signingWrapperFactory: SigningWrapperFactoryProtocol + let operationQueue: OperationQueue private(set) var account: MetaEthereumAccountResponse? init( request: DAppOperationRequest, - signingWrapperFactory: SigningWrapperFactoryProtocol + signingWrapperFactory: SigningWrapperFactoryProtocol, + operationQueue: OperationQueue ) { self.request = request self.signingWrapperFactory = signingWrapperFactory + self.operationQueue = operationQueue } private func validateAndProvideConfirmationModel() { @@ -86,11 +90,28 @@ extension DAppEthereumSignBytesInteractor: DAppOperationConfirmInteractorInputPr let rawBytes = try prepareRawBytes() - let signature = try signer.sign(rawBytes).rawData() + let signingOperation = ClosureOperation { + try signer.sign(rawBytes).rawData() + } + + signingOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard let self = self else { + return + } + + do { + let signature = try signingOperation.extractNoCancellableResultData() + let response = DAppOperationResponse(signature: signature) - let response = DAppOperationResponse(signature: signature) + self.presenter?.didReceive(responseResult: .success(response), for: self.request) + } catch { + self.presenter?.didReceive(responseResult: .failure(error), for: self.request) + } + } + } - presenter?.didReceive(responseResult: .success(response), for: request) + operationQueue.addOperation(signingOperation) } catch { presenter?.didReceive(responseResult: .failure(error), for: request) } diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift index ac58a5e711..751f2d5b48 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmViewFactory.swift @@ -220,7 +220,8 @@ struct DAppOperationConfirmViewFactory { ) -> DAppEthereumSignBytesInteractor { DAppEthereumSignBytesInteractor( request: request, - signingWrapperFactory: SigningWrapperFactory() + signingWrapperFactory: SigningWrapperFactory(), + operationQueue: OperationManagerFacade.sharedDefaultQueue ) } } From 5c1bf94f7bd7a625ff1d9b521fa960475d1aebd8 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 10:20:29 +0300 Subject: [PATCH 22/31] fix websocket service reconnection --- .../WalletConnect/WalletConnectService.swift | 156 ++++++++++++------ 1 file changed, 110 insertions(+), 46 deletions(-) diff --git a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift index 3d5a380769..0ca851194c 100644 --- a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift +++ b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift @@ -136,7 +136,7 @@ final class WalletConnectService { return } - let socketFactory = DefaultSocketFactory() + let socketFactory = DefaultSocketFactory(logger: logger) let relayClient = RelayClient(relayHost: relayHost, projectId: metadata.projectId, socketFactory: socketFactory) networking = NetworkingClientFactory.create(relayClient: relayClient) } @@ -262,20 +262,16 @@ extension WalletConnectService: WalletConnectServiceProtocol { private final class DefaultWebSocket: WebSocketConnecting { public var isConnected: Bool { - connected - } + mutex.lock() - public var request: URLRequest { - get { - webSocket.request + defer { + mutex.unlock() } - set { - webSocket.request = newValue - } + return connected } - @Atomic(defaultValue: false) private var connected: Bool + private var connected: Bool = false public var onConnect: (() -> Void)? @@ -283,12 +279,63 @@ private final class DefaultWebSocket: WebSocketConnecting { public var onText: ((String) -> Void)? - private let webSocket: WebSocket + private var webSocket: WebSocket? + + let logger: LoggerProtocol + let engineFactory: WebSocketEngineFactoryProtocol + let mutex = NSLock() + var request: URLRequest + + init(request: URLRequest, engineFactory: WebSocketEngineFactoryProtocol, logger: LoggerProtocol) { + self.engineFactory = engineFactory + self.request = request + self.logger = logger + } + + func connect() { + mutex.lock() + + defer { + mutex.unlock() + } + + logger.debug("Will connect") + + stopWebsocket() + startWebsocket() + } + + func disconnect() { + mutex.lock() + + defer { + mutex.unlock() + } - init(request: URLRequest, engine: Engine) { + connected = false + + logger.debug("Will disconnect") + + stopWebsocket() + } + + func write(string: String, completion: (() -> Void)?) { + mutex.lock() + + defer { + mutex.unlock() + } + + webSocket?.write(string: string, completion: completion) + } + + private func startWebsocket() { + let engine = engineFactory.createEngine() webSocket = WebSocket(request: request, engine: engine) - webSocket.onEvent = { [weak self] event in + webSocket?.onEvent = { [weak self] event in + self?.logger.debug("Did receive event: \(event)") + switch event { case .connected: self?.markConnectedAndNotify() @@ -296,71 +343,88 @@ private final class DefaultWebSocket: WebSocketConnecting { self?.markDisconnectedAndNotify( error: WSError(type: .protocolError, message: message, code: code) ) - case .cancelled, .reconnectSuggested: + case .cancelled: self?.markDisconnectedAndNotify(error: nil) + case .reconnectSuggested: + self?.stopWebsocket() + self?.startWebsocket() + case let .viabilityChanged(isViable): + if isViable { + self?.stopWebsocket() + self?.startWebsocket() + } else { + self?.markDisconnectedAndNotify(error: nil) + } case let .error(error): self?.markDisconnectedAndNotify(error: error) case let .text(text): self?.onText?(text) - default: + case .binary: + self?.logger.warning("Binary received but not supported") + case .ping, .pong: break } } - } - - func connect() { - webSocket.connect() - } - - func disconnect() { - guard connected else { - return - } - connected = false - - webSocket.forceDisconnect() - - onDisconnect?(nil) + webSocket?.connect() } - func write(string: String, completion: (() -> Void)?) { - webSocket.write(string: string, completion: completion) + private func stopWebsocket() { + webSocket?.onEvent = nil + webSocket?.forceDisconnect() + webSocket = nil } private func markConnectedAndNotify() { - guard !connected else { - return - } + mutex.lock() connected = true + mutex.unlock() + onConnect?() } private func markDisconnectedAndNotify(error: Error?) { - guard connected else { - return - } + mutex.lock() connected = false + + stopWebsocket() + + mutex.unlock() + onDisconnect?(error) } } -private struct DefaultSocketFactory: WebSocketFactory { - func create(with url: URL) -> WebSocketConnecting { - var urlRequest = URLRequest(url: url) - - // This is specifics of Starscream due to how Origin is set - urlRequest.addValue("allowed.domain.com", forHTTPHeaderField: "Origin") +private protocol WebSocketEngineFactoryProtocol { + func createEngine() -> Engine +} - let engine = WSEngine( +private final class DefaultEngineFactory: WebSocketEngineFactoryProtocol { + func createEngine() -> Engine { + WSEngine( transport: FoundationTransport(), certPinner: FoundationSecurity(), compressionHandler: nil ) + } +} + +private final class DefaultSocketFactory: WebSocketFactory { + let logger: LoggerProtocol + + init(logger: LoggerProtocol) { + self.logger = logger + } + + func create(with url: URL) -> WebSocketConnecting { + var urlRequest = URLRequest(url: url) + + // This is specifics of Starscream due to how Origin is set + urlRequest.addValue("allowed.domain.com", forHTTPHeaderField: "Origin") - return DefaultWebSocket(request: urlRequest, engine: engine) + return DefaultWebSocket(request: urlRequest, engineFactory: DefaultEngineFactory(), logger: logger) } } From 9c8794bae2fe88378516159c36653ac1e76bef46 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 10:43:31 +0300 Subject: [PATCH 23/31] fix tests --- .../DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift b/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift index d212925c47..fa63a5b6d5 100644 --- a/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift +++ b/novawalletTests/Modules/DApps/DAppOperationConfirm/DAppOperationConfirmTests.swift @@ -234,7 +234,8 @@ class DAppOperationConfirmTests: XCTestCase { let interactor = DAppSignBytesConfirmInteractor( request: request, chain: chain, - signingWrapperFactory: signingWrapperFactory + signingWrapperFactory: signingWrapperFactory, + operationQueue: OperationQueue() ) let delegate = MockDAppOperationConfirmDelegate() From 1c9b64b34853f965a46e90225b8b58ef014f6276 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 10:56:13 +0300 Subject: [PATCH 24/31] fix mutex lock --- .../WalletConnect/WalletConnectService.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift index 0ca851194c..8c424fbfa3 100644 --- a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift +++ b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift @@ -346,12 +346,10 @@ private final class DefaultWebSocket: WebSocketConnecting { case .cancelled: self?.markDisconnectedAndNotify(error: nil) case .reconnectSuggested: - self?.stopWebsocket() - self?.startWebsocket() + self?.makeRestart() case let .viabilityChanged(isViable): if isViable { - self?.stopWebsocket() - self?.startWebsocket() + self?.makeRestart() } else { self?.markDisconnectedAndNotify(error: nil) } @@ -374,6 +372,15 @@ private final class DefaultWebSocket: WebSocketConnecting { webSocket?.forceDisconnect() webSocket = nil } + + private func makeRestart() { + mutex.lock() + + self?.stopWebsocket() + self?.startWebsocket() + + mutex.unlock() + } private func markConnectedAndNotify() { mutex.lock() From 53c364542b5bc1bc23bbb4d32df168d6827f8863 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 10:57:41 +0300 Subject: [PATCH 25/31] refactoring --- .../Services/WalletConnect/WalletConnectService.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift index 8c424fbfa3..a25b4e1093 100644 --- a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift +++ b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift @@ -346,10 +346,10 @@ private final class DefaultWebSocket: WebSocketConnecting { case .cancelled: self?.markDisconnectedAndNotify(error: nil) case .reconnectSuggested: - self?.makeRestart() + self?.protectedRestart() case let .viabilityChanged(isViable): if isViable { - self?.makeRestart() + self?.protectedRestart() } else { self?.markDisconnectedAndNotify(error: nil) } @@ -373,7 +373,7 @@ private final class DefaultWebSocket: WebSocketConnecting { webSocket = nil } - private func makeRestart() { + private func protectedRestart() { mutex.lock() self?.stopWebsocket() From 7a21d453fb1cbfa566381b228c5acbb23899aacb Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 11:02:20 +0300 Subject: [PATCH 26/31] fix build --- .../Common/Services/WalletConnect/WalletConnectService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift index a25b4e1093..c32851a142 100644 --- a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift +++ b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift @@ -376,8 +376,8 @@ private final class DefaultWebSocket: WebSocketConnecting { private func protectedRestart() { mutex.lock() - self?.stopWebsocket() - self?.startWebsocket() + stopWebsocket() + startWebsocket() mutex.unlock() } From c936a1ee2d0a858cd4d181c878f814e50f1383dc Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 14:37:05 +0300 Subject: [PATCH 27/31] add system account validation --- novawallet.xcodeproj/project.pbxproj | 12 +++++ .../Protocols/BaseErrorPresentable.swift | 31 ++++++++++++ .../WalletConnect/WalletConnectService.swift | 6 +-- .../SystemAccountValidating.swift | 48 +++++++++++++++++++ .../Validators/BaseDataValidatorFactory.swift | 26 ++++++++++ .../CrossChainTransferSetupPresenter.swift | 9 +++- .../OnChainTransferSetupPresenter.swift | 9 +++- novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 9 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 novawallet/Common/SystemAccounts/SystemAccountValidating.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index aee8e9d5cc..32aee7b145 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ 0C8A25592A553A6C0072882A /* KeyboardAppearanceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */; }; 0C9680F12A8A85BB006A411B /* TokenAddValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */; }; 0C9680F32A8AC2F2006A411B /* EvmTokenAddResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9680F22A8AC2F2006A411B /* EvmTokenAddResult.swift */; }; + 0C9C642D2A8CE30A004DC078 /* SystemAccountValidating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */; }; 0C9ECB5A2A4A9AB400BDCA73 /* AssetListAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */; }; 0CA307BC2F570941CD22C9AA /* ExportMnemonicConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4688AF0658F8BB7A90C2BE /* ExportMnemonicConfirmViewFactory.swift */; }; 0CAC01552A52E0CC0069413E /* AssetListModelHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */; }; @@ -3767,6 +3768,7 @@ 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardAppearanceState.swift; sourceTree = ""; }; 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenAddValidationFactory.swift; sourceTree = ""; }; 0C9680F22A8AC2F2006A411B /* EvmTokenAddResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmTokenAddResult.swift; sourceTree = ""; }; + 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemAccountValidating.swift; sourceTree = ""; }; 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListAccountCell.swift; sourceTree = ""; }; 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListModelHelpers.swift; sourceTree = ""; }; 0CAC01562A52E1960069413E /* AssetListPresenterHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListPresenterHelpers.swift; sourceTree = ""; }; @@ -7635,6 +7637,14 @@ path = Effects; sourceTree = ""; }; + 0C9C642B2A8CE2D4004DC078 /* SystemAccounts */ = { + isa = PBXGroup; + children = ( + 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */, + ); + path = SystemAccounts; + sourceTree = ""; + }; 0CE550B42A4973BA00F0A7AC /* StakingUnbondSetup */ = { isa = PBXGroup; children = ( @@ -11584,6 +11594,7 @@ 849013D124A92686008F705E /* Common */ = { isa = PBXGroup; children = ( + 0C9C642B2A8CE2D4004DC078 /* SystemAccounts */, 0C463FCE2A592ACD003E71C9 /* Effects */, 8846F72129D6B9CB00B8B776 /* Multibase */, 8846FFA729C8527200796A7E /* PropertyWrappers */, @@ -18923,6 +18934,7 @@ 8430AAFB260230C5005B1066 /* ValidatorState.swift in Sources */, 8424308D265B1814003E07EC /* CrowdloanOperationFactory.swift in Sources */, 880855F628D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift in Sources */, + 0C9C642D2A8CE30A004DC078 /* SystemAccountValidating.swift in Sources */, 88D02FF42943207400E26390 /* BigInt+Decimal.swift in Sources */, 847999B82889510C00D1BAD2 /* TextInputViewDelegate.swift in Sources */, 842A738427DDF55A006EE1EA /* OperationIdOptionsPresentable.swift in Sources */, diff --git a/novawallet/Common/Protocols/BaseErrorPresentable.swift b/novawallet/Common/Protocols/BaseErrorPresentable.swift index f68c66f741..2cedd8180b 100644 --- a/novawallet/Common/Protocols/BaseErrorPresentable.swift +++ b/novawallet/Common/Protocols/BaseErrorPresentable.swift @@ -13,6 +13,12 @@ protocol BaseErrorPresentable { action: @escaping () -> Void, locale: Locale? ) + + func presentIsSystemAccount( + from view: ControllerBackedProtocol?, + onContinue: @escaping () -> Void, + locale: Locale? + ) } extension BaseErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -126,4 +132,29 @@ extension BaseErrorPresentable where Self: AlertPresentable & ErrorPresentable { present(message: message, title: title, closeAction: closeAction, from: view) } + + func presentIsSystemAccount( + from view: ControllerBackedProtocol?, + onContinue: @escaping () -> Void, + locale: Locale? + ) { + let title = R.string.localizable.sendSystemAccountTitle(preferredLanguages: locale?.rLanguages) + let message = R.string.localizable.sendSystemAccountMessage(preferredLanguages: locale?.rLanguages) + + let continueAction = AlertPresentableAction( + title: R.string.localizable.commonContinue(preferredLanguages: locale?.rLanguages), + style: .destructive + ) { + onContinue() + } + + let viewModel = AlertPresentableViewModel( + title: title, + message: message, + actions: [continueAction], + closeAction: R.string.localizable.commonCancel(preferredLanguages: locale?.rLanguages) + ) + + present(viewModel: viewModel, style: .alert, from: view) + } } diff --git a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift index c32851a142..e0b3b83e14 100644 --- a/novawallet/Common/Services/WalletConnect/WalletConnectService.swift +++ b/novawallet/Common/Services/WalletConnect/WalletConnectService.swift @@ -372,13 +372,13 @@ private final class DefaultWebSocket: WebSocketConnecting { webSocket?.forceDisconnect() webSocket = nil } - + private func protectedRestart() { mutex.lock() - + stopWebsocket() startWebsocket() - + mutex.unlock() } diff --git a/novawallet/Common/SystemAccounts/SystemAccountValidating.swift b/novawallet/Common/SystemAccounts/SystemAccountValidating.swift new file mode 100644 index 0000000000..11678c1fc6 --- /dev/null +++ b/novawallet/Common/SystemAccounts/SystemAccountValidating.swift @@ -0,0 +1,48 @@ +import Foundation + +protocol SystemAccountValidating { + func isSystem(accountId: AccountId) -> Bool +} + +final class PrefixSystemAccountValidation: SystemAccountValidating { + let prefixBytes: Data + + init(prefixBytes: Data) { + self.prefixBytes = prefixBytes + } + + init(string: String) { + prefixBytes = string.data(using: .utf8) ?? Data(string.bytes) + } + + func isSystem(accountId: AccountId) -> Bool { + accountId.starts(with: prefixBytes) + } +} + +final class CompoundSystemAccountValidation: SystemAccountValidating { + let validations: [SystemAccountValidating] + + init(validations: [SystemAccountValidating]) { + self.validations = validations + } + + func isSystem(accountId: AccountId) -> Bool { + validations.contains { $0.isSystem(accountId: accountId) } + } +} + +extension CompoundSystemAccountValidation { + static func substrateAccounts() -> CompoundSystemAccountValidation { + CompoundSystemAccountValidation(validations: [ + // Pallet-specific technical accounts, e.g. crowdloan-fund, nomination pool, + PrefixSystemAccountValidation(string: "modl"), + // Parachain sovereign accounts on relaychain + PrefixSystemAccountValidation(string: "para"), + // Relaychain sovereign account on parachains + PrefixSystemAccountValidation(string: "Parent"), + // Sibling parachain soveregin accounts + PrefixSystemAccountValidation(string: "sibl") + ]) + } +} diff --git a/novawallet/Common/Validation/Validators/BaseDataValidatorFactory.swift b/novawallet/Common/Validation/Validators/BaseDataValidatorFactory.swift index afb7df974b..9401fe2760 100644 --- a/novawallet/Common/Validation/Validators/BaseDataValidatorFactory.swift +++ b/novawallet/Common/Validation/Validators/BaseDataValidatorFactory.swift @@ -28,6 +28,8 @@ protocol BaseDataValidatingFactoryProtocol: AnyObject { minimumBalance: BigUInt?, locale: Locale ) -> DataValidating + + func accountIsNotSystem(for accountId: AccountId?, locale: Locale) -> DataValidating } extension BaseDataValidatingFactoryProtocol { @@ -206,4 +208,28 @@ extension BaseDataValidatingFactoryProtocol { return has(fee: feeDecimal, locale: locale, onError: onError) } + + func accountIsNotSystem(for accountId: AccountId?, locale: Locale) -> DataValidating { + WarningConditionViolation(onWarning: { [weak self] delegate in + guard let view = self?.view else { + return + } + + self?.basePresentable.presentIsSystemAccount( + from: view, + onContinue: { + delegate.didCompleteWarningHandling() + }, + locale: locale + ) + }, preservesCondition: { + guard let accountId = accountId else { + return true + } + + let validation = CompoundSystemAccountValidation.substrateAccounts() + + return !validation.isSystem(accountId: accountId) + }) + } } diff --git a/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift index cae5bd9bac..63f09b3732 100644 --- a/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift @@ -395,7 +395,12 @@ extension CrossChainTransferSetupPresenter: TransferSetupChildPresenterProtocol selectedLocale: selectedLocale ) - validators.append( + validators.append(contentsOf: [ + dataValidatingFactory.accountIsNotSystem( + for: getRecepientAccountId(), + locale: selectedLocale + ), + dataValidatingFactory.willBeReaped( amount: sendingAmount, fee: isOriginUtilityTransfer ? originFee : 0, @@ -403,7 +408,7 @@ extension CrossChainTransferSetupPresenter: TransferSetupChildPresenterProtocol minBalance: originSendingMinBalance, locale: selectedLocale ) - ) + ]) validators.append( phishingValidatingFactory.notPhishing( diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift index 9b0b533c4e..306f06f6d9 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift @@ -329,7 +329,12 @@ extension OnChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { selectedLocale: selectedLocale ) - validators.append( + validators.append(contentsOf: [ + dataValidatingFactory.accountIsNotSystem( + for: getRecepientAccountId(), + locale: selectedLocale + ), + dataValidatingFactory.willBeReaped( amount: sendingAmount, fee: isUtilityTransfer ? fee?.value : 0, @@ -337,7 +342,7 @@ extension OnChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { minBalance: sendingAssetExistence?.minBalance, locale: selectedLocale ) - ) + ]) validators.append( phishingValidatingFactory.notPhishing( diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 0e79f63ad4..c0e4ead636 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1294,3 +1294,5 @@ "common.refresh.fee" = "Refresh fee"; "token.add.remote.exist.message" = "The entered contract address is present in Nova as a %@ token. Are you sure you want to modify it?"; "update.token.completion.message" = "%@ token updated"; +"send.system.account.title" = "Tokens will be lost"; +"send.system.account.message" = "Recipient is a system account. It is not controlled by any company or individual. Are you sure you still want to perform this transfer?"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 16a6238c60..6fb62a4758 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1293,3 +1293,5 @@ "evm.transaction.fee.too.high.message" = "Подсчитанная комиссия сети​ (%@) получилась намного чем комиссия сети по умолчанию (%@). Это может быть из-за временной нагрузки на сеть. Вы можете обновить комиссию для получения меньшего значения."; "common.refresh.fee" = "Обновить"; "token.add.remote.exist.message" = "Адрес смарт-контракта добавлен в Nova как %@ токен. Вы уверены, что хотите изменить его?"; +"send.system.account.title" = "Токены будут потеряны"; +"send.system.account.message" = "Получатель является системным аккаунтом. Он не управляется компанией и не является персональным. Вы уверены, что всё ещё хотите сделать перевод?"; From c10b6bda74e425ad480b70dac17fa71a51070f81 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 14:44:33 +0300 Subject: [PATCH 28/31] fix translation --- novawallet/ru.lproj/Localizable.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 6fb62a4758..6237d9a2e7 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1294,4 +1294,4 @@ "common.refresh.fee" = "Обновить"; "token.add.remote.exist.message" = "Адрес смарт-контракта добавлен в Nova как %@ токен. Вы уверены, что хотите изменить его?"; "send.system.account.title" = "Токены будут потеряны"; -"send.system.account.message" = "Получатель является системным аккаунтом. Он не управляется компанией и не является персональным. Вы уверены, что всё ещё хотите сделать перевод?"; +"send.system.account.message" = "Получатель является системным аккаунтом. Данный аккаунт не управляется компанией и не является персональным. Вы уверены, что всё ещё хотите сделать перевод?"; From 8de90b3587438eda987ec22b45c014d24bb26330 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 18:01:59 +0300 Subject: [PATCH 29/31] don't present unsupported method error for dapp --- .../States/WalletConnectBaseState.swift | 4 +++- .../WalletConnectStateAuthorizing.swift | 7 ++++--- .../States/WalletConnectStateInitiating.swift | 2 +- .../States/WalletConnectStateNewMessage.swift | 21 +++++++++---------- .../States/WalletConnectStateReady.swift | 3 ++- .../States/WalletConnectStateSigning.swift | 6 +++--- .../Transport/WalletConnectTransport.swift | 2 +- 7 files changed, 24 insertions(+), 21 deletions(-) diff --git a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectBaseState.swift b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectBaseState.swift index 1bea9dd250..aa79124b58 100644 --- a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectBaseState.swift +++ b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectBaseState.swift @@ -2,9 +2,11 @@ import Foundation class WalletConnectBaseState { weak var stateMachine: WalletConnectStateMachineProtocol? + let logger: LoggerProtocol - init(stateMachine: WalletConnectStateMachineProtocol) { + init(stateMachine: WalletConnectStateMachineProtocol, logger: LoggerProtocol) { self.stateMachine = stateMachine + self.logger = logger } func emitUnexpected(message: Any, nextState: WalletConnectStateProtocol) { diff --git a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateAuthorizing.swift b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateAuthorizing.swift index 5bced6f32b..189e46376d 100644 --- a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateAuthorizing.swift +++ b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateAuthorizing.swift @@ -8,12 +8,13 @@ class WalletConnectStateAuthorizing: WalletConnectBaseState { init( proposal: Session.Proposal, resolution: WalletConnectProposalResolution, - stateMachine: WalletConnectStateMachineProtocol + stateMachine: WalletConnectStateMachineProtocol, + logger: LoggerProtocol ) { self.proposal = proposal self.resolution = resolution - super.init(stateMachine: stateMachine) + super.init(stateMachine: stateMachine, logger: logger) } private func save( @@ -76,7 +77,7 @@ extension WalletConnectStateAuthorizing: WalletConnectStateProtocol { return } - let nextState = WalletConnectStateReady(stateMachine: stateMachine) + let nextState = WalletConnectStateReady(stateMachine: stateMachine, logger: logger) save( authResponse: response, diff --git a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateInitiating.swift b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateInitiating.swift index 7b3e7dfe27..7371663360 100644 --- a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateInitiating.swift +++ b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateInitiating.swift @@ -27,7 +27,7 @@ extension WalletConnectStateInitiating: WalletConnectStateProtocol { let chainIds = dataSource.chainsStore.availableChainIds() if !chainIds.isEmpty { - stateMachine.emit(nextState: WalletConnectStateReady(stateMachine: stateMachine)) + stateMachine.emit(nextState: WalletConnectStateReady(stateMachine: stateMachine, logger: logger)) } } } diff --git a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateNewMessage.swift b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateNewMessage.swift index d4e48f49c1..d5d890db59 100644 --- a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateNewMessage.swift +++ b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateNewMessage.swift @@ -24,11 +24,12 @@ final class WalletConnectStateNewMessage: WalletConnectBaseState { init( message: WalletConnectTransportMessage, - stateMachine: WalletConnectStateMachineProtocol + stateMachine: WalletConnectStateMachineProtocol, + logger: LoggerProtocol ) { self.message = message - super.init(stateMachine: stateMachine) + super.init(stateMachine: stateMachine, logger: logger) } private func process(proposal: Session.Proposal, dataSource: DAppStateDataSource) { @@ -55,7 +56,8 @@ final class WalletConnectStateNewMessage: WalletConnectBaseState { let nextState = WalletConnectStateAuthorizing( proposal: proposal, resolution: resolution, - stateMachine: stateMachine + stateMachine: stateMachine, + logger: logger ) stateMachine.emit(authRequest: authRequest, nextState: nextState) @@ -72,7 +74,7 @@ final class WalletConnectStateNewMessage: WalletConnectBaseState { stateMachine.emit( signDecision: decision, - nextState: WalletConnectStateReady(stateMachine: stateMachine), + nextState: WalletConnectStateReady(stateMachine: stateMachine, logger: logger), error: error ) } @@ -128,15 +130,12 @@ final class WalletConnectStateNewMessage: WalletConnectBaseState { } guard let method = WalletConnectMethod(rawValue: request.method) else { - rejectRequest(request: request, reason: "unsupported method \(request.method)") + logger.warning("Rejecting unsupported method: \(request.method)") + rejectRequest(request: request, reason: nil) return } - guard - let chain = WalletConnectModelFactory.resolveChain( - for: request.chainId, - chainsStore: chainsStore - ) else { + guard let chain = WalletConnectModelFactory.resolveChain(for: request.chainId, chainsStore: chainsStore) else { rejectRequest(request: request, reason: "unsupported chain id: \(request.chainId)") return } @@ -171,7 +170,7 @@ final class WalletConnectStateNewMessage: WalletConnectBaseState { operationData: operationData ) - let nextState = WalletConnectStateSigning(request: request, stateMachine: stateMachine) + let nextState = WalletConnectStateSigning(request: request, stateMachine: stateMachine, logger: logger) stateMachine.emit( signingRequest: signingRequest, diff --git a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateReady.swift b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateReady.swift index 68748eeb43..85821534f4 100644 --- a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateReady.swift +++ b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateReady.swift @@ -14,7 +14,8 @@ extension WalletConnectStateReady: WalletConnectStateProtocol { let nextState = WalletConnectStateNewMessage( message: message, - stateMachine: stateMachine + stateMachine: stateMachine, + logger: logger ) stateMachine.emit(nextState: nextState) diff --git a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift index c849a9e661..f698dfaab9 100644 --- a/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift +++ b/novawallet/Modules/DApp/WalletConnect/States/WalletConnectStateSigning.swift @@ -5,10 +5,10 @@ import SubstrateSdk final class WalletConnectStateSigning: WalletConnectBaseState { let request: Request - init(request: Request, stateMachine: WalletConnectStateMachineProtocol) { + init(request: Request, stateMachine: WalletConnectStateMachineProtocol, logger: LoggerProtocol) { self.request = request - super.init(stateMachine: stateMachine) + super.init(stateMachine: stateMachine, logger: logger) } } @@ -26,7 +26,7 @@ extension WalletConnectStateSigning: WalletConnectStateProtocol { return } - let nextState = WalletConnectStateReady(stateMachine: stateMachine) + let nextState = WalletConnectStateReady(stateMachine: stateMachine, logger: logger) if let signature = response.signature, diff --git a/novawallet/Modules/DApp/WalletConnect/Transport/WalletConnectTransport.swift b/novawallet/Modules/DApp/WalletConnect/Transport/WalletConnectTransport.swift index cdf4558872..fc154af18e 100644 --- a/novawallet/Modules/DApp/WalletConnect/Transport/WalletConnectTransport.swift +++ b/novawallet/Modules/DApp/WalletConnect/Transport/WalletConnectTransport.swift @@ -203,7 +203,7 @@ extension WalletConnectTransport { service.delegate = self service.setup() - state = WalletConnectStateInitiating(stateMachine: self) + state = WalletConnectStateInitiating(stateMachine: self, logger: logger) state?.proceed(with: dataSource) } From 6329d01b39153b1bfaf24f2864c337e7bb2642eb Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 16 Aug 2023 18:13:47 +0300 Subject: [PATCH 30/31] don't display unsupported method error when communicating with dapp --- .../DAppOperationConfirm/DAppOperationConfirmPresenter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift index 6db7a013b1..311e7a66f8 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmPresenter.swift @@ -80,7 +80,7 @@ final class DAppOperationConfirmPresenter { let rejectAction = AlertPresentableAction( title: R.string.localizable.commonReject(preferredLanguages: selectedLocale.rLanguages) ) { [weak self] in - self?.refreshFee() + self?.interactor.reject() } let errorContent = error.toErrorContent(for: selectedLocale) From dac760dbcb8fe2f35ece08dad3eb405fb73f1e8d Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 17 Aug 2023 14:58:21 +0300 Subject: [PATCH 31/31] fix localization --- novawallet/en.lproj/Localizable.strings | 50 ++++++++++++------------- novawallet/ru.lproj/Localizable.strings | 49 ++++++++++++------------ 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index c0e4ead636..9d182bb243 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -270,25 +270,6 @@ "staking.reward.details.reward" = "Reward"; "staking.reward" = "Reward"; "staking.slash" = "Slash"; -"staking.reward.filters.period.header" = "Show staking rewards"; -"staking.reward.filters.period.select.date" = "Select date"; -"staking.reward.filters.period.end.date.open" = "End date is always today"; -"staking.reward.filters.period.date.start" = "Starts"; -"staking.reward.filters.period.date.end" = "Ends"; -"staking.reward.filters.period.all.time" = "All time"; -"staking.reward.filters.period.last.week" = "Last 7 days (7D)"; -"staking.reward.filters.period.last.month" = "Last 30 days (30D)"; -"staking.reward.filters.period.last.three.months" = "Last 3 month (3M)"; -"staking.reward.filters.period.last.six.months" = "Last 6 months (6M)"; -"staking.reward.filters.period.last.year" = "Last year (1Y)"; -"staking.reward.filters.period.custom" = "Custom period"; -"staking.reward.filters.period.all.time.short" = "All"; -"staking.reward.filters.period.last.week.short" = "7D"; -"staking.reward.filters.period.last.month.short" = "30D"; -"staking.reward.filters.period.last.three.months.short" = "3M"; -"staking.reward.filters.period.last.six.months.short" = "6M"; -"staking.reward.filters.period.last.year.short" = "1Y"; -"staking.reward.filters.period.custom.month.short" = "%@D"; "common.call" = "Call"; "common.module" = "Module"; "wallet.asset.buy.with" = "Buy with"; @@ -1095,7 +1076,7 @@ "crowdloan.empty.message_v3_9_1" = "Crowdloans information\nwill appear here when they start"; "common.error.no.data.retrieved_v3_9_1" = "No data retrieved"; "common.add.token" = "Add token"; -"add.token.already.exists.title" = "This token already exist"; +"add.token.already.exists.title" = "This token already exists"; "add.token.already.exists.message" = "The entered contract address is present in Nova as a %@ token."; "add.token.invalid.contract.address.title" = "Invalid contract address"; "add.token.invalid.network.contract.message" = "The entered contract address is not a %@ ERC-20 contract."; @@ -1262,6 +1243,24 @@ "governance.referendums.settings.title" = "Referenda"; "wallet.send.recipient.blocked.title" = "Recipient cannot accept transfer"; "wallet.send.recipient.blocked.message" = "Recipient has been blocked by token owner and cannot currently accept incoming transfers"; +"staking.reward.filters.period.date.end" = "Ends"; +"staking.reward.filters.period.end.date.open" = "End date is always today"; +"staking.reward.filters.period.date.start" = "Starts"; +"staking.reward.filters.period.custom" = "Custom period"; +"staking.reward.filters.period.last.year" = "Last year (1Y)"; +"staking.reward.filters.period.last.six.months" = "Last 6 months (6M)"; +"staking.reward.filters.period.last.three.months" = "Last 3 month (3M)"; +"staking.reward.filters.period.last.month" = "Last 30 days (30D)"; +"staking.reward.filters.period.last.week" = "Last 7 days (7D)"; +"staking.reward.filters.period.all.time" = "All time"; +"staking.reward.filters.period.header" = "Show staking rewards for"; +"staking.reward.filters.period.last.year.short" = "1Y"; +"staking.reward.filters.period.last.six.months.short" = "6M"; +"staking.reward.filters.period.last.three.months.short" = "3M"; +"staking.reward.filters.period.last.month.short" = "30D"; +"staking.reward.filters.period.last.week.short" = "7D"; +"staking.reward.filters.period.all.time.short" = "All"; +"staking.reward.filters.period.select.date" = "Select date"; "governance.referendums.filter.empty" = "There are no referenda with filters applied"; "common.polkadot.vault" = "Polkadot Vault"; "welcome.polkadot.vault.step1" = "Open Polkadot Vault application on your smartphone"; @@ -1289,10 +1288,11 @@ "asset.operation.send.title" = "Send"; "asset.operation.receive.title" = "Receive"; "asset.operation.buy.title" = "Buy"; -"evm.transaction.fee.too.high.title" = "Network fee too high"; -"evm.transaction.fee.too.high.message" = "The estimated network fee​ (%@) is much higher than the default network fee (%@). This might be due to temporary network congestion. You can refresh to wait for a lower network fee."; -"common.refresh.fee" = "Refresh fee"; "token.add.remote.exist.message" = "The entered contract address is present in Nova as a %@ token. Are you sure you want to modify it?"; -"update.token.completion.message" = "%@ token updated"; +"send.system.account.message" = "Recipient is a system account. It is not controlled by any company or individual.\nAre you sure you still want to perform this transfer?"; "send.system.account.title" = "Tokens will be lost"; -"send.system.account.message" = "Recipient is a system account. It is not controlled by any company or individual. Are you sure you still want to perform this transfer?"; +"common.refresh.fee" = "Refresh fee"; +"evm.transaction.fee.too.high.message" = "The estimated network fee %@ is much higher than the default network fee (%@). This might be due to temporary network congestion. You can refresh to wait for a lower network fee."; +"evm.transaction.fee.too.high.title" = "Network fee is too high"; +"update.token.completion.message" = "%@ token updated"; +"staking.reward.filters.period.custom.month.short" = "%@D"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 6237d9a2e7..002843d895 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -270,25 +270,6 @@ "staking.reward.details.reward" = "Вознаграждение"; "staking.reward" = "Вознаграждение"; "staking.slash" = "Слэш"; -"staking.reward.filters.period.header" = "Показывать награды за"; -"staking.reward.filters.period.select.date" = "Выбрать дату"; -"staking.reward.filters.period.end.date.open" = "Дата окончания всегда сегодня"; -"staking.reward.filters.period.date.start" = "Начинается"; -"staking.reward.filters.period.date.end" = "Заканчивается"; -"staking.reward.filters.period.all.time" = "За все время"; -"staking.reward.filters.period.last.week" = "Последние 7 дней (7Д)"; -"staking.reward.filters.period.last.month" = "Последние 30 дней (30Д)"; -"staking.reward.filters.period.last.three.months" = "Последние 3 месяца (3М)"; -"staking.reward.filters.period.last.six.months" = "Последние 6 месяцев (6М)"; -"staking.reward.filters.period.last.year" = "Последний год (1Г)"; -"staking.reward.filters.period.custom" = "Свой период"; -"staking.reward.filters.period.all.time.short" = "Все"; -"staking.reward.filters.period.last.week.short" = "7Д"; -"staking.reward.filters.period.last.month.short" = "30Д"; -"staking.reward.filters.period.last.three.months.short" = "3М"; -"staking.reward.filters.period.last.six.months.short" = "6М"; -"staking.reward.filters.period.last.year.short" = "1Г"; -"staking.reward.filters.period.custom.month.short" = "%@Д"; "common.call" = "Вызов"; "common.module" = "Модуль"; "wallet.asset.buy.with" = "Купить с"; @@ -1262,6 +1243,24 @@ "governance.referendums.settings.title" = "Референдумы"; "wallet.send.recipient.blocked.title" = "Получатель не может принять перевод"; "wallet.send.recipient.blocked.message" = "Получатель был заблокирован владельцем токена и в настоящее время не может принимать входящие переводы"; +"staking.reward.filters.period.date.end" = "Конец"; +"staking.reward.filters.period.end.date.open" = "Конечная дата всегда текущая"; +"staking.reward.filters.period.date.start" = "Начало"; +"staking.reward.filters.period.custom" = "Другой период"; +"staking.reward.filters.period.last.year" = "Последний год (1Г)"; +"staking.reward.filters.period.last.six.months" = "Последние 6 месяцев (6М)"; +"staking.reward.filters.period.last.three.months" = "Последние 3 месяца (3М)"; +"staking.reward.filters.period.last.month" = "Последние 30 дней (30Д)"; +"staking.reward.filters.period.last.week" = "Последние 7 дней (7Д)"; +"staking.reward.filters.period.all.time" = "Все время"; +"staking.reward.filters.period.header" = "Показывать вознаграждения за"; +"staking.reward.filters.period.last.year.short" = "1Г"; +"staking.reward.filters.period.last.six.months.short" = "6М"; +"staking.reward.filters.period.last.three.months.short" = "3М"; +"staking.reward.filters.period.last.month.short" = "30Д"; +"staking.reward.filters.period.last.week.short" = "7Д"; +"staking.reward.filters.period.all.time.short" = "Все"; +"staking.reward.filters.period.select.date" = "Выберите дату"; "governance.referendums.filter.empty" = "Не найдены референдумы с указанными фильтрами"; "common.polkadot.vault" = "Polkadot Vault"; "welcome.polkadot.vault.step1" = "Откройте приложение Polkadot Vault на смартфоне"; @@ -1289,9 +1288,11 @@ "asset.operation.send.title" = "Отправить"; "asset.operation.receive.title" = "Получить"; "asset.operation.buy.title" = "Купить"; -"evm.transaction.fee.too.high.title" = "Комиссия сети слишком большая"; -"evm.transaction.fee.too.high.message" = "Подсчитанная комиссия сети​ (%@) получилась намного чем комиссия сети по умолчанию (%@). Это может быть из-за временной нагрузки на сеть. Вы можете обновить комиссию для получения меньшего значения."; -"common.refresh.fee" = "Обновить"; -"token.add.remote.exist.message" = "Адрес смарт-контракта добавлен в Nova как %@ токен. Вы уверены, что хотите изменить его?"; +"token.add.remote.exist.message" = "Введенный адрес контракта присутствует в Nova как токен %@. Вы уверены, что хотите изменить его?"; +"send.system.account.message" = "Получатель является системным аккаунтом. Этот аккаунт не контролируется какой-либо компанией или частным лицом. \nВы уверены, что все еще хотите выполнить данный перевод?"; "send.system.account.title" = "Токены будут потеряны"; -"send.system.account.message" = "Получатель является системным аккаунтом. Данный аккаунт не управляется компанией и не является персональным. Вы уверены, что всё ещё хотите сделать перевод?"; +"common.refresh.fee" = "Обновить"; +"evm.transaction.fee.too.high.message" = "Предполагаемая комиссия %@ намного выше, чем комиссия по умолчанию (%@). Это может быть связано с временной перегрузкой сети. Вы можете обновить комиссию, чтобы дождаться более низкой суммы."; +"evm.transaction.fee.too.high.title" = "Комиссия сети слишком высокая"; +"update.token.completion.message" = "%@ токен обновлён"; +"staking.reward.filters.period.custom.month.short" = "%@Д";